[
  {
    "path": ".dockerignore",
    "content": "# https://docs.docker.com/engine/reference/builder/#dockerignore-file\n**\n\n# Scripts that run within Docker\n!build-bin/maybe_install_npm\n!build-bin/maven/maven_build\n!build-bin/maven/maven_build_or_unjar\n!build-bin/maven/maven_opts\n!build-bin/maven/maven_unjar\n!build-bin/docker/docker-healthcheck\n\n!docker/start-zipkin\n!/zipkin-server/target/zipkin-server-*exec.jar\n!/zipkin-server/target/zipkin-server-*slim.jar\n\n!docker/test-images/zipkin-activemq/start-activemq\n\n!docker/test-images/zipkin-cassandra/install.sh\n!docker/test-images/zipkin-cassandra/start-cassandra\n!zipkin-storage/cassandra/src/main/resources/*.cql\n\n!docker/test-images/zipkin-elasticsearch7/config/\n!docker/test-images/zipkin-elasticsearch7/start-elasticsearch\n!docker/test-images/zipkin-elasticsearch8/config/\n!docker/test-images/zipkin-elasticsearch8/start-elasticsearch\n\n!docker/test-images/zipkin-opensearch2/config/\n!docker/test-images/zipkin-opensearch2/start-opensearch\n\n!docker/test-images/zipkin-eureka/src/\n!docker/test-images/zipkin-eureka/pom.xml\n!docker/test-images/zipkin-eureka/start-eureka\n\n!docker/test-images/zipkin-kafka/install.sh\n!docker/test-images/zipkin-kafka/start-kafka-zookeeper\n\n!docker/test-images/zipkin-mysql/install.sh\n!docker/test-images/zipkin-mysql/start-mysql\n!zipkin-storage/mysql-v1/src/main/resources/mysql.sql\n\n!docker/test-images/zipkin-rabbitmq/config/\n\n!docker/test-images/zipkin-ui/nginx.conf\n!docker/test-images/zipkin-ui/start-nginx\n!zipkin-lens/target/zipkin-lens-*.jar\n\n!docker/test-images/zipkin-uiproxy/nginx.conf\n!docker/test-images/zipkin-uiproxy/start-nginx\n\n# Allow on-demand \"mvn package\". <modules> referenced in pom.xml must be added even if not built\n!zipkin/src/main/**\n!zipkin-collector/src/main/**\n!zipkin-collector/core/src/main/**\n!zipkin-collector/activemq/src/main/**\n!zipkin-collector/kafka/src/main/**\n!zipkin-collector/rabbitmq/src/main/**\n!zipkin-collector/scribe/src/main/**\n!zipkin-collector/pulsar/src/main/**\n!zipkin-junit5/src/main/**\n!zipkin-storage/src/main/**\n!zipkin-storage/cassandra/src/main/**\n!zipkin-storage/mysql-v1/src/main/**\n!zipkin-storage/elasticsearch/src/main/**\n!zipkin-server/src/main/**\n!zipkin-tests/src/main/**\n!zipkin-lens/javadoc/**\n!zipkin-lens/public/**\n!zipkin-lens/src/**\n!zipkin-lens/.linguirc\n!zipkin-lens/.npmrc\n!zipkin-lens/index.html\n!zipkin-lens/package-lock.json\n!zipkin-lens/package.json\n!zipkin-lens/pom.xml\n!zipkin-lens/tsconfig.json\n!zipkin-lens/vite.config.ts\n!**/pom.xml\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.java text=auto eol=lf\n*.bnd text=auto eol=lf\n\n*.sh text=auto eol=lf\n.*_profile text=auto eol=lf\n\n*.md text=auto eol=lf\n*.txt text=auto eol=lf\n\n*.yml text=auto eol=lf\n*.yaml text=auto eol=lf\n*.xml text=auto eol=lf\n*.properties text=auto eol=lf\n\n*.js text=auto eol=lf\n*.jsx text=auto eol=lf\n*.ts text=auto eol=lf\n*.tsx text=auto eol=lf\n*.json text=auto eol=lf\n*.mustache text=auto eol=lf\n*.css text=auto eol=lf\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing to Zipkin\n\nIf you would like to contribute code, fork this GitHub repository and\nsend a pull request (on a branch other than `master` or `gh-pages`).\n\nWhen submitting code, please apply [Square Code Style](https://github.com/square/java-code-styles).\n* If the settings import correctly, CodeStyle/Java will be named Square and use 2 space tab and indent, with 4 space continuation indent.\n\n## License\n\nBy contributing your code, you agree to license your contribution under\nthe terms of the [APLv2](../LICENSE).\n\nAll files are released with the Apache 2.0 license.\n\nIf you are adding a new file it should have a header like below. This\ncan be automatically added by running `./mvnw com.mycila:license-maven-plugin:format`.\n\n```\n/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n ```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "content": "---\nname: Bug\nabout: If you’ve found a bug, spend the time to write a failing test. Bugs with tests get fixed and stay fixed. If you have a solution in mind, skip raising an issue and open a pull request instead.\nlabels: bug\n---\n## Describe the Bug\nA clear and concise description of what the bug is. If you have a solution in mind, skip raising an issue and open a pull request instead.\n\nIf this is a UI issue...\n  * Attach a screen shot or animated gif showing what you think is wrong\n  * Include JSON of a trace that produces it, being careful to not include private data\n    * You can literally include the JSON in [triple-backticks](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax#quoting-code)\n    * Otherwise, you can use a [gist](https://gist.github.com/) or pastebin\n\nRegardless, the best is to spend some time to write a failing test. Bugs with tests get fixed and stay fixed.\n\n## Steps to Reproduce\nSteps to reproduce the behavior:\n\n## Expected Behaviour\nSuggest what you think correct behaviour should be. Note, it may be solved differently depending on the problem.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Question\n    url: https://gitter.im/openzipkin/zipkin\n    about: Please ask questions about how to do something or to understand why something isn't working on our Gitter chat - we'll be happy to respond there in detail. This issue tracker is not for questions.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.md",
    "content": "---\nname: Feature Request\nabout: Please first, look at existing issues to see if the feature has been requested before.\nlabels: enhancement\n---\nPlease first, look at [existing issues](https://github.com/openzipkin/zipkin/issues) to see if the feature has been requested before. If you don't find anything tell us what problem you’re trying to solve. Often a solution already exists! Don’t send pull requests to implement new features without first getting our support. Sometimes we leave features out on purpose to keep the project small.\n\n## Feature\nDescription of the feature\n\n## Rationale\nWhy would this feature help others besides me?\n\n## Example Scenario\nWhat kind of use cases would benefit from this feature?\n\n## Prior Art\n* Links to prior art\n* More links\n"
  },
  {
    "path": ".github/workflows/create_release.yml",
    "content": "---\nname: create_release\n\n# We create a release version on a trigger tag, regardless of if the commit is\n# documentation-only.\non:  # yamllint disable-line rule:truthy\n  push:\n    tags:  # e.g. release-1.2.3\n      - 'release-[0-9]+.[0-9]+.[0-9]+**'\n\njobs:\n  create_release:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n        with:\n          # Prevent use of implicit GitHub Actions read-only GITHUB_TOKEN\n          # because maven-release-plugin pushes commits to master.\n          token: ${{ secrets.GH_TOKEN }}\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: '17'  # earliest LTS supported by Spring Boot 3\n      - name: Cache local Maven repository\n        uses: actions/cache@v4\n        with:\n          path: ~/.m2/repository\n          key: ${{ runner.os }}-jdk-17-maven-${{ hashFiles('**/pom.xml') }}\n          restore-keys: ${{ runner.os }}-jdk-17-maven-\n      - name: Create Release\n        env:\n          # GH_USER=<user that created GH_TOKEN>\n          GH_USER: ${{ secrets.GH_USER }}\n          # GH_TOKEN=<hex token value>\n          # * makes release commits and tags\n          # * needs repo:status, public_repo\n          # * referenced in .settings.xml\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n        run: |  # GITHUB_REF will be refs/tags/release-MAJOR.MINOR.PATCH\n          build-bin/git/login_git &&\n          build-bin/maven/maven_release $(echo ${GITHUB_REF} | cut -d/ -f 3)\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "# yamllint --format github .github/workflows/deploy.yml\n---\nname: deploy\n\n# We deploy on master and release versions, regardless of if the commit is\n# documentation-only or not.\non:  # yamllint disable-line rule:truthy\n  push:\n    branches:\n      - master\n    # Don't deploy tags because the same commit for MAJOR.MINOR.PATCH is also\n    # on master: Redundant deployment of a release version will fail uploading.\n    tags-ignore:\n      - '*'\n\njobs:\n  deploy:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n        with:\n          # Prevent use of implicit GitHub Actions read-only GITHUB_TOKEN\n          # because javadoc_to_gh_pages pushes commits to the gh-pages branch.\n          token: ${{ secrets.GH_TOKEN }}\n          # Allow build-bin/javadoc_to_gh_pages to fetch and push gh-pages\n          # See https://github.com/actions/checkout/issues/578\n          fetch-depth: 0\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: '17'  # earliest LTS supported by Spring Boot 3\n      - name: Cache local Maven repository\n        uses: actions/cache@v4\n        with:\n          path: ~/.m2/repository\n          key: ${{ runner.os }}-jdk-17-maven-${{ hashFiles('**/pom.xml') }}\n          restore-keys: ${{ runner.os }}-jdk-17-maven-\n      - name: Cache NPM Packages\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-npm-packages-${{ hashFiles('zipkin-lens/package-lock.json') }}\n      # Don't attempt to cache Docker. Sensitive information can be stolen\n      # via forks, and login session ends up in ~/.docker. This is ok because\n      # we publish DOCKER_PARENT_IMAGE to ghcr.io, hence local to the runner.\n      - name: Deploy\n        env:\n          # GH_USER=<user that created GH_TOKEN>\n          GH_USER: ${{ secrets.GH_USER }}\n          # GH_TOKEN=<hex token value>\n          # * pushes gh-pages during build-bin/javadoc_to_gh_pages\n          # * pushes Docker images to ghcr.io\n          # * create via https://github.com/settings/tokens\n          # * needs repo:status, public_repo, write:packages, delete:packages\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}\n          # GPG_PASSPHRASE=<passphrase for GPG_SIGNING_KEY>\n          # * referenced in .settings.xml\n          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n          # SONATYPE_USER=<sonatype account token>\n          # * deploys snapshots and releases to Sonatype\n          # * needs access to io.zipkin via OSSRH-16669\n          # * generate via https://oss.sonatype.org/#profile;User%20Token\n          # * referenced in .settings.xml\n          SONATYPE_USER: ${{ secrets.SONATYPE_USER }}\n          # SONATYPE_PASSWORD=<password to sonatype account token>\n          #  * referenced in .settings.xml\n          SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}\n          # DOCKERHUB_USER=<typically dockerzipkindeployer>\n          #  * only push repos in openzipkin org to Docker Hub on release\n          DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}\n          # DOCKERHUB_TOKEN=<access token for DOCKERHUB_USER>\n          #  * Access Token from here https://hub.docker.com/settings/security\n          DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}\n        run: |  # GITHUB_REF = refs/heads/master or refs/tags/MAJOR.MINOR.PATCH\n          build-bin/configure_deploy &&\n          build-bin/deploy $(echo ${GITHUB_REF} | cut -d/ -f 3)\n"
  },
  {
    "path": ".github/workflows/docker_push.yml",
    "content": "---\nname: docker_push\n\n# We re-push docker on a trigger tag, regardless of if the commit is\n# documentation-only.\non:  # yamllint disable-line rule:truthy\n  push:\n    tags:  # e.g. docker-1.2.3\n      - 'docker-[0-9]+.[0-9]+.[0-9]+**'\n\njobs:\n  docker_push:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      # Don't attempt to cache Docker. Sensitive information can be stolen\n      # via forks, and login session ends up in ~/.docker. This is ok because\n      # we publish DOCKER_PARENT_IMAGE to ghcr.io, hence local to the runner.\n      - name: Docker Push\n        run: |  # GITHUB_REF = refs/tags/docker-MAJOR.MINOR.PATCH\n          build-bin/git/login_git &&\n          build-bin/docker/configure_docker_push &&\n          build-bin/docker_push $(echo ${GITHUB_REF} | cut -d/ -f 3)\n        env:\n          # GH_USER=<user that created GH_TOKEN>\n          GH_USER: ${{ secrets.GH_USER }}\n          # GH_TOKEN=<hex token value>\n          # * pushes Docker images to ghcr.io\n          # * create via https://github.com/settings/tokens\n          # * needs repo:status, public_repo, write:packages, delete:packages\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n          # DOCKERHUB_USER=<typically dockerzipkindeployer>\n          #  * only push repos in openzipkin org to Docker Hub on release\n          DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}\n          # DOCKERHUB_TOKEN=<access token for DOCKERHUB_USER>\n          #  * Access Token from here https://hub.docker.com/settings/security\n          DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "---\nname: lint\n\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths:\n      - '**/*.md'\n      - '.github/workflows/*.yml'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths:\n      - '**/*.md'\n      - '.github/workflows/*.yml'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  lint:\n    name: lint\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Lint\n        run: |\n          build-bin/configure_lint\n          build-bin/lint\n"
  },
  {
    "path": ".github/workflows/security.yml",
    "content": "---\nname: security\n\n# We don't scan documentation-only commits.\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  security:\n    name: security\n    runs-on: ubuntu-24.04  # newest available distribution, aka numbat\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - uses: actions/cache@v4\n        name: Cache Trivy Database\n        with:\n          path: .trivy\n          key: ${{ runner.os }}-trivy\n          restore-keys: ${{ runner.os }}-trivy\n      - name: Run Trivy vulnerability and secret scanner\n        uses: aquasecurity/trivy-action@master\n        id: trivy\n        env:  # See https://github.com/aquasecurity/trivy/discussions/7668\n          TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db\n          TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db\n        with:\n          scan-type: 'fs'\n          scan-ref: '.'  # scan the entire repository\n          scanners: vuln,secret\n          exit-code: '1'\n          severity: HIGH,CRITICAL\n          output: trivy-report.md\n          cache-dir: .trivy\n      - name: Set Summary\n        shell: bash\n        if: ${{ failure() && steps.trivy.conclusion == 'failure' }}\n        # Add the Trivy report to the summary\n        #\n        # Note: This will cause a workflow error if trivy-report.md > the step\n        # limit 1MiB. If this was due to too many CVEs, consider fixing them ;)\n        run: cat trivy-report.md >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "---\nname: test\n\n# We don't test documentation-only commits.\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  test:\n    name: test (JDK ${{ matrix.java_version }})\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    strategy:\n      fail-fast: false  # don't fail fast as some failures are LTS specific\n      matrix:  # match with maven-enforcer-plugin rules in pom.xml\n        include:\n          - java_version: 17  # earliest LTS supported by Spring Boot 3\n            maven_args: -Prelease -Dgpg.skip\n          - java_version: 21  # Most recent LTS\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: ${{ matrix.java_version }}\n      - name: Cache local Maven repository\n        uses: actions/cache@v4\n        with:\n          path: ~/.m2/repository\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-jdk-${{ matrix.java_version }}-maven-${{ hashFiles('**/pom.xml') }}\n          restore-keys: ${{ runner.os }}-jdk-${{ matrix.java_version }}-maven-\n      - name: Cache NPM Packages\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-npm-packages-${{ hashFiles('zipkin-lens/package-lock.json') }}\n      - name: Test without Docker\n        run: |\n          build-bin/maven_go_offline &&\n          build-bin/test -DexcludedGroups=docker ${{ matrix.maven_args }}\n\n  test_docker:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    strategy:\n      matrix:\n        include:\n          - name: zipkin-collector-activemq\n          - name: zipkin-collector-kafka\n          - name: zipkin-collector-rabbitmq\n          - name: zipkin-collector-pulsar\n          - name: zipkin-storage-cassandra\n          - name: zipkin-storage-elasticsearch\n          - name: zipkin-storage-mysql-v1\n          - name: zipkin-server\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: '21'  # Most recent LTS\n      - name: Cache local Maven repository\n        uses: actions/cache@v4\n        with:\n          path: ~/.m2/repository\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-jdk-${{ matrix.java_version }}-maven-${{ hashFiles('**/pom.xml') }}\n          restore-keys: ${{ runner.os }}-jdk-${{ matrix.java_version }}-maven-\n      # Don't attempt to cache Docker. Sensitive information can be stolen\n      # via forks, and login session ends up in ~/.docker. This is ok because\n      # we publish DOCKER_PARENT_IMAGE to ghcr.io, hence local to the runner.\n      - name: Test with Docker\n        # configure_test seeds NPM cache, which isn't needed for these tests.\n        #\n        # What we are doing here is configuring docker, then installing the\n        # module's dependencies prior to testing it with docker. This allows\n        # us to avoid running tests except the leaf module.\n        run: |\n          build-bin/docker/configure_docker &&\n          build-bin/maven/maven_go_offline &&\n          build-bin/maven/maven_build -pl :${{ matrix.name }} --am &&\n          build-bin/test -Dgroups=docker -pl :${{ matrix.name }}\n        env:\n          MAVEN_GOAL: install  # docker build needs dependencies in mavenLocal\n          MAVEN_CONFIG: '-Dlicense.skip=true'  # license check already run\n"
  },
  {
    "path": ".github/workflows/test_readme.yml",
    "content": "---\nname: test_readme\n\n# These test build commands mentioned in various README.md files.\n#\n# We don't test documentation-only commits.\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  zipkin-server:\n    name: zipkin-server/README.md ${{ matrix.name }}\n    runs-on: ${{ matrix.os }}\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        include:  # Not ubuntu as already tested as a part of the docker job\n          - name: macos\n            os: macos-14\n          - name: windows\n            os: windows-2022\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: '21'  # Most recent LTS\n          cache: 'maven'\n      - name: Cache NPM Packages\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-npm-packages-${{ hashFiles('zipkin-lens/package-lock.json') }}\n      - name: Execute Server Build  # command from zipkin-server/README.md\n        run: ./mvnw --also-make -pl zipkin-server clean package\n        env:\n          MAVEN_CONFIG: '-T1C -q --batch-mode -DskipTests'\n\n  docker:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    timeout-minutes: 20\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      # Remove apt repos that are known to break from time to time.\n      # See https://github.com/actions/virtual-environments/issues/323\n      - name: Remove broken apt repos\n        run: |\n          for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`\n          do sudo rm $apt_file\n          done\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'  # zulu as it supports a wide version range\n          java-version: '21'  # Most recent LTS\n          cache: 'maven'\n      # Don't attempt to cache Docker. Sensitive information can be stolen\n      # via forks, and login session ends up in ~/.docker. This is ok because\n      # we publish DOCKER_PARENT_IMAGE to ghcr.io, hence local to the runner.\n      - name: Cache NPM Packages\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          # yamllint disable-line rule:line-length\n          key: ${{ runner.os }}-npm-packages-${{ hashFiles('zipkin-lens/package-lock.json') }}\n      - name: Build zipkin-server  # redundant, but needed for docker/README.md\n        run: ./mvnw --also-make -pl zipkin-server clean package\n        env:\n          MAVEN_CONFIG: '-T1C -q --batch-mode -DskipTests'\n      - name: docker/README.md - openzipkin/zipkin\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin:test &&\n          docker run --rm --entrypoint=/bin/sh openzipkin/zipkin:test \\\n            -c 'cd BOOT-INF/lib && du -sk *aarch* *x86* *a64*' || true\n        env:\n          RELEASE_FROM_MAVEN_BUILD: true\n      - name: docker/README.md - openzipkin/zipkin-slim\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-slim:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-slim:test &&\n          docker run --rm --entrypoint=/bin/sh openzipkin/zipkin-slim:test \\\n            -c 'cd BOOT-INF/lib && du -sk *aarch* *x86* *a64*' || true\n        env:\n          DOCKER_TARGET: zipkin-slim\n          RELEASE_FROM_MAVEN_BUILD: true\n      - name: docker/test-images/zipkin-ui/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-ui:test\n          DOCKER_FILE: docker/test-images/zipkin-ui/Dockerfile\n          RELEASE_FROM_MAVEN_BUILD: true\n      - name: docker/test-images/zipkin-uiproxy/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-uiproxy:test\n          DOCKER_FILE: docker/test-images/zipkin-uiproxy/Dockerfile\n      - name: docker/test-images/zipkin-activemq/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-activemq:test\n          DOCKER_FILE: docker/test-images/zipkin-activemq/Dockerfile\n      - name: docker/test-images/zipkin-cassandra/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-cassandra:test\n          DOCKER_FILE: docker/test-images/zipkin-cassandra/Dockerfile\n      - name: docker/test-images/zipkin-elasticsearch7/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-elasticsearch7:test\n          DOCKER_FILE: docker/test-images/zipkin-elasticsearch7/Dockerfile\n      - name: docker/test-images/zipkin-elasticsearch8/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-elasticsearch8:test\n          DOCKER_FILE: docker/test-images/zipkin-elasticsearch8/Dockerfile\n      - name: docker/test-images/zipkin-opensearch2/README.md\n        run: |\n          build-bin/docker/docker_build ${DOCKER_TAG} &&\n          build-bin/docker/docker_test_image ${DOCKER_TAG}\n        env:\n          DOCKER_TAG: openzipkin/zipkin-opensearch2:test\n          DOCKER_FILE: docker/test-images/zipkin-opensearch2/Dockerfile\n      - name: docker/test-images/zipkin-eureka/README.md\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-eureka:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-eureka:test\n        env:\n          DOCKER_FILE: docker/test-images/zipkin-eureka/Dockerfile\n      - name: docker/test-images/zipkin-kafka/README.md\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-kafka:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-kafka:test\n        env:\n          DOCKER_FILE: docker/test-images/zipkin-kafka/Dockerfile\n      - name: docker/test-images/zipkin-mysql/README.md\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-mysql:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-mysql:test\n        env:\n          DOCKER_FILE: docker/test-images/zipkin-mysql/Dockerfile\n      - name: docker/test-images/zipkin-rabbitmq/README.md\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-rabbitmq:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-rabbitmq:test\n        env:\n          DOCKER_FILE: docker/test-images/zipkin-rabbitmq/Dockerfile\n      - name: docker/test-images/zipkin-pulsar/README.md\n        run: |\n          build-bin/docker/docker_build openzipkin/zipkin-pulsar:test &&\n          build-bin/docker/docker_test_image openzipkin/zipkin-pulsar:test\n        env:\n          DOCKER_FILE: docker/test-images/zipkin-pulsar/Dockerfile\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n#*\n*#\n.#*\ndependency-reduced-pom.xml\n.factorypath\n.classpath\n.project\n.settings/\n.springBeans\ntarget/\n_site/\n.idea\n*.iml\n*.swp\n# quickstart or temporary copies of zipkin's jar\n/*.jar\n# temporary directory used by build-bin/javadoc_to_gh_pages for building gh-pages\n/javadoc-builddir\n.DS_Store\n\n# This project does not use Gradle but some developers may use it to e.g., setup a Node environment.\n# It doesn't hurt to just exclude it here.\n.gradle\n\n# This project does not use Yarn but some developers may use it to e.g., start zipkin-lens dev server.\n# It doesn't hurt to just exclude it here.\nyarn.lock\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\nwrapperVersion=3.3.2\ndistributionType=bin\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar\n"
  },
  {
    "path": ".settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<settings xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"\n    xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0\n                          http://maven.apache.org/xsd/settings-1.0.0.xsd\">\n  <servers>\n    <server>\n      <id>gpg.passphrase</id>\n      <passphrase>${env.GPG_PASSPHRASE}</passphrase>\n    </server>\n    <server>\n      <id>ossrh</id>\n      <username>${env.SONATYPE_USER}</username>\n      <password>${env.SONATYPE_PASSWORD}</password>\n    </server>\n    <server>\n      <id>github.com</id>\n      <username>zipkinci</username>\n      <password>${env.GH_TOKEN}</password>\n    </server>\n  </servers>\n</settings>\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\nThis product contains a modified part of Gson, distributed by Google:\n\n  * License: Apache License v2.0\n  * Homepage: https://github.com/google/gson\n\nThis product contains a modified part of Guava, distributed by Google:\n\n  * License: Apache License v2.0\n  * Homepage: https://github.com/google/guava\n\nThis product contains a modified part of Okio, distributed by Square:\n\n  * License: Apache License v2.0\n  * Homepage: https://github.com/square/okio\n"
  },
  {
    "path": "README.md",
    "content": "# zipkin\n\n[![Gitter chat](http://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/openzipkin/zipkin)\n[![Build Status](https://github.com/openzipkin/zipkin/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/openzipkin/zipkin/actions?query=workflow%3Atest+branch%3Amaster)\n[![Maven Central](https://img.shields.io/maven-central/v/io.zipkin/zipkin-server.svg)](https://central.sonatype.com/search?q=zipkin&namespace=io.zipkin&name=zipkin-server)\n\n[Zipkin](https://zipkin.io) is a distributed tracing system. It helps gather\ntiming data needed to troubleshoot latency problems in service architectures.\nFeatures include both the collection and lookup of this data.\n\nIf you have a trace ID in a log file, you can jump directly to it. Otherwise,\nyou can query based on attributes such as service, operation name, tags and\nduration. Some interesting data will be summarized for you, such as the\npercentage of time spent in a service, and whether operations failed.\n\n<img src=\"https://zipkin.io/public/img/web-screenshot.png\" alt=\"Trace view screenshot\" />\n\nThe Zipkin UI also presents a dependency diagram showing how many traced\nrequests went through each application. This can be helpful for identifying\naggregate behavior including error paths or calls to deprecated services.\n\n<img src=\"https://zipkin.io/public/img/dependency-graph.png\" alt=\"Dependency graph screenshot\" />\n\nApplication’s need to be “instrumented” to report trace data to Zipkin. This\nusually means configuration of a [tracer or instrumentation library](https://zipkin.io/pages/tracers_instrumentation.html). The most\npopular ways to report data to Zipkin are via http or Kafka, though many other\noptions exist, such as Apache ActiveMQ, gRPC, RabbitMQ and Apache Pulsar. The data served to\nthe UI is stored in-memory, or persistently with a supported backend such as\nApache Cassandra or Elasticsearch.\n\n## Quick-start\n\nThe quickest way to get started is to fetch the [latest released server](https://central.sonatype.com/search?q=zipkin&namespace=io.zipkin&name=zipkin-server&sort=published) as a self-contained\nexecutable jar. Note that the Zipkin server requires minimum JRE 17+. For example:\n\n```bash\ncurl -sSL https://zipkin.io/quickstart.sh | bash -s\njava -jar zipkin.jar\n```\n\nYou can also start Zipkin via Docker.\n```bash\n# Note: this is mirrored as ghcr.io/openzipkin/zipkin\ndocker run -d -p 9411:9411 openzipkin/zipkin\n```\n\nOnce the server is running, you can view traces with the Zipkin UI at http://localhost:9411/zipkin.\n\nIf your applications aren't sending traces, yet, configure them with [Zipkin instrumentation](https://zipkin.io/pages/tracers_instrumentation) or try one of our [examples](https://github.com/openzipkin?utf8=%E2%9C%93&q=example).\n\nCheck out the [`zipkin-server`](zipkin-server/README.md) documentation for configuration details, or [Docker examples](docker/examples) for how to use docker-compose.\n\n### Zipkin Slim\n\nThe slim build of Zipkin is smaller and starts faster. It supports in-memory and Elasticsearch storage, but doesn't support messaging transports like Kafka or RabbitMQ. If these constraints match your needs, you can try slim like below:\n\nRunning via Java:\n```bash\ncurl -sSL https://zipkin.io/quickstart.sh | bash -s io.zipkin:zipkin-server:LATEST:slim zipkin.jar\njava -jar zipkin.jar\n```\n\nRunning via Docker:\n```bash\n# Note: this is mirrored as ghcr.io/openzipkin/zipkin-slim\ndocker run -d -p 9411:9411 openzipkin/zipkin-slim\n```\n\nRunning via [Homebrew](https://formulae.brew.sh/formula/zipkin):\n```bash\nbrew install zipkin\n# to run in foreground\nzipkin\n# to run in background\nbrew services start zipkin\n```\n\n## Core Library\nThe [core library](zipkin/src/main/java/zipkin2) is used by both Zipkin instrumentation and the Zipkin server.\n\nThis includes built-in codec for Zipkin's v1 and v2 json formats. A direct dependency on gson\n(json library) is avoided by minifying and repackaging classes used. The result is a 155k jar which\nwon't conflict with any library you use.\n\nEx.\n```java\n// All data are recorded against the same endpoint, associated with your service graph\nlocalEndpoint = Endpoint.newBuilder().serviceName(\"tweetie\").ip(\"192.168.0.1\").build()\nspan = Span.newBuilder()\n    .traceId(\"d3d200866a77cc59\")\n    .id(\"d3d200866a77cc59\")\n    .name(\"targz\")\n    .localEndpoint(localEndpoint)\n    .timestamp(epochMicros())\n    .duration(durationInMicros)\n    .putTag(\"compression.level\", \"9\");\n\n// Now, you can encode it as json\nbytes = SpanBytesEncoder.JSON_V2.encode(span);\n```\n\nNote: The above is just an example, most likely you'll want to use an existing tracing library like [Brave](https://github.com/openzipkin/brave)\n\n### Core Library Requires Java 8+\n\nThe minimum Java language level of the core library is 8. This helps support those writing agent\ninstrumentation. Version 2.x was the last to support Java 6.\n\n*Note*: [zipkin-reporter-brave](https://github.com/openzipkin/zipkin-reporter-java/blob/master/brave/README.md)\ndoes not use this library. So, [brave](https://github.com/openzipkin/brave) still supports Java 6.\n\n## Storage Component\nZipkin includes a [StorageComponent](zipkin/src/main/java/zipkin2/storage/StorageComponent.java), used to store and query spans and\ndependency links. This is used by the server and those making collectors, or span reporters.\nFor this reason, storage components have minimal dependencies, though require Java 17+.\n\nEx.\n```java\n// this won't create network connections\nstorage = ElasticsearchStorage.newBuilder()\n                              .hosts(asList(\"http://myelastic:9200\")).build();\n\n// prepare a call\ntraceCall = storage.spanStore().getTrace(\"d3d200866a77cc59\");\n\n// execute it synchronously or asynchronously\ntrace = traceCall.execute();\n\n// clean up any sessions, etc\nstorage.close();\n```\n\n### In-Memory\nThe [InMemoryStorage](zipkin-server#in-memory-storage) component is packaged in zipkin's core library. It\nis neither persistent, nor viable for realistic work loads. Its purpose\nis for testing, for example starting a server on your laptop without any\ndatabase needed.\n\n### Cassandra\nThe [Cassandra](zipkin-server#cassandra-storage) component uses Cassandra\n3.11.3+ features, but is tested against the latest patch of Cassandra 4.1.\n\nThis is the second generation of our Cassandra schema. It stores spans\nusing UDTs, such that they appear like Zipkin v2 json in cqlsh. It is\ndesigned for scale, and uses a combination of SASI and manually\nimplemented indexes to make querying larger data more performant.\n\nNote: This store requires a [job to aggregate](https://github.com/openzipkin/zipkin-dependencies) dependency links.\n\n### Elasticsearch\nThe [Elasticsearch](zipkin-server#elasticsearch-storage) component uses\nElasticsearch 5+ features, but is tested against Elasticsearch 7-8.x and\nOpenSearch 2.x.\n\nIt stores spans as Zipkin v2 json so that integration with other tools is\nstraightforward. To help with scale, this uses a combination of custom\nand manually implemented indexing.\n\nNote: This store requires a [spark job](https://github.com/openzipkin/zipkin-dependencies) to aggregate dependency links.\n\n### Disabling search\nThe following API endpoints provide search features, and are enabled by\ndefault. Search primarily allows the trace list screen of the UI operate.\n* `GET /services` - Distinct Span.localServiceName\n* `GET /remoteServices?serviceName=X` - Distinct Span.remoteServiceName by Span.localServiceName\n* `GET /spans?serviceName=X` - Distinct Span.name by Span.localServiceName\n* `GET /autocompleteKeys` - Distinct keys of Span.tags subject to configurable whitelist\n* `GET /autocompleteValues?key=X` - Distinct values of Span.tags by key\n* `GET /traces` - Traces matching a query possibly including the above criteria\n\n\nWhen search is disabled, traces can only be retrieved by ID\n(`GET /trace/{traceId}`). Disabling search is only viable when there is\nan alternative way to find trace IDs, such as logs. Disabling search can\nreduce storage costs or increase write throughput.\n\n`StorageComponent.Builder.searchEnabled(false)` is implied when a zipkin\nis run with the env variable `SEARCH_ENABLED=false`.\n\n### Legacy (v1) components\nThe following components are no longer encouraged, but exist to help aid\ntransition to supported ones. These are indicated as \"v1\" as they use\ndata layouts based on Zipkin's V1 Thrift model, as opposed to the\nsimpler v2 data model currently used.\n\n#### MySQL\nThe [MySQL v1](zipkin-storage/mysql-v1) component uses MySQL 5.6+\nfeatures, but is tested against MariaDB 10.11.\n\nThe schema was designed to be easy to understand and get started with;\nit was not designed for performance. Ex spans fields are columns, so\nyou can perform ad-hoc queries using SQL. However, this component has\n[known performance issues](https://github.com/openzipkin/zipkin/issues/1233): queries will eventually take seconds to return\nif you put a lot of data into it.\n\nThis store does not require a [job to aggregate](https://github.com/openzipkin/zipkin-dependencies) dependency links.\nHowever, running the job will improve performance of dependencies\nqueries.\n\n## Running the server from source\nThe [Zipkin server](zipkin-server) receives spans via HTTP POST and respond to queries\nfrom its UI. It can also run collectors, such as RabbitMQ or Kafka.\n\nTo run the server from the currently checked out source, enter the\nfollowing. JDK 17+ is required to compile the source.\n```bash\n# Build the server and also make its dependencies\n$ ./mvnw -q --batch-mode -DskipTests --also-make -pl zipkin-server clean install\n# Run the server\n$ java -jar ./zipkin-server/target/zipkin-server-*exec.jar\n```\n\n## Artifacts\nServer artifacts are under the maven group id `io.zipkin`\nLibrary artifacts are under the maven group id `io.zipkin.zipkin2`\n\n### Library Releases\nReleases are at [Sonatype](https://oss.sonatype.org/content/repositories/releases) and [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.zipkin%22)\n\n### Library Snapshots\nSnapshots are uploaded to [Sonatype](https://oss.sonatype.org/content/repositories/snapshots) after\ncommits to master.\n\n### Docker Images\nReleased versions of zipkin-server are published to Docker Hub as `openzipkin/zipkin` and GitHub\nContainer Registry as `ghcr.io/openzipkin/zipkin`. See [docker](docker) for details.\n\n### Helm Charts\nHelm charts are available via `helm repo add zipkin https://zipkin.io/zipkin-helm`.\nSee [zipkin-helm](https://github.com/openzipkin/zipkin-helm) for details.\n\n### Javadocs\nhttps://zipkin.io/zipkin contains versioned folders with JavaDocs published on each (non-PR) build, as well\nas releases.\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# OpenZipkin Release Process\n\nThis repo uses semantic versions. Please keep this in mind when choosing version numbers.\n\n1. **Verify all dependencies are up-to-date**\n\n   Before you start a release, make sure all dependencies are up-to-date, or are documented why not.\n   Pay special attention to the [security workflow](.github/workflows/security.yml), which should\n   run clean.\n\n1. **Alert others you are releasing**\n\n   There should be no commits made to master while the release is in progress (about 10 minutes). Before you start\n   a release, alert others on [gitter](https://gitter.im/openzipkin/zipkin) so that they don't accidentally merge\n   anything. If they do, and the build fails because of that, you'll have to recreate the release tag described below.\n\n1. **Push a git tag**\n\n   The trigger format is `release-MAJOR.MINOR.PATCH`, ex `git tag release-1.18.1 && git push origin release-1.18.1`.\n\n1. **Wait for CI**\n\n   The `release-MAJOR.MINOR.PATCH` tag triggers [`build-bin/maven/maven_release`](build-bin/maven/maven_release),\n   which creates commits, `MAJOR.MINOR.PATCH` tag, and increments the version (maven-release-plugin).\n\n   The `MAJOR.MINOR.PATCH` tag triggers [`build-bin/deploy`](build-bin/deploy), which does the following:\n     * Publishes jars to https://oss.sonatype.org/content/repositories/releases [`build-bin/maven/maven_deploy`](build-bin/maven/maven_deploy)\n       * Later, the same jars synchronize to Maven Central\n     * Publishes Javadoc to https://zipkin.io/brave into a versioned subdirectory\n     * Pushes images to Docker registries [`build-bin/docker_push`](build-bin/docker_push)\n\n   Notes:\n     * https://search.maven.org/ index will take longer than direct links like https://repo1.maven.org/maven2/io/zipkin\n\n## Credentials\n\nThe release process uses various credentials. If you notice something failing due to unauthorized,\nlook at the notes in [.github/workflows/deploy.yml] and check the [org secrets](https://github.com/organizations/openzipkin/settings/secrets/actions).\n\n### Troubleshooting invalid credentials\n\nIf you receive a '401 unauthorized' failure from OSSRH, it is likely\n`SONATYPE_USER` or `SONATYPE_PASSWORD` entries are invalid, or possibly the\nuser associated with them does not have rights to upload.\n\nThe least destructive test is to try to publish a snapshot manually. By passing\nthe values CI would use, you can kick off a snapshot from your laptop. This\nis a good way to validate that your unencrypted credentials are authorized.\n\nHere's an example of a snapshot deploy with specified credentials.\n```bash\n$ export GPG_TTY=$(tty) && GPG_PASSPHRASE=whackamole SONATYPE_USER=adrianmole SONATYPE_PASSWORD=ed6f20bde9123bbb2312b221 build-bin/build-bin/maven/maven_deploy\n```\n\n## Manually releasing\n\nIf for some reason, you lost access to CI or otherwise cannot get automation to work, bear in mind\nthis is a normal maven project, and can be released accordingly.\n\n*Note:* If [Sonatype is down](https://status.sonatype.com/), the below will not work.\n\n```bash\n# First, set variable according to your personal credentials. These would normally be assigned as\n# org secrets: https://github.com/organizations/openzipkin/settings/secrets/actions\nexport GPG_TTY=$(tty)\nexport GPG_PASSPHRASE=your_gpg_passphrase\nexport SONATYPE_USER=your_sonatype_account\nexport SONATYPE_PASSWORD=your_sonatype_password\nrelease_version=xx-version-to-release-xx\n\n# now from latest master, create the release. This creates and pushes the MAJOR.MINOR.PATCH tag\n./build-bin/maven/maven_release release-${release_version}\n\n# once this works, deploy the release\ngit checkout ${release_version}\n./build-bin/deploy\n\n# Finally, clean up\n./mvnw release:clean\ngit checkout master\ngit reset HEAD --hard\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# OpenZipkin Security Process\n\nThis document outlines the process for handling security concerns in OpenZipkin projects.\n\nAny vulnerability or misconfiguration detected in our [security workflow](.github/workflows/security.yml)\nshould be addressed as a normal pull request.\n\nOpenZipkin is a volunteer community and does not have a dedicated security team. There may be\nperiods where no volunteer is able to address a security concern. There is no SLA or warranty\noffered by volunteers. If you are a security researcher, please consider this before escalating.\n\nFor security concerns that are sensitive or otherwise outside the scope of public issues, please\ncontact zipkin-admin@googlegroups.com.\n"
  },
  {
    "path": "benchmarks/README.md",
    "content": "# zipkin-benchmarks\n\nThis module includes [JMH](http://openjdk.java.net/projects/code-tools/jmh/)\nbenchmarks for zipkin. You can use these to measure overhead.\n\n### Running the benchmark\nFrom the project directory, run this to build the benchmarks:\n\n```bash\n$ ./mvnw install -pl benchmarks -am -Dmaven.test.skip.exec=true\n```\n\nand the following to run them:\n\n```bash\n$ java -jar benchmarks/target/benchmarks.jar\n```\n"
  },
  {
    "path": "benchmarks/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>benchmarks</artifactId>\n  <name>Benchmarks</name>\n  <description>Benchmarks (JMH)</description>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <maven.compiler.release>17</maven.compiler.release>\n\n    <jmh.version>1.37</jmh.version>\n    <unpack-proto.directory>${project.build.directory}/main/proto</unpack-proto.directory>\n  </properties>\n\n  <!-- Apache Cassandra java-driver is typically behind on jackson and netty -->\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>io.netty</groupId>\n        <artifactId>netty-bom</artifactId>\n        <version>${netty.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <dependency>\n        <groupId>com.fasterxml.jackson</groupId>\n        <artifactId>jackson-bom</artifactId>\n        <version>${jackson.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <!-- All dependencies are marked test, because benchmarks are a form of test.\n       This module is skipped on -DskipTests, ensuring it isn't deployed. -->\n  <dependencies>\n    <!-- These are main deps as wire-maven-plugin isn't designed for tests -->\n    <dependency>\n      <groupId>com.squareup.wire</groupId>\n      <artifactId>wire-runtime-jvm</artifactId>\n      <version>${wire.version}</version>\n      <scope>provided</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.zipkin.proto3</groupId>\n      <artifactId>zipkin-proto3</artifactId>\n      <version>${zipkin-proto3.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.openjdk.jmh</groupId>\n      <artifactId>jmh-core</artifactId>\n      <version>${jmh.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- IntelliJ doesn't know to use annotation processing on tests without this -->\n    <dependency>\n      <groupId>org.openjdk.jmh</groupId>\n      <artifactId>jmh-generator-annprocess</artifactId>\n      <version>${jmh.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- for codec benchmarks -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.squareup.moshi</groupId>\n      <artifactId>moshi</artifactId>\n      <version>1.15.2</version>\n      <exclusions>\n        <!-- let wire control the okio version -->\n        <exclusion>\n          <groupId>com.squareup.okio</groupId>\n          <artifactId>okio</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.esotericsoftware</groupId>\n      <artifactId>kryo</artifactId>\n      <version>${kryo.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.google.protobuf</groupId>\n      <artifactId>protobuf-java</artifactId>\n      <version>3.25.8</version>\n      <scope>test</scope>\n    </dependency>\n\n    <!-- Ensure server benchmarks run with a consistent logger -->\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-server</artifactId>\n      <version>${project.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>org.slf4j</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>org.springframework.boot</groupId>\n          <artifactId>spring-boot-starter-log4j2</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-simple</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>jul-to-slf4j</artifactId>\n      <version>${slf4j.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-elasticsearch</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-tests</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <!-- zipkin-server optional dependencies for running ServerIntegratedBenchmark -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-cassandra</artifactId>\n      <version>${project.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>io.netty</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.apache.cassandra</groupId>\n      <artifactId>java-driver-core</artifactId>\n      <version>${java-driver.version}</version>\n      <scope>test</scope>\n      <exclusions>\n        <exclusion>\n          <groupId>io.netty</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-mysql-v1</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.mariadb.jdbc</groupId>\n      <artifactId>mariadb-java-client</artifactId>\n      <version>${mariadb-java-client.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.zaxxer</groupId>\n      <artifactId>HikariCP</artifactId>\n      <version>${HikariCP.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n      <version>${testcontainers.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <!-- use assembly instead of shade, as it naturally works for test-jar -->\n    <plugins>\n      <plugin>\n        <artifactId>maven-assembly-plugin</artifactId>\n        <version>${maven-assembly-plugin.version}</version>\n        <configuration>\n          <descriptors>\n            <descriptor>src/test/assembly/test-jar.xml</descriptor>\n          </descriptors>\n        </configuration>\n        <executions>\n          <execution>\n            <id>make-assembly</id>\n            <phase>package</phase>\n            <goals>\n              <goal>single</goal>\n            </goals>\n            <configuration>\n              <attach>true</attach>\n              <finalName>benchmarks</finalName>\n              <appendAssemblyId>false</appendAssemblyId>\n              <archive>\n                <manifest>\n                  <mainClass>org.openjdk.jmh.Main</mainClass>\n                </manifest>\n              </archive>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <artifactId>maven-compiler-plugin</artifactId>\n        <executions>\n          <execution>\n            <id>default-compile</id>\n            <phase>compile</phase>\n            <goals>\n              <goal>compile</goal>\n            </goals>\n            <configuration combine.self=\"override\">\n              <!-- instead of javac-with-errorprone -->\n              <compilerId>javac</compilerId>\n              <!-- scrub errorprone compiler args -->\n              <compilerArgs />\n            </configuration>\n          </execution>\n          <execution>\n            <id>test-compile</id>\n            <phase>test-compile</phase>\n            <goals>\n              <goal>testCompile</goal>\n            </goals>\n            <configuration combine.self=\"override\">\n              <!-- instead of javac-with-errorprone -->\n              <compilerId>javac</compilerId>\n              <!-- scrub errorprone compiler args -->\n              <compilerArgs />\n              <annotationProcessorPaths>\n                <path>\n                  <groupId>org.openjdk.jmh</groupId>\n                  <artifactId>jmh-generator-annprocess</artifactId>\n                  <version>${jmh.version}</version>\n                </path>\n              </annotationProcessorPaths>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <artifactId>maven-dependency-plugin</artifactId>\n      </plugin>\n      <plugin>\n        <groupId>de.m3y.maven</groupId>\n        <artifactId>wire-maven-plugin</artifactId>\n        <configuration>\n          <generatedSourceDirectory>${generated-proto.directory}</generatedSourceDirectory>\n        </configuration>\n      </plugin>\n    </plugins>\n  </build>\n</project>\n"
  },
  {
    "path": "benchmarks/src/test/assembly/test-jar.xml",
    "content": "<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.2.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd\">\n  <!-- This is an alternative to maven-shade-plugin, which makes an executable\n       jar from the test source tree. This was made due to lack of success\n       getting 'shadeTestJar' and similar configuration working. -->\n  <id>test-jar</id>\n  <formats>\n    <format>jar</format>\n  </formats>\n  <includeBaseDirectory>false</includeBaseDirectory>\n  <dependencySets>\n    <dependencySet>\n      <outputDirectory>/</outputDirectory>\n      <useProjectArtifact>true</useProjectArtifact>\n      <unpack>true</unpack>\n      <scope>test</scope>\n    </dependencySet>\n  </dependencySets>\n  <fileSets>\n    <fileSet>\n      <directory>${project.build.directory}/test-classes</directory>\n      <outputDirectory>/</outputDirectory>\n      <includes>\n        <include>**/*</include>\n      </includes>\n      <useDefaultExcludes>true</useDefaultExcludes>\n    </fileSet>\n  </fileSets>\n</assembly>\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/EndpointBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.net.Inet4Address;\nimport java.net.Inet6Address;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(2)\npublic class EndpointBenchmarks {\n  static final String IPV4 = \"43.0.192.2\", IPV6 = \"2001:db8::c001\";\n  static final InetAddress IPV4_ADDR, IPV6_ADDR;\n\n  static {\n    try {\n      IPV4_ADDR = Inet4Address.getByName(IPV4);\n      IPV6_ADDR = Inet6Address.getByName(IPV6);\n    } catch (UnknownHostException e) {\n      throw new AssertionError(e);\n    }\n  }\n\n  Endpoint.Builder builder = Endpoint.newBuilder();\n\n  @Benchmark public boolean parseIpv4_literal() {\n    return builder.parseIp(IPV4);\n  }\n\n  @Benchmark public boolean parseIpv4_addr() {\n    return builder.parseIp(IPV4_ADDR);\n  }\n\n  @Benchmark public boolean parseIpv4_bytes() {\n    return builder.parseIp(IPV4_ADDR.getAddress());\n  }\n\n  @Benchmark public boolean parseIpv6_literal() {\n    return builder.parseIp(IPV6);\n  }\n\n  @Benchmark public boolean parseIpv6_addr() {\n    return builder.parseIp(IPV6_ADDR);\n  }\n\n  @Benchmark public boolean parseIpv6_bytes() {\n    return builder.parseIp(IPV6_ADDR.getAddress());\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .addProfiler(\"gc\")\n      .include(\".*\" + EndpointBenchmarks.class.getSimpleName())\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/SpanBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport com.esotericsoftware.kryo.Kryo;\nimport com.esotericsoftware.kryo.io.Input;\nimport com.esotericsoftware.kryo.io.Output;\nimport com.esotericsoftware.kryo.serializers.JavaSerializer;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\n\nimport static zipkin2.internal.HexCodec.lowerHexToUnsignedLong;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(2)\npublic class SpanBenchmarks {\n  static final Endpoint FRONTEND =\n    Endpoint.newBuilder().serviceName(\"frontend\").ip(\"127.0.0.1\").build();\n  static final Endpoint BACKEND =\n    Endpoint.newBuilder().serviceName(\"backend\").ip(\"192.168.99.101\").port(9000).build();\n  static final Span clientSpan = buildClientSpan(Span.newBuilder());\n\n  final Span.Builder sharedBuilder;\n\n  public SpanBenchmarks() {\n    sharedBuilder = buildClientSpan().toBuilder();\n  }\n\n  static final String traceIdHex = \"86154a4ba6e91385\", spanIdHex = \"4d1e00c0db9010db\";\n  static final long traceId = lowerHexToUnsignedLong(traceIdHex);\n  static final long spanId = lowerHexToUnsignedLong(spanIdHex);\n\n  @Benchmark\n  public Span buildClientSpan() {\n    return buildClientSpan(Span.newBuilder());\n  }\n\n  @Benchmark\n  public Span buildClientSpan_longs() {\n    return buildClientSpan_longs(Span.newBuilder());\n  }\n\n  static Span buildClientSpan(Span.Builder builder) {\n    return builder\n      .traceId(traceIdHex)\n      .parentId(traceIdHex)\n      .id(spanIdHex)\n      .name(\"get\")\n      .kind(Span.Kind.CLIENT)\n      .localEndpoint(FRONTEND)\n      .remoteEndpoint(BACKEND)\n      .timestamp(1472470996199000L)\n      .duration(207000L)\n      .addAnnotation(1472470996238000L, \"ws\")\n      .addAnnotation(1472470996403000L, \"wr\")\n      .putTag(\"http.path\", \"/api\")\n      .putTag(\"clnt/finagle.version\", \"6.45.0\")\n      .build();\n  }\n\n  static Span buildClientSpan_longs(Span.Builder builder) {\n    return builder\n      .traceId(0L, traceId)\n      .parentId(traceId)\n      .id(spanId)\n      .name(\"get\")\n      .kind(Span.Kind.CLIENT)\n      .localEndpoint(FRONTEND)\n      .remoteEndpoint(BACKEND)\n      .timestamp(1472470996199000L)\n      .duration(207000L)\n      .addAnnotation(1472470996238000L, \"ws\")\n      .addAnnotation(1472470996403000L, \"wr\")\n      .putTag(\"http.path\", \"/api\")\n      .putTag(\"clnt/finagle.version\", \"6.45.0\")\n      .build();\n  }\n\n  @Benchmark\n  public Span buildClientSpan_clear() {\n    return buildClientSpan(sharedBuilder.clear());\n  }\n\n  @Benchmark\n  public Span buildClientSpan_clone() {\n    return sharedBuilder.clone().build();\n  }\n\n  static final Kryo kryo = new Kryo();\n  static final byte[] clientSpanSerialized;\n\n  static {\n    kryo.register(Span.class, new JavaSerializer());\n    Output output = new Output(4096);\n    kryo.writeObject(output, clientSpan);\n    output.flush();\n    clientSpanSerialized = output.getBuffer();\n  }\n\n  /** manually implemented with json so not as slow as normal java */\n  @Benchmark\n  public Span serialize_kryo() {\n    return kryo.readObject(new Input(clientSpanSerialized), Span.class);\n  }\n\n  @Benchmark\n  public byte[] deserialize_kryo() {\n    Output output = new Output(clientSpanSerialized.length);\n    kryo.writeObject(output, clientSpan);\n    output.flush();\n    return output.getBuffer();\n  }\n\n  @Benchmark\n  public String padLeft_1Char() {\n    return Span.padLeft(\"1\", 16);\n  }\n\n  @Benchmark\n  public String padLeft_15Chars() {\n    return Span.padLeft(\"123456789012345\", 16);\n  }\n\n  @Benchmark\n  public String padLeft_17Chars() {\n    return Span.padLeft(\"12345678901234567\", 32);\n  }\n\n  @Benchmark\n  public String padLeft_31Chars() {\n    return Span.padLeft(\"1234567890123456789012345678901\", 32);\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + SpanBenchmarks.class.getSimpleName() + \".*\")\n      .addProfiler(\"gc\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/CodecBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.storage.cassandra.internal.Resources.resourceToString;\n\n/**\n * The {@link SpanBytesEncoder bundled java codec} aims to be both small in size (i.e. does not\n * significantly increase the size of zipkin's jar), and efficient. It may not always be fastest,\n * but we should try to keep it competitive.\n *\n * <p>Note that the wire benchmarks use their structs, not ours. This will result in more efficient\n * writes as there's no hex codec of IDs, stringifying of IPs etc. A later change could do that, but\n * it likely still going to be more efficient than our dependency-free codec. This means in cases\n * where extra dependencies are ok (such as our server), we could consider using wire.\n */\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class CodecBenchmarks {\n  static final byte[] clientSpanJsonV2 = resourceToString(\"/zipkin2-client.json\").getBytes(UTF_8);\n  static final Span clientSpan = SpanBytesDecoder.JSON_V2.decodeOne(clientSpanJsonV2);\n  static final byte[] clientSpanJsonV1 = SpanBytesEncoder.JSON_V1.encode(clientSpan);\n  static final byte[] clientSpanProto3 = SpanBytesEncoder.PROTO3.encode(clientSpan);\n  static final byte[] clientSpanThrift = SpanBytesEncoder.THRIFT.encode(clientSpan);\n  static final List<Span> tenClientSpans = Collections.nCopies(10, clientSpan);\n  static final byte[] tenClientSpansJsonV2 = SpanBytesEncoder.JSON_V2.encodeList(tenClientSpans);\n\n  @Benchmark\n  public Span decodeClientSpan_JSON_V1() {\n    return SpanBytesDecoder.JSON_V1.decodeOne(clientSpanJsonV1);\n  }\n\n  @Benchmark\n  public Span decodeClientSpan_JSON_V2() {\n    return SpanBytesDecoder.JSON_V2.decodeOne(clientSpanJsonV2);\n  }\n\n  @Benchmark\n  public Span decodeClientSpan_PROTO3() {\n    return SpanBytesDecoder.PROTO3.decodeOne(clientSpanProto3);\n  }\n\n  @Benchmark\n  public Span decodeClientSpan_THRIFT() {\n    return SpanBytesDecoder.THRIFT.decodeOne(clientSpanThrift);\n  }\n\n  @Benchmark\n  public int sizeInBytesClientSpan_JSON_V2() {\n    return SpanBytesEncoder.JSON_V2.sizeInBytes(clientSpan);\n  }\n\n  @Benchmark\n  public int sizeInBytesClientSpan_JSON_V1() {\n    return SpanBytesEncoder.JSON_V1.sizeInBytes(clientSpan);\n  }\n\n  @Benchmark\n  public int sizeInBytesClientSpan_PROTO3() {\n    return SpanBytesEncoder.PROTO3.sizeInBytes(clientSpan);\n  }\n\n  @Benchmark\n  public int sizeInBytesClientSpan_THRIFT() {\n    return SpanBytesEncoder.THRIFT.sizeInBytes(clientSpan);\n  }\n\n  @Benchmark\n  public byte[] writeClientSpan_JSON_V2() {\n    return SpanBytesEncoder.JSON_V2.encode(clientSpan);\n  }\n\n  @Benchmark\n  public byte[] writeClientSpan_JSON_V1() {\n    return SpanBytesEncoder.JSON_V1.encode(clientSpan);\n  }\n\n  @Benchmark\n  public byte[] writeClientSpan_PROTO3() {\n    return SpanBytesEncoder.PROTO3.encode(clientSpan);\n  }\n\n  @Benchmark\n  public byte[] writeClientSpan_THRIFT() {\n    return SpanBytesEncoder.THRIFT.encode(clientSpan);\n  }\n\n  @Benchmark\n  public List<Span> decodeTenClientSpans_JSON_V2() {\n    return SpanBytesDecoder.JSON_V2.decodeList(tenClientSpansJsonV2);\n  }\n\n  @Benchmark\n  public byte[] writeTenClientSpans_JSON_V2() {\n    return SpanBytesEncoder.JSON_V2.encodeList(tenClientSpans);\n  }\n\n  static final byte[] chineseSpanJsonV2 = resourceToString(\"/zipkin2-chinese.json\").getBytes(UTF_8);\n  static final Span chineseSpan = SpanBytesDecoder.JSON_V2.decodeOne(chineseSpanJsonV2);\n  static final byte[] chineseSpanProto3 = SpanBytesEncoder.PROTO3.encode(chineseSpan);\n  static final byte[] chineseSpanJsonV1 = SpanBytesEncoder.JSON_V1.encode(chineseSpan);\n  static final byte[] chineseSpanThrift = SpanBytesEncoder.THRIFT.encode(chineseSpan);\n\n  @Benchmark\n  public Span decodeChineseSpan_JSON_V1() {\n    return SpanBytesDecoder.JSON_V1.decodeOne(chineseSpanJsonV1);\n  }\n\n  @Benchmark\n  public Span decodeChineseSpan_JSON_V2() {\n    return SpanBytesDecoder.JSON_V2.decodeOne(chineseSpanJsonV2);\n  }\n\n  @Benchmark\n  public Span decodeChineseSpan_PROTO3() {\n    return SpanBytesDecoder.PROTO3.decodeOne(chineseSpanProto3);\n  }\n\n  @Benchmark\n  public Span decodeChineseSpan_THRIFT() {\n    return SpanBytesDecoder.THRIFT.decodeOne(chineseSpanThrift);\n  }\n\n  @Benchmark\n  public byte[] writeChineseSpan_JSON_V2() {\n    return SpanBytesEncoder.JSON_V2.encode(chineseSpan);\n  }\n\n  @Benchmark\n  public byte[] writeChineseSpan_JSON_V1() {\n    return SpanBytesEncoder.JSON_V1.encode(chineseSpan);\n  }\n\n  @Benchmark\n  public byte[] writeChineseSpan_PROTO3() {\n    return SpanBytesEncoder.PROTO3.encode(chineseSpan);\n  }\n\n  @Benchmark\n  public byte[] writeChineseSpan_THRIFT() {\n    return SpanBytesEncoder.THRIFT.encode(chineseSpan);\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .addProfiler(\"gc\")\n      .include(\".*\" + CodecBenchmarks.class.getSimpleName())\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/JacksonSpanDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.ReadBuffer;\n\npublic final class JacksonSpanDecoder {\n\n  static final JsonFactory JSON_FACTORY = new JsonFactory();\n\n  public static List<Span> decodeList(byte[] spans) {\n    try {\n      return decodeList(JSON_FACTORY.createParser(spans));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  public static List<Span> decodeList(ByteBuffer spans) {\n    try {\n      return decodeList(JSON_FACTORY.createParser(ReadBuffer.wrapUnsafe(spans)));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  public static Span decodeOne(byte[] span) {\n    try {\n      return decodeOne(JSON_FACTORY.createParser(span));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  public static Span decodeOne(ByteBuffer span) {\n    try {\n      return decodeOne(JSON_FACTORY.createParser(ReadBuffer.wrapUnsafe(span)));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  static List<Span> decodeList(JsonParser jsonParser) {\n    List<Span> out = new ArrayList<>();\n\n    try {\n      if (jsonParser.nextToken() != JsonToken.START_ARRAY) {\n        throw new IOException(\"Not a valid JSON array, start token: \" + jsonParser.currentToken());\n      }\n\n      while (jsonParser.nextToken() != JsonToken.END_ARRAY) {\n        out.add(parseSpan(jsonParser));\n      }\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n\n    return out;\n  }\n\n  static Span decodeOne(JsonParser jsonParser) {\n    try {\n      if (!jsonParser.hasCurrentToken()) {\n        jsonParser.nextToken();\n      }\n      return parseSpan(jsonParser);\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  static Span parseSpan(JsonParser jsonParser) throws IOException {\n    if (!jsonParser.isExpectedStartObjectToken()) {\n      throw new IOException(\"Not a valid JSON object, start token: \" +\n        jsonParser.currentToken());\n    }\n\n    Span.Builder result = Span.newBuilder();\n\n    while (jsonParser.nextToken() != JsonToken.END_OBJECT) {\n      String fieldName = jsonParser.currentName();\n      JsonToken value = jsonParser.nextToken();\n      if (value == JsonToken.VALUE_NULL) {\n        continue;\n      }\n      switch (fieldName) {\n        case \"traceId\":\n          result.traceId(jsonParser.getValueAsString());\n          break;\n        case \"parentId\":\n          result.parentId(jsonParser.getValueAsString());\n          break;\n        case \"id\":\n          result.id(jsonParser.getValueAsString());\n          break;\n        case \"kind\":\n          result.kind(Span.Kind.valueOf(jsonParser.getValueAsString()));\n          break;\n        case \"name\":\n          result.name(jsonParser.getValueAsString());\n          break;\n        case \"timestamp\":\n          result.timestamp(jsonParser.getValueAsLong());\n          break;\n        case \"duration\":\n          result.duration(jsonParser.getValueAsLong());\n          break;\n        case \"localEndpoint\":\n          result.localEndpoint(parseEndpoint(jsonParser));\n          break;\n        case \"remoteEndpoint\":\n          result.remoteEndpoint(parseEndpoint(jsonParser));\n          break;\n        case \"annotations\":\n          if (!jsonParser.isExpectedStartArrayToken()) {\n            throw new IOException(\"Invalid span, expecting annotations array start, got: \" +\n              value);\n          }\n          while (jsonParser.nextToken() != JsonToken.END_ARRAY) {\n            Annotation a = parseAnnotation(jsonParser);\n            result.addAnnotation(a.timestamp(), a.value());\n          }\n          break;\n        case \"tags\":\n          if (value != JsonToken.START_OBJECT) {\n            throw new IOException(\"Invalid span, expecting tags object, got: \" + value);\n          }\n          while (jsonParser.nextToken() != JsonToken.END_OBJECT) {\n            result.putTag(jsonParser.currentName(), jsonParser.nextTextValue());\n          }\n          break;\n        case \"debug\":\n          result.debug(jsonParser.getBooleanValue());\n          break;\n        case \"shared\":\n          result.shared(jsonParser.getBooleanValue());\n          break;\n        default:\n          jsonParser.skipChildren();\n      }\n    }\n\n    return result.build();\n  }\n\n  static Endpoint parseEndpoint(JsonParser jsonParser) throws IOException {\n    if (!jsonParser.isExpectedStartObjectToken()) {\n      throw new IOException(\"Not a valid JSON object, start token: \" +\n        jsonParser.currentToken());\n    }\n\n    String serviceName = null, ipv4 = null, ipv6 = null;\n    int port = 0;\n\n    while (jsonParser.nextToken() != JsonToken.END_OBJECT) {\n      String fieldName = jsonParser.currentName();\n      JsonToken value = jsonParser.nextToken();\n      if (value == JsonToken.VALUE_NULL) {\n        continue;\n      }\n\n      switch (fieldName) {\n        case \"serviceName\":\n          serviceName = jsonParser.getValueAsString();\n          break;\n        case \"ipv4\":\n          ipv4 = jsonParser.getValueAsString();\n          break;\n        case \"ipv6\":\n          ipv6 = jsonParser.getValueAsString();\n          break;\n        case \"port\":\n          port = jsonParser.getValueAsInt();\n          break;\n        default:\n          jsonParser.skipChildren();\n      }\n    }\n\n    if (serviceName == null && ipv4 == null && ipv6 == null && port == 0) return null;\n    return Endpoint.newBuilder()\n      .serviceName(serviceName)\n      .ip(ipv4)\n      .ip(ipv6)\n      .port(port)\n      .build();\n  }\n\n  static Annotation parseAnnotation(JsonParser jsonParser) throws IOException {\n    if (!jsonParser.isExpectedStartObjectToken()) {\n      throw new IOException(\"Not a valid JSON object, start token: \" +\n        jsonParser.currentToken());\n    }\n\n    long timestamp = 0;\n    String value = null;\n\n    while (jsonParser.nextToken() != JsonToken.END_OBJECT) {\n      String fieldName = jsonParser.currentName();\n\n      switch (fieldName) {\n        case \"timestamp\":\n          timestamp = jsonParser.getValueAsLong();\n          break;\n        case \"value\":\n          value = jsonParser.getValueAsString();\n          break;\n        default:\n          jsonParser.skipChildren();\n      }\n    }\n\n    if (timestamp == 0 || value == null) {\n      throw new IllegalStateException(\"Incomplete annotation at \" + jsonParser.currentToken());\n    }\n    return Annotation.create(timestamp, value);\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/JacksonSpanDecoderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.TRACE;\n\npublic class JacksonSpanDecoderTest {\n  byte[] encoded = SpanBytesEncoder.JSON_V2.encodeList(TRACE);\n  byte[] encodedSpan = SpanBytesEncoder.JSON_V2.encode(CLIENT_SPAN);\n\n  @Test void decodeList_bytes() {\n    assertThat(JacksonSpanDecoder.decodeList(encoded))\n      .isEqualTo(TRACE);\n  }\n\n  @Test void decodeList_byteBuffer() {\n    ByteBuf encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(encoded.length);\n    encodedBuf.writeBytes(encoded);\n    try {\n      assertThat(JacksonSpanDecoder.decodeList(encodedBuf.nioBuffer()))\n        .isEqualTo(TRACE);\n    } finally {\n      encodedBuf.release();\n    }\n  }\n\n  @Test void decodeOne() {\n    assertThat(JacksonSpanDecoder.decodeOne(encodedSpan))\n      .isEqualTo(CLIENT_SPAN);\n  }\n\n  @Test void decodeOne_byteBuffer() {\n    ByteBuf encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(encodedSpan.length);\n    encodedBuf.writeBytes(encodedSpan);\n    try {\n      assertThat(JacksonSpanDecoder.decodeOne(encodedBuf.nioBuffer()))\n        .isEqualTo(CLIENT_SPAN);\n    } finally {\n      encodedBuf.release();\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/JsonCodecBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.Setup;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.TearDown;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.storage.cassandra.internal.Resources.resourceToString;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class JsonCodecBenchmarks {\n  static final MoshiSpanDecoder MOSHI = MoshiSpanDecoder.create();\n\n  static final byte[] clientSpanJsonV2 = resourceToString(\"/zipkin2-client.json\").getBytes(UTF_8);\n  static final Span clientSpan = SpanBytesDecoder.JSON_V2.decodeOne(clientSpanJsonV2);\n\n  // Assume a message is 1000 spans (which is a high number for as this is per-node-second)\n  static final List<Span> spans = Collections.nCopies(1000, clientSpan);\n  static final byte[] encodedBytes = SpanBytesEncoder.JSON_V2.encodeList(spans);\n\n  private ByteBuf encodedBuf;\n\n  @Setup public void setup() {\n    encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(encodedBytes.length);\n    encodedBuf.writeBytes(encodedBytes);\n  }\n\n  @TearDown public void tearDown() {\n    encodedBuf.release();\n  }\n\n  @Benchmark public List<Span> bytes_jacksonDecoder() {\n    return JacksonSpanDecoder.decodeList(encodedBytes);\n  }\n\n  @Benchmark public List<Span> bytes_moshiDecoder() {\n    return MOSHI.decodeList(encodedBytes);\n  }\n\n  @Benchmark public List<Span> bytes_zipkinDecoder() {\n    return SpanBytesDecoder.JSON_V2.decodeList(encodedBytes);\n  }\n\n  @Benchmark public List<Span> bytebuffer_jacksonDecoder() {\n    return JacksonSpanDecoder.decodeList(encodedBuf.nioBuffer());\n  }\n\n  @Benchmark public List<Span> bytebuffer_moshiDecoder() {\n    return MOSHI.decodeList(encodedBuf.nioBuffer());\n  }\n\n  @Benchmark public List<Span> bytebuffer_zipkinDecoder() {\n    return SpanBytesDecoder.JSON_V2.decodeList(encodedBuf.nioBuffer());\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws Exception {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + JsonCodecBenchmarks.class.getSimpleName() + \".*\")\n      .addProfiler(\"gc\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/MoshiSpanDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport com.squareup.moshi.JsonAdapter;\nimport com.squareup.moshi.JsonReader;\nimport com.squareup.moshi.JsonWriter;\nimport com.squareup.moshi.Moshi;\nimport com.squareup.moshi.Types;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.List;\nimport okio.Buffer;\nimport okio.BufferedSource;\nimport okio.Okio;\nimport okio.Timeout;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\n\n/**\n * Read-only json adapters resurrected from before we switched to Java 6 as storage components can\n * be Java 7+\n */\npublic final class MoshiSpanDecoder {\n  final JsonAdapter<List<Span>> listSpansAdapter;\n\n  public static MoshiSpanDecoder create() {\n    return new MoshiSpanDecoder();\n  }\n\n  MoshiSpanDecoder() {\n    listSpansAdapter = new Moshi.Builder()\n      .add(Span.class, SPAN_ADAPTER)\n      .build().adapter(Types.newParameterizedType(List.class, Span.class));\n  }\n\n  public List<Span> decodeList(byte[] spans) {\n    BufferedSource source = new Buffer().write(spans);\n    try {\n      return listSpansAdapter.fromJson(source);\n    } catch (IOException e) {\n      throw new AssertionError(e); // no I/O\n    }\n  }\n\n  public List<Span> decodeList(ByteBuffer spans) {\n    try {\n      return listSpansAdapter.fromJson(JsonReader.of(Okio.buffer(new ByteBufferSource(spans))));\n    } catch (IOException e) {\n      throw new AssertionError(e); // no I/O\n    }\n  }\n\n  static final class ByteBufferSource implements okio.Source {\n    final ByteBuffer source;\n\n    final Buffer.UnsafeCursor cursor = new Buffer.UnsafeCursor();\n\n    ByteBufferSource(ByteBuffer source) {\n      this.source = source;\n    }\n\n    @Override public long read(Buffer sink, long byteCount) {\n      try (Buffer.UnsafeCursor ignored = sink.readAndWriteUnsafe(cursor)) {\n        long oldSize = sink.size();\n        int length = (int) Math.min(source.remaining(), Math.min(8192, byteCount));\n        if (length == 0) return -1;\n        cursor.expandBuffer(length);\n        source.get(cursor.data, cursor.start, length);\n        cursor.resizeBuffer(oldSize + length);\n        return length;\n      }\n    }\n\n    @Override public Timeout timeout() {\n      return Timeout.NONE;\n    }\n\n    @Override public void close() {\n    }\n  }\n\n  public static final JsonAdapter<Span> SPAN_ADAPTER =\n    new JsonAdapter<Span>() {\n      @Override\n      public Span fromJson(JsonReader reader) throws IOException {\n        Span.Builder result = Span.newBuilder();\n        reader.beginObject();\n        while (reader.hasNext()) {\n          String nextName = reader.nextName();\n          if (reader.peek() == JsonReader.Token.NULL) {\n            reader.skipValue();\n            continue;\n          }\n          switch (nextName) {\n            case \"traceId\":\n              result.traceId(reader.nextString());\n              break;\n            case \"parentId\":\n              result.parentId(reader.nextString());\n              break;\n            case \"id\":\n              result.id(reader.nextString());\n              break;\n            case \"kind\":\n              result.kind(Span.Kind.valueOf(reader.nextString()));\n              break;\n            case \"name\":\n              result.name(reader.nextString());\n              break;\n            case \"timestamp\":\n              result.timestamp(reader.nextLong());\n              break;\n            case \"duration\":\n              result.duration(reader.nextLong());\n              break;\n            case \"localEndpoint\":\n              result.localEndpoint(ENDPOINT_ADAPTER.fromJson(reader));\n              break;\n            case \"remoteEndpoint\":\n              result.remoteEndpoint(ENDPOINT_ADAPTER.fromJson(reader));\n              break;\n            case \"annotations\":\n              reader.beginArray();\n              while (reader.hasNext()) {\n                Annotation a = ANNOTATION_ADAPTER.fromJson(reader);\n                result.addAnnotation(a.timestamp(), a.value());\n              }\n              reader.endArray();\n              break;\n            case \"tags\":\n              reader.beginObject();\n              while (reader.hasNext()) {\n                result.putTag(reader.nextName(), reader.nextString());\n              }\n              reader.endObject();\n              break;\n            case \"debug\":\n              result.debug(reader.nextBoolean());\n              break;\n            case \"shared\":\n              result.shared(reader.nextBoolean());\n              break;\n            default:\n              reader.skipValue();\n          }\n        }\n        reader.endObject();\n        return result.build();\n      }\n\n      @Override\n      public void toJson(JsonWriter writer, @Nullable Span value) {\n        throw new UnsupportedOperationException();\n      }\n    };\n\n  static final JsonAdapter<Annotation> ANNOTATION_ADAPTER =\n    new JsonAdapter<Annotation>() {\n      @Override\n      public Annotation fromJson(JsonReader reader) throws IOException {\n        reader.beginObject();\n        Long timestamp = null;\n        String value = null;\n        while (reader.hasNext()) {\n          switch (reader.nextName()) {\n            case \"timestamp\":\n              timestamp = reader.nextLong();\n              break;\n            case \"value\":\n              value = reader.nextString();\n              break;\n            default:\n              reader.skipValue();\n          }\n        }\n        reader.endObject();\n        if (timestamp == null || value == null) {\n          throw new IllegalStateException(\"Incomplete annotation at \" + reader.getPath());\n        }\n        return Annotation.create(timestamp, value);\n      }\n\n      @Override\n      public void toJson(JsonWriter writer, @Nullable Annotation value) {\n        throw new UnsupportedOperationException();\n      }\n    };\n\n  static final JsonAdapter<Endpoint> ENDPOINT_ADAPTER =\n    new JsonAdapter<Endpoint>() {\n      @Override\n      public Endpoint fromJson(JsonReader reader) throws IOException {\n        reader.beginObject();\n        String serviceName = null, ipv4 = null, ipv6 = null;\n        int port = 0;\n        while (reader.hasNext()) {\n          String nextName = reader.nextName();\n          if (reader.peek() == JsonReader.Token.NULL) {\n            reader.skipValue();\n            continue;\n          }\n          switch (nextName) {\n            case \"serviceName\":\n              serviceName = reader.nextString();\n              break;\n            case \"ipv4\":\n              ipv4 = reader.nextString();\n              break;\n            case \"ipv6\":\n              ipv6 = reader.nextString();\n              break;\n            case \"port\":\n              port = reader.nextInt();\n              break;\n            default:\n              reader.skipValue();\n          }\n        }\n        reader.endObject();\n        if (serviceName == null && ipv4 == null && ipv6 == null && port == 0) return null;\n        return Endpoint.newBuilder()\n          .serviceName(serviceName)\n          .ip(ipv4)\n          .ip(ipv6)\n          .port(port)\n          .build();\n      }\n\n      @Override\n      public void toJson(JsonWriter writer, @Nullable Endpoint value) {\n        throw new UnsupportedOperationException();\n      }\n    }.nullSafe();\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/MoshiSpanDecoderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.TRACE;\n\npublic class MoshiSpanDecoderTest {\n  byte[] encoded = SpanBytesEncoder.JSON_V2.encodeList(TRACE);\n\n  @Test void decodeList_bytes() {\n    assertThat(new MoshiSpanDecoder().decodeList(encoded))\n      .isEqualTo(TRACE);\n  }\n\n  @Test void decodeList_byteBuffer() {\n    ByteBuf encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(encoded.length);\n    encodedBuf.writeBytes(encoded);\n    try {\n      assertThat(new MoshiSpanDecoder().decodeList(encodedBuf.nioBuffer()))\n        .isEqualTo(TRACE);\n    } finally {\n      encodedBuf.release();\n    }\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/ProtoCodecBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.Setup;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.TearDown;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.storage.cassandra.internal.Resources.resourceToString;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class ProtoCodecBenchmarks {\n\n  static final byte[] clientSpanJsonV2 = resourceToString(\"/zipkin2-client.json\").getBytes(UTF_8);\n  static final Span clientSpan = SpanBytesDecoder.JSON_V2.decodeOne(clientSpanJsonV2);\n\n  // Assume a message is 1000 spans (which is a high number for as this is per-node-second)\n  static final List<Span> spans = Collections.nCopies(1000, clientSpan);\n  static final byte[] encodedBytes = SpanBytesEncoder.PROTO3.encodeList(spans);\n\n  private ByteBuf encodedBuf;\n\n  @Setup\n  public void setup() {\n    encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(encodedBytes.length);\n    encodedBuf.writeBytes(encodedBytes);\n  }\n\n  @TearDown\n  public void tearDown() {\n    encodedBuf.release();\n  }\n\n  @Benchmark\n  public List<Span> bytes_zipkinDecoder() {\n    return SpanBytesDecoder.PROTO3.decodeList(encodedBytes);\n  }\n\n  @Benchmark\n  public List<Span> bytes_protobufDecoder() {\n    return ProtobufSpanDecoder.decodeList(encodedBytes);\n  }\n\n  @Benchmark\n  public List<Span> bytes_wireDecoder() {\n    return WireSpanDecoder.decodeList(encodedBytes);\n  }\n\n  @Benchmark\n  public List<Span> bytebuffer_zipkinDecoder() {\n    return SpanBytesDecoder.PROTO3.decodeList(encodedBuf.nioBuffer());\n  }\n\n  @Benchmark\n  public List<Span> bytebuffer_protobufDecoder() {\n    return ProtobufSpanDecoder.decodeList(encodedBuf.nioBuffer());\n  }\n\n  @Benchmark\n  public List<Span> bytebuffer_wireDecoder() {\n    return WireSpanDecoder.decodeList(encodedBuf.nioBuffer());\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws Exception {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + ProtoCodecBenchmarks.class.getSimpleName())\n      .addProfiler(\"gc\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/ProtobufSpanDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport com.google.protobuf.CodedInputStream;\nimport com.google.protobuf.WireFormat;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.logging.Logger;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.RecyclableBuffers;\n\npublic class ProtobufSpanDecoder {\n  static final Logger LOG = Logger.getLogger(ProtobufSpanDecoder.class.getName());\n  static final boolean DEBUG = false;\n\n  // map<string,string> in proto is a special field with key, value\n  static final int MAP_KEY_KEY = (1 << 3) | WireFormat.WIRETYPE_LENGTH_DELIMITED;\n  static final int MAP_VALUE_KEY = (2 << 3) | WireFormat.WIRETYPE_LENGTH_DELIMITED;\n\n  static boolean decodeTag(CodedInputStream input, Span.Builder span) throws IOException {\n    // now, we are in the tag fields\n    String key = null, value = \"\"; // empty tags allowed\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.readTag();\n      switch (tag) {\n        case 0:\n          done = true;\n          break;\n        case MAP_KEY_KEY: {\n          key = input.readStringRequireUtf8();\n          break;\n        }\n        case MAP_VALUE_KEY: {\n          value = input.readStringRequireUtf8();\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    if (key == null) return false;\n    span.putTag(key, value);\n    return true;\n  }\n\n  static boolean decodeAnnotation(CodedInputStream input, Span.Builder span) throws IOException {\n    long timestamp = 0L;\n    String value = null;\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.readTag();\n      switch (tag) {\n        case 0:\n          done = true;\n          break;\n        case 9: {\n          timestamp = input.readFixed64();\n          break;\n        }\n        case 18: {\n          value = input.readStringRequireUtf8();\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    if (timestamp == 0L || value == null) return false;\n    span.addAnnotation(timestamp, value);\n    return true;\n  }\n\n  private static Endpoint decodeEndpoint(CodedInputStream input) throws IOException {\n    Endpoint.Builder endpoint = Endpoint.newBuilder();\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.readTag();\n      switch (tag) {\n        case 0:\n          done = true;\n          break;\n        case 10: {\n          endpoint.serviceName(input.readStringRequireUtf8());\n          break;\n        }\n        case 18:\n        case 26: {\n          endpoint.parseIp(input.readByteArray());\n          break;\n        }\n        case 32: {\n          endpoint.port(input.readInt32());\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n    return endpoint.build();\n  }\n\n  public static Span decodeOne(CodedInputStream input) throws IOException {\n    Span.Builder span = Span.newBuilder();\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.readTag();\n      switch (tag) {\n        case 0:\n          done = true;\n          break;\n        case 10: {\n          span.traceId(readHexString(input));\n          break;\n        }\n        case 18: {\n          span.parentId(readHexString(input));\n          break;\n        }\n        case 26: {\n          span.id(readHexString(input));\n          break;\n        }\n        case 32: {\n          int kind = input.readEnum();\n          if (kind == 0) break;\n          if (kind > Span.Kind.values().length) break;\n          span.kind(Span.Kind.values()[kind - 1]);\n          break;\n        }\n        case 42: {\n          span.name(input.readStringRequireUtf8());\n          break;\n        }\n        case 49: {\n          span.timestamp(input.readFixed64());\n          break;\n        }\n        case 56: {\n          span.duration(input.readUInt64());\n          break;\n        }\n        case 66: {\n          int length = input.readRawVarint32();\n          int oldLimit = input.pushLimit(length);\n\n          span.localEndpoint(decodeEndpoint(input));\n\n          input.checkLastTagWas(0);\n          input.popLimit(oldLimit);\n          break;\n        }\n        case 74: {\n          int length = input.readRawVarint32();\n          int oldLimit = input.pushLimit(length);\n\n          span.remoteEndpoint(decodeEndpoint(input));\n\n          input.checkLastTagWas(0);\n          input.popLimit(oldLimit);\n          break;\n        }\n        case 82: {\n          int length = input.readRawVarint32();\n          int oldLimit = input.pushLimit(length);\n\n          decodeAnnotation(input, span);\n\n          input.checkLastTagWas(0);\n          input.popLimit(oldLimit);\n          break;\n        }\n        case 90: {\n          int length = input.readRawVarint32();\n          int oldLimit = input.pushLimit(length);\n\n          decodeTag(input, span);\n\n          input.checkLastTagWas(0);\n          input.popLimit(oldLimit);\n          break;\n        }\n        case 96: {\n          span.debug(input.readBool());\n          break;\n        }\n        case 104: {\n          span.shared(input.readBool());\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    return span.build();\n  }\n\n  public static List<Span> decodeList(byte[] spans) {\n    return decodeList(CodedInputStream.newInstance(spans));\n  }\n\n  public static List<Span> decodeList(ByteBuffer spans) {\n    return decodeList(CodedInputStream.newInstance(spans));\n  }\n\n  public static List<Span> decodeList(CodedInputStream input) {\n    ArrayList<Span> spans = new ArrayList<>();\n\n    try {\n      boolean done = false;\n      while (!done) {\n        int tag = input.readTag();\n        switch (tag) {\n          case 0:\n            done = true;\n            break;\n          case 10:\n            int length = input.readRawVarint32();\n            int oldLimit = input.pushLimit(length);\n            spans.add(decodeOne(input));\n            input.checkLastTagWas(0);\n            input.popLimit(oldLimit);\n            break;\n          default: {\n            logAndSkip(input, tag);\n            break;\n          }\n        }\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n\n    return spans;\n  }\n\n  static final char[] HEX_DIGITS =\n    {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n\n  private static String readHexString(CodedInputStream input) throws IOException {\n    int size = input.readRawVarint32();\n    int length = size * 2;\n\n    // All our hex fields are at most 32 characters.\n    if (length > 32) {\n      throw new AssertionError(\"hex field greater than 32 chars long: \" + length);\n    }\n\n    char[] result = RecyclableBuffers.shortStringBuffer();\n\n    for (int i = 0; i < length; i += 2) {\n      byte b = input.readRawByte();\n      result[i] = HEX_DIGITS[(b >> 4) & 0xf];\n      result[i + 1] = HEX_DIGITS[b & 0xf];\n    }\n\n    return new String(result, 0, length);\n  }\n\n  static void logAndSkip(CodedInputStream input, int tag) throws IOException {\n    if (DEBUG) { // avoiding volatile reads as we don't log on skip in our normal codec\n      int nextWireType = WireFormat.getTagWireType(tag);\n      int nextFieldNumber = WireFormat.getTagFieldNumber(tag);\n      LOG.fine(\"Skipping field: byte=%s, fieldNumber=%s, wireType=%s\".formatted(\n        input.getTotalBytesRead(), nextFieldNumber, nextWireType));\n    }\n    input.skipField(tag);\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/codec/WireSpanDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport com.google.protobuf.WireFormat;\nimport com.squareup.wire.ProtoAdapter;\nimport com.squareup.wire.ProtoReader;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.logging.Logger;\nimport okio.Buffer;\nimport okio.ByteString;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.RecyclableBuffers;\n\npublic class WireSpanDecoder {\n  static final Logger LOG = Logger.getLogger(WireSpanDecoder.class.getName());\n  static final boolean DEBUG = false;\n\n  static boolean decodeTag(ProtoReader input, Span.Builder span) throws IOException {\n    // now, we are in the tag fields\n    String key = null, value = \"\"; // empty tags allowed\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.nextTag();\n      switch (tag) {\n        case -1:\n          done = true;\n          break;\n        case 1: {\n          key = input.readString();\n          break;\n        }\n        case 2: {\n          value = input.readString();\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    if (key == null) return false;\n    span.putTag(key, value);\n    return true;\n  }\n\n  static boolean decodeAnnotation(ProtoReader input, Span.Builder span) throws IOException {\n    long timestamp = 0L;\n    String value = null;\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.nextTag();\n      switch (tag) {\n        case -1:\n          done = true;\n          break;\n        case 1: {\n          timestamp = input.readFixed64();\n          break;\n        }\n        case 2: {\n          value = input.readString();\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    if (timestamp == 0L || value == null) return false;\n    span.addAnnotation(timestamp, value);\n    return true;\n  }\n\n  private static Endpoint decodeEndpoint(ProtoReader input) throws IOException {\n    Endpoint.Builder endpoint = Endpoint.newBuilder();\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.nextTag();\n      switch (tag) {\n        case -1:\n          done = true;\n          break;\n        case 1: {\n          String s = input.readString();\n          endpoint.serviceName(s);\n          break;\n        }\n        case 2:\n        case 3: {\n          endpoint.parseIp(input.readBytes().toByteArray());\n          break;\n        }\n        case 4: {\n          endpoint.port(input.readVarint32());\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n    return endpoint.build();\n  }\n\n  public static Span decodeOne(ProtoReader input) throws IOException {\n    Span.Builder span = Span.newBuilder();\n\n    boolean done = false;\n    while (!done) {\n      int tag = input.nextTag();\n      switch (tag) {\n        case -1:\n          done = true;\n          break;\n        case 1: {\n          span.traceId(readHexString(input));\n          break;\n        }\n        case 2: {\n          span.parentId(readHexString(input));\n          break;\n        }\n        case 3: {\n          span.id(readHexString(input));\n          break;\n        }\n        case 4: {\n          int kind = input.readVarint32();\n          if (kind == 0) break;\n          if (kind > Span.Kind.values().length) break;\n          span.kind(Span.Kind.values()[kind - 1]);\n          break;\n        }\n        case 5: {\n          String name = input.readString();\n          span.name(name);\n          break;\n        }\n        case 6: {\n          span.timestamp(input.readFixed64());\n          break;\n        }\n        case 7: {\n          span.duration(input.readVarint64());\n          break;\n        }\n        case 8: {\n          long token = input.beginMessage();\n\n          span.localEndpoint(decodeEndpoint(input));\n\n          input.endMessageAndGetUnknownFields(token);\n          break;\n        }\n        case 9: {\n          long token = input.beginMessage();\n\n          span.remoteEndpoint(decodeEndpoint(input));\n\n          input.endMessageAndGetUnknownFields(token);\n          break;\n        }\n        case 10: {\n          long token = input.beginMessage();\n\n          decodeAnnotation(input, span);\n\n          input.endMessageAndGetUnknownFields(token);\n          break;\n        }\n        case 11: {\n          long token = input.beginMessage();\n\n          decodeTag(input, span);\n\n          input.endMessageAndGetUnknownFields(token);\n          break;\n        }\n        case 12: {\n          span.debug(ProtoAdapter.BOOL.decode(input));\n          break;\n        }\n        case 13: {\n          span.shared(ProtoAdapter.BOOL.decode(input));\n          break;\n        }\n        default: {\n          logAndSkip(input, tag);\n          break;\n        }\n      }\n    }\n\n    return span.build();\n  }\n\n  public static List<Span> decodeList(byte[] spans) {\n    return decodeList(new ProtoReader(new Buffer().write(spans)));\n  }\n\n  public static List<Span> decodeList(ByteBuffer spans) {\n    Buffer buffer = new Buffer();\n    try {\n      buffer.write(spans);\n    } catch (IOException e) {\n      throw new AssertionError(e); // no I/O\n    }\n    return decodeList(new ProtoReader(buffer));\n  }\n\n  public static List<Span> decodeList(ProtoReader input) {\n    ArrayList<Span> spans = new ArrayList<>();\n\n    final long token;\n    try {\n      token = input.beginMessage();\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n\n    try {\n      boolean done = false;\n      while (!done) {\n        int tag = input.nextTag();\n        switch (tag) {\n          case -1:\n            done = true;\n            break;\n          case 1: {\n            long subToken = input.beginMessage();\n\n            spans.add(decodeOne(input));\n\n            input.endMessageAndGetUnknownFields(subToken);\n            break;\n          }\n          default: {\n            logAndSkip(input, tag);\n            break;\n          }\n        }\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n\n    try {\n      input.endMessageAndGetUnknownFields(token);\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n\n    return spans;\n  }\n\n  static final char[] HEX_DIGITS =\n    {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n\n  // https://github.com/square/wire/issues/958\n  private static String readHexString(ProtoReader input) throws IOException {\n    ByteString bytes = input.readBytes();\n    int length = bytes.size() * 2;\n\n    // All our hex fields are at most 32 characters.\n    if (length > 32) {\n      throw new AssertionError(\"hex field greater than 32 chars long: \" + length);\n    }\n\n    char[] result = RecyclableBuffers.shortStringBuffer();\n\n    for (int i = 0; i < bytes.size(); i++) {\n      byte b = bytes.getByte(i);\n      result[2 * i] = HEX_DIGITS[(b >> 4) & 0xf];\n      result[2 * i + 1] = HEX_DIGITS[b & 0xf];\n    }\n\n    return new String(result, 0, length);\n  }\n\n  static void logAndSkip(ProtoReader input, int tag) throws IOException {\n    if (DEBUG) { // avoiding volatile reads as we don't log on skip in our normal codec\n      int nextWireType = WireFormat.getTagWireType(tag);\n      int nextFieldNumber = WireFormat.getTagFieldNumber(tag);\n      LOG.fine(\"Skipping field: byte=%s, fieldNumber=%s, wireType=%s\".formatted(\n        0, nextFieldNumber, nextWireType));\n    }\n    input.skip();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/collector/MetricsBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.prometheus.PrometheusConfig;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.server.internal.MicrometerCollectorMetrics;\n\n@Measurement(iterations = 80, time = 1)\n@Warmup(iterations = 20, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.AverageTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class MetricsBenchmarks {\n  static final int LONG_SPAN = 5000;\n  static final int MEDIUM_SPAN = 1000;\n  static final int SHORT_SPAN = 500;\n  private MeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);\n  private InMemoryCollectorMetrics inMemoryCollectorMetrics = new InMemoryCollectorMetrics()\n    .forTransport(\"jmh\");\n  private MicrometerCollectorMetrics micrometerCollectorMetrics =\n    new MicrometerCollectorMetrics(registry)\n      .forTransport(\"jmh\");\n\n  @Benchmark\n  public int incrementBytes_longSpans_inMemory() {\n    return incrementBytes(inMemoryCollectorMetrics, LONG_SPAN);\n  }\n\n  @Benchmark\n  public int incrementBytes_longSpans_Actuate() {\n    return incrementBytes(micrometerCollectorMetrics, LONG_SPAN);\n  }\n\n  @Benchmark\n  public int incrementBytes_mediumSpans_inMemory() {\n    return incrementBytes(inMemoryCollectorMetrics, MEDIUM_SPAN);\n  }\n\n  @Benchmark\n  public int incrementBytes_mediumSpans_Actuate() {\n    return incrementBytes(micrometerCollectorMetrics, MEDIUM_SPAN);\n  }\n\n  @Benchmark\n  public int incrementBytes_shortSpans_inMemory() {\n    return incrementBytes(inMemoryCollectorMetrics, SHORT_SPAN);\n  }\n\n  @Benchmark\n  public int incrementBytes_shortSpans_Actuate() {\n    return incrementBytes(micrometerCollectorMetrics, SHORT_SPAN);\n  }\n\n  private int incrementBytes(CollectorMetrics collectorMetrics, int bytes) {\n    collectorMetrics.incrementBytes(bytes);\n    return bytes;\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + MetricsBenchmarks.class.getSimpleName() + \".*\")\n      .threads(40)\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/elasticsearch/internal/BulkRequestBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.linecorp.armeria.common.HttpRequest;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.internal.BulkCallBuilder.IndexEntry;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V6_0;\nimport static zipkin2.storage.cassandra.internal.Resources.resourceToString;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(2)\npublic class BulkRequestBenchmarks {\n  static final Span CLIENT_SPAN =\n    SpanBytesDecoder.JSON_V2.decodeOne(resourceToString(\"/zipkin2-client.json\").getBytes(UTF_8));\n\n  final ElasticsearchStorage es = ElasticsearchStorage.newBuilder(() -> null).build();\n  final long indexTimestamp = CLIENT_SPAN.timestampAsLong() / 1000L;\n  final String spanIndex =\n    es.indexNameFormatter().formatTypeAndTimestampForInsert(\"span\", '-', indexTimestamp);\n  final IndexEntry<Span> entry =\n    BulkCallBuilder.newIndexEntry(spanIndex, \"span\", CLIENT_SPAN, BulkIndexWriter.SPAN);\n\n  @Benchmark public ByteBuf writeRequest_singleSpan() {\n    return BulkCallBuilder.serialize(PooledByteBufAllocator.DEFAULT, entry, true);\n  }\n\n  @Benchmark public HttpRequest buildAndWriteRequest_singleSpan() {\n    BulkCallBuilder builder = new BulkCallBuilder(es, V6_0, \"index-span\");\n    builder.index(spanIndex, \"span\", CLIENT_SPAN, BulkIndexWriter.SPAN);\n    return builder.build().request.get();\n  }\n\n  @Benchmark public HttpRequest buildAndWriteRequest_tenSpans() {\n    BulkCallBuilder builder = new BulkCallBuilder(es, V6_0, \"index-span\");\n    for (int i = 0; i < 10; i++) {\n      builder.index(spanIndex, \"span\", CLIENT_SPAN, BulkIndexWriter.SPAN);\n    }\n    return builder.build().request.get();\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .addProfiler(\"gc\")\n      .include(\".*\" + BulkRequestBenchmarks.class.getSimpleName() + \".*\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/internal/DelayLimiterBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.Random;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(2)\npublic class DelayLimiterBenchmarks {\n\n  final Random rng = new Random();\n  final DelayLimiter<Long> limiter = DelayLimiter.newBuilder()\n    .ttl(1L, TimeUnit.HOURS) // legacy default from Cassandra\n    .cardinality(5 * 4000) // Ex. 5 site tags with cardinality 4000 each\n    .build();\n\n  @Benchmark public boolean shouldInvoke_randomData() {\n    return limiter.shouldInvoke(rng.nextLong());\n  }\n\n  @Benchmark public boolean shouldInvoke_sameData() {\n    return limiter.shouldInvoke(1L);\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .addProfiler(\"gc\")\n      .include(\".*\" + DelayLimiterBenchmarks.class.getSimpleName() + \".*\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/internal/Proto3CodecInteropTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport com.squareup.wire.ProtoWriter;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport okio.ByteString;\nimport org.assertj.core.data.MapEntry;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.internal.Proto3ZipkinFields.TagField;\nimport zipkin2.proto3.Annotation;\nimport zipkin2.proto3.Endpoint;\nimport zipkin2.proto3.ListOfSpans;\nimport zipkin2.proto3.Span;\n\nimport static okio.ByteString.decodeHex;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.MapEntry.entry;\nimport static zipkin2.internal.Proto3ZipkinFields.SPAN;\nimport static zipkin2.internal.Proto3ZipkinFields.SpanField.ANNOTATION;\nimport static zipkin2.internal.Proto3ZipkinFields.SpanField.LOCAL_ENDPOINT;\nimport static zipkin2.internal.Proto3ZipkinFields.SpanField.REMOTE_ENDPOINT;\nimport static zipkin2.internal.Proto3ZipkinFields.SpanField.TAG_KEY;\n\n// Compares against Square Wire as it is easier than Google's protobuf tooling\npublic class Proto3CodecInteropTest {\n  static final zipkin2.Endpoint ORDER = zipkin2.Endpoint.newBuilder()\n    .serviceName(\"订单维护服务\")\n    .ip(\"2001:db8::c001\")\n    .build();\n\n  static final zipkin2.Endpoint PROFILE = zipkin2.Endpoint.newBuilder()\n    .serviceName(\"个人信息服务\")\n    .ip(\"192.168.99.101\")\n    .port(9000)\n    .build();\n\n  static final zipkin2.Span ZIPKIN_SPAN = zipkin2.Span.newBuilder()\n    .traceId(\"4d1e00c0db9010db86154a4ba6e91385\")\n    .parentId(\"86154a4ba6e91385\")\n    .id(\"4d1e00c0db9010db\")\n    .kind(zipkin2.Span.Kind.SERVER)\n    .name(\"个人信息查询\")\n    .timestamp(1472470996199000L)\n    .duration(207000L)\n    .localEndpoint(ORDER)\n    .remoteEndpoint(PROFILE)\n    .addAnnotation(1472470996199000L, \"foo happened\")\n    .putTag(\"http.path\", \"/person/profile/query\")\n    .putTag(\"http.status_code\", \"403\")\n    .putTag(\"clnt/finagle.version\", \"6.45.0\")\n    .putTag(\"error\", \"此用户没有操作权限\")\n    .shared(true)\n    .build();\n  static final List<zipkin2.Span> ZIPKIN_SPANS = List.of(ZIPKIN_SPAN, ZIPKIN_SPAN);\n\n  static final Span PROTO_SPAN = new Span.Builder()\n    .trace_id(decodeHex(ZIPKIN_SPAN.traceId()))\n    .parent_id(decodeHex(ZIPKIN_SPAN.parentId()))\n    .id(decodeHex(ZIPKIN_SPAN.id()))\n    .kind(Span.Kind.valueOf(ZIPKIN_SPAN.kind().name()))\n    .name(ZIPKIN_SPAN.name())\n    .timestamp(ZIPKIN_SPAN.timestampAsLong())\n    .duration(ZIPKIN_SPAN.durationAsLong())\n    .local_endpoint(new Endpoint.Builder()\n      .service_name(ORDER.serviceName())\n      .ipv6(ByteString.of(ORDER.ipv6Bytes())).build()\n    )\n    .remote_endpoint(new Endpoint.Builder()\n      .service_name(PROFILE.serviceName())\n      .ipv4(ByteString.of(PROFILE.ipv4Bytes()))\n      .port(PROFILE.portAsInt()).build()\n    )\n    .annotations(List.of(new Annotation.Builder()\n      .timestamp(ZIPKIN_SPAN.annotations().get(0).timestamp())\n      .value(ZIPKIN_SPAN.annotations().get(0).value())\n      .build()))\n    .tags(ZIPKIN_SPAN.tags())\n    .shared(true)\n    .build();\n  ListOfSpans PROTO_SPANS = new ListOfSpans.Builder()\n    .spans(List.of(PROTO_SPAN, PROTO_SPAN)).build();\n\n  @Test void encodeIsCompatible() throws IOException {\n    okio.Buffer buffer = new okio.Buffer();\n\n    Span.ADAPTER.encodeWithTag(new ProtoWriter(buffer), 1, PROTO_SPAN);\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(ZIPKIN_SPAN))\n      .containsExactly(buffer.readByteArray());\n  }\n\n  @Test void decodeOneIsCompatible() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(PROTO_SPANS.encode()))\n      .isEqualTo(ZIPKIN_SPAN);\n  }\n\n  @Test void decodeListIsCompatible() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(PROTO_SPANS.encode()))\n      .containsExactly(ZIPKIN_SPAN, ZIPKIN_SPAN);\n  }\n\n  @Test void encodeListIsCompatible_buff() {\n    byte[] wireBytes = PROTO_SPANS.encode();\n    byte[] zipkin_buff = new byte[10 + wireBytes.length];\n\n    assertThat(SpanBytesEncoder.PROTO3.encodeList(ZIPKIN_SPANS, zipkin_buff, 5))\n      .isEqualTo(wireBytes.length);\n\n    assertThat(zipkin_buff)\n      .startsWith(0, 0, 0, 0, 0)\n      .containsSequence(wireBytes)\n      .endsWith(0, 0, 0, 0, 0);\n  }\n\n  @Test void encodeListIsCompatible() {\n    byte[] wireBytes = PROTO_SPANS.encode();\n\n    assertThat(SpanBytesEncoder.PROTO3.encodeList(ZIPKIN_SPANS))\n      .containsExactly(wireBytes);\n  }\n\n  @Test void span_sizeInBytes_matchesWire() {\n    assertThat(SPAN.sizeInBytes(ZIPKIN_SPAN))\n      .isEqualTo(Span.ADAPTER.encodedSizeWithTag(SPAN.fieldNumber, PROTO_SPAN));\n  }\n\n  @Test void annotation_sizeInBytes_matchesWire() {\n    zipkin2.Annotation zipkinAnnotation = ZIPKIN_SPAN.annotations().get(0);\n\n    assertThat(ANNOTATION.sizeInBytes(zipkinAnnotation)).isEqualTo(\n      Annotation.ADAPTER.encodedSizeWithTag(ANNOTATION.fieldNumber, PROTO_SPAN.annotations.get(0))\n    );\n  }\n\n  @Test void annotation_write_matchesWire() {\n    zipkin2.Annotation zipkinAnnotation = ZIPKIN_SPAN.annotations().get(0);\n    Span wireSpan = new Span.Builder().annotations(PROTO_SPAN.annotations).build();\n\n    byte[] zipkinBytes = new byte[ANNOTATION.sizeInBytes(zipkinAnnotation)];\n    ANNOTATION.write(WriteBuffer.wrap(zipkinBytes, 0), zipkinAnnotation);\n\n    assertThat(zipkinBytes)\n      .containsExactly(wireSpan.encode());\n  }\n\n  @Test void annotation_read_matchesWireEncodingWithTag() {\n    zipkin2.Annotation zipkinAnnotation = ZIPKIN_SPAN.annotations().get(0);\n    Span wireSpan = new Span.Builder().annotations(PROTO_SPAN.annotations).build();\n\n    ReadBuffer wireBytes = ReadBuffer.wrap(wireSpan.encode());\n    assertThat(wireBytes.readVarint32())\n      .isEqualTo(ANNOTATION.key);\n\n    zipkin2.Span.Builder builder = zipkinSpanBuilder();\n    ANNOTATION.readLengthPrefixAndValue(wireBytes, builder);\n    assertThat(builder.build().annotations())\n      .containsExactly(zipkinAnnotation);\n  }\n\n  @Test void endpoint_sizeInBytes_matchesWireEncodingWithTag() {\n    assertThat(LOCAL_ENDPOINT.sizeInBytes(ZIPKIN_SPAN.localEndpoint())).isEqualTo(\n      Endpoint.ADAPTER.encodedSizeWithTag(LOCAL_ENDPOINT.fieldNumber, PROTO_SPAN.local_endpoint)\n    );\n\n    assertThat(REMOTE_ENDPOINT.sizeInBytes(ZIPKIN_SPAN.remoteEndpoint())).isEqualTo(\n      Endpoint.ADAPTER.encodedSizeWithTag(REMOTE_ENDPOINT.fieldNumber, PROTO_SPAN.remote_endpoint)\n    );\n  }\n\n  @Test void localEndpoint_write_matchesWire() {\n    byte[] zipkinBytes = new byte[LOCAL_ENDPOINT.sizeInBytes(ZIPKIN_SPAN.localEndpoint())];\n    LOCAL_ENDPOINT.write(WriteBuffer.wrap(zipkinBytes, 0), ZIPKIN_SPAN.localEndpoint());\n    Span wireSpan = new Span.Builder().local_endpoint(PROTO_SPAN.local_endpoint).build();\n\n    assertThat(zipkinBytes)\n      .containsExactly(wireSpan.encode());\n  }\n\n  @Test void remoteEndpoint_write_matchesWire() {\n    byte[] zipkinBytes = new byte[REMOTE_ENDPOINT.sizeInBytes(ZIPKIN_SPAN.remoteEndpoint())];\n    REMOTE_ENDPOINT.write(WriteBuffer.wrap(zipkinBytes, 0), ZIPKIN_SPAN.remoteEndpoint());\n    Span wireSpan = new Span.Builder().remote_endpoint(PROTO_SPAN.remote_endpoint).build();\n\n    assertThat(zipkinBytes)\n      .containsExactly(wireSpan.encode());\n  }\n\n  @Test void tag_sizeInBytes_matchesWire() {\n    MapEntry<String, String> entry = entry(\"clnt/finagle.version\", \"6.45.0\");\n    Span wireSpan = new Span.Builder().tags(Map.of(entry.key, entry.value)).build();\n\n    assertThat(new TagField(TAG_KEY).sizeInBytes(entry))\n      .isEqualTo(Span.ADAPTER.encodedSize(wireSpan));\n  }\n\n  @Test void writeTagField_matchesWire() {\n    MapEntry<String, String> entry = entry(\"clnt/finagle.version\", \"6.45.0\");\n    TagField field = new TagField(TAG_KEY);\n    byte[] zipkinBytes = new byte[field.sizeInBytes(entry)];\n    field.write(WriteBuffer.wrap(zipkinBytes, 0), entry);\n\n    Span oneField = new Span.Builder().tags(Map.of(entry.key, entry.value)).build();\n    assertThat(zipkinBytes)\n      .containsExactly(oneField.encode());\n  }\n\n  @Test void writeTagField_matchesWire_emptyValue() {\n    MapEntry<String, String> entry = entry(\"error\", \"\");\n    TagField field = new TagField(TAG_KEY);\n    byte[] zipkinBytes = new byte[field.sizeInBytes(entry)];\n    field.write(WriteBuffer.wrap(zipkinBytes, 0), entry);\n\n    Span oneField = new Span.Builder().tags(Map.of(entry.key, entry.value)).build();\n    assertThat(zipkinBytes)\n      .containsExactly(oneField.encode());\n  }\n\n  static zipkin2.Span.Builder zipkinSpanBuilder() {\n    return zipkin2.Span.newBuilder().traceId(\"a\").id(\"a\");\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/internal/ReadBufferBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class ReadBufferBenchmarks {\n  byte[] longBuff = {\n    (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n  };\n\n  @Benchmark public long readLong() {\n    int pos = 0;\n    return (longBuff[pos] & 0xffL) << 56\n      | (longBuff[pos + 1] & 0xffL) << 48\n      | (longBuff[pos + 2] & 0xffL) << 40\n      | (longBuff[pos + 3] & 0xffL) << 32\n      | (longBuff[pos + 4] & 0xffL) << 24\n      | (longBuff[pos + 5] & 0xffL) << 16\n      | (longBuff[pos + 6] & 0xffL) << 8\n      | (longBuff[pos + 7] & 0xffL);\n  }\n\n  @Benchmark public long readLong_localArray() {\n    int pos = 0;\n    byte[] longBuff = this.longBuff;\n    return (longBuff[pos] & 0xffL) << 56\n      | (longBuff[pos + 1] & 0xffL) << 48\n      | (longBuff[pos + 2] & 0xffL) << 40\n      | (longBuff[pos + 3] & 0xffL) << 32\n      | (longBuff[pos + 4] & 0xffL) << 24\n      | (longBuff[pos + 5] & 0xffL) << 16\n      | (longBuff[pos + 6] & 0xffL) << 8\n      | (longBuff[pos + 7] & 0xffL);\n  }\n\n  @Benchmark public long readLong_8arity_localArray() {\n    int pos = 0;\n    return readLong(\n      longBuff[pos] & 0xff,\n      longBuff[pos + 1] & 0xff,\n      longBuff[pos + 2] & 0xff,\n      longBuff[pos + 3] & 0xff,\n      longBuff[pos + 4] & 0xff,\n      longBuff[pos + 5] & 0xff,\n      longBuff[pos + 6] & 0xff,\n      longBuff[pos + 7] & 0xff\n    );\n  }\n\n  @Benchmark public long readLong_8arity() {\n    int pos = 0;\n    byte[] longBuff = this.longBuff;\n    return readLong(\n      longBuff[pos] & 0xff,\n      longBuff[pos + 1] & 0xff,\n      longBuff[pos + 2] & 0xff,\n      longBuff[pos + 3] & 0xff,\n      longBuff[pos + 4] & 0xff,\n      longBuff[pos + 5] & 0xff,\n      longBuff[pos + 6] & 0xff,\n      longBuff[pos + 7] & 0xff\n    );\n  }\n\n  static long readLong(int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7) {\n    return (p0 & 0xffL) << 56\n      | (p1 & 0xffL) << 48\n      | (p2 & 0xffL) << 40\n      | (p3 & 0xffL) << 32\n      | (p4 & 0xffL) << 24\n      | (p5 & 0xffL) << 16\n      | (p6 & 0xffL) << 8\n      | (p7 & 0xffL);\n  }\n\n  @Benchmark public long readLongReverseBytes() {\n    return Long.reverseBytes(readLong());\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + ReadBufferBenchmarks.class.getSimpleName() + \".*\")\n      .addProfiler(\"gc\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/internal/WriteBufferBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.nio.ByteBuffer;\nimport java.util.concurrent.TimeUnit;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.AverageTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(1)\npublic class WriteBufferBenchmarks {\n  // Order id = d07c4daa-0fa9-4c03-90b1-e06c4edae250 doesn't exist\n  static final String CHINESE_UTF8 = \"订单d07c4daa-0fa9-4c03-90b1-e06c4edae250不存在\";\n  static final int CHINESE_UTF8_SIZE = UTF_8.encode(CHINESE_UTF8).remaining();\n  /* length-prefixing a 1 KiB span */\n  static final int TEST_INT = 1024;\n  /* epoch micros timestamp */\n  static final long TEST_LONG = 1472470996199000L;\n  byte[] bytes = new byte[8];\n  WriteBuffer buffer = WriteBuffer.wrap(bytes);\n\n  @Benchmark public int utf8SizeInBytes_chinese() {\n    return WriteBuffer.utf8SizeInBytes(CHINESE_UTF8);\n  }\n\n  @Benchmark public byte[] writeUtf8_chinese() {\n    byte[] bytesUtf8 = new byte[CHINESE_UTF8_SIZE];\n    WriteBuffer.wrap(bytesUtf8, 0).writeUtf8(CHINESE_UTF8);\n    return bytesUtf8;\n  }\n\n  @Benchmark public ByteBuffer writeUtf8_chinese_jdk() {\n    return UTF_8.encode(CHINESE_UTF8);\n  }\n\n  @Benchmark public int varIntSizeInBytes_32() {\n    return WriteBuffer.varintSizeInBytes(TEST_INT);\n  }\n\n  @Benchmark public int varIntSizeInBytes_64() {\n    return WriteBuffer.varintSizeInBytes(TEST_LONG);\n  }\n\n  @Benchmark public int writeVarint_32() {\n    buffer.writeVarint(TEST_INT);\n    return buffer.pos();\n  }\n\n  @Benchmark public int writeVarint_64() {\n    buffer.writeVarint(TEST_LONG);\n    return buffer.pos();\n  }\n\n  @Benchmark public int writeLongLe() {\n    buffer.writeLongLe(TEST_LONG);\n    return buffer.pos();\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .include(\".*\" + WriteBufferBenchmarks.class.getSimpleName() + \".*\")\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/server/ServerIntegratedBenchmark.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.linecorp.armeria.client.WebClient;\nimport io.netty.handler.codec.http.QueryStringEncoder;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.Network;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.lifecycle.Startables;\nimport org.testcontainers.utility.MountableFile;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\n/**\n * This benchmark runs zipkin-server, storage backends, an example application, prometheus, grafana,\n * and wrk using docker to generate a performance report.\n *\n * <p>Currently there are two environment variable knobs\n *\n * <ul>\n *   <li>\n *     RELEASE_VERSION - specify to a released zipkin server. If unspecified, will use the current code,\n *     i.e., the code currently displayed in your IDE.\n *   </li>\n *   <li>\n *     ZIPKIN_BENCHMARK_WAIT - set to true to have the benchmark wait until user manually terminates at the end.\n *     Useful to manually inspect prometheus / grafana.\n *   </li>\n * </ul>\n *\n * <p>Note to Windows laptop users: you will probably need to restart the Docker daemon before a\n * session of benchmarks. Docker containers seem to have time get out of sync when a computer sleeps\n * until you restart the daemon - this causes Prometheus metrics to not scrape properly.\n */\n@Disabled  // Run manually\nclass ServerIntegratedBenchmark {\n  static final Logger LOG = LoggerFactory.getLogger(ServerIntegratedBenchmark.class);\n\n  static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n  @Nullable static final String RELEASE_VERSION = System.getenv(\"RELEASE_VERSION\");\n\n  static final boolean WAIT_AFTER_BENCHMARK = \"true\".equals(System.getenv(\"ZIPKIN_BENCHMARK_WAIT\"));\n\n  List<GenericContainer<?>> containers;\n\n  @BeforeEach void setUp() {\n    containers = new ArrayList<>();\n  }\n\n  @AfterEach void tearDown() {\n    containers.forEach(GenericContainer::stop);\n  }\n\n  @Test void inMemory() throws Exception {\n    runBenchmark(null);\n  }\n\n  @Test void elasticsearch() throws Exception {\n    GenericContainer<?> elasticsearch =\n      new GenericContainer<>(parse(\"ghcr.io/openzipkin/zipkin-elasticsearch7:3.4.3\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"elasticsearch\")\n        .withLabel(\"name\", \"elasticsearch\")\n        .withLabel(\"storageType\", \"elasticsearch\")\n        .withExposedPorts(9200)\n        .waitingFor(Wait.forHealthcheck());\n    containers.add(elasticsearch);\n\n    runBenchmark(elasticsearch);\n  }\n\n  @Test void cassandra3() throws Exception {\n    GenericContainer<?> cassandra =\n      new GenericContainer<>(parse(\"ghcr.io/openzipkin/zipkin-cassandra:3.4.3\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"cassandra\")\n        .withLabel(\"name\", \"cassandra\")\n        .withLabel(\"storageType\", \"cassandra3\")\n        .withExposedPorts(9042)\n        .waitingFor(Wait.forHealthcheck());\n    containers.add(cassandra);\n\n    runBenchmark(cassandra);\n  }\n\n  @Test void mysql() throws Exception {\n    GenericContainer<?> mysql =\n      new GenericContainer<>(parse(\"ghcr.io/openzipkin/zipkin-mysql:3.4.3\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"mysql\")\n        .withLabel(\"name\", \"mysql\")\n        .withLabel(\"storageType\", \"mysql\")\n        .withExposedPorts(3306)\n        .waitingFor(Wait.forHealthcheck());\n    containers.add(mysql);\n\n    runBenchmark(mysql);\n  }\n\n  void runBenchmark(@Nullable GenericContainer<?> storage) throws Exception {\n    runBenchmark(storage, createZipkinContainer(storage));\n  }\n\n  void runBenchmark(@Nullable GenericContainer<?> storage, GenericContainer<?> zipkin)\n    throws Exception {\n    GenericContainer<?> backend =\n      new GenericContainer<>(parse(\"ghcr.io/openzipkin/brave-example:armeria\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"backend\")\n        .withCommand(\"backend\")\n        .withExposedPorts(9000)\n        .waitingFor(Wait.forHealthcheck());\n\n    GenericContainer<?> frontend =\n      new GenericContainer<>(parse(\"ghcr.io/openzipkin/brave-example:armeria\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"frontend\")\n        .withCommand(\"frontend\")\n        .withExposedPorts(8081)\n        .waitingFor(Wait.forHealthcheck());\n    containers.add(frontend);\n\n    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    // Use same version as in docker/examples/docker-compose-prometheus.yml\n    GenericContainer<?> prometheus =\n      new GenericContainer<>(parse(\"quay.io/prometheus/prometheus:v2.55.1\"))\n        .withNetwork(Network.SHARED)\n        .withNetworkAliases(\"prometheus\")\n        .withExposedPorts(9090)\n        .withCopyFileToContainer(\n          MountableFile.forClasspathResource(\"prometheus.yml\"), \"/etc/prometheus/prometheus.yml\");\n    containers.add(prometheus);\n\n    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    // Use same version as in docker/examples/docker-compose-prometheus.yml\n    GenericContainer<?> grafana = new GenericContainer<>(parse(\"quay.io/giantswarm/grafana:7.5.12\"))\n      .withNetwork(Network.SHARED)\n      .withNetworkAliases(\"grafana\")\n      .withExposedPorts(3000)\n      .withEnv(\"GF_AUTH_ANONYMOUS_ENABLED\", \"true\")\n      .withEnv(\"GF_AUTH_ANONYMOUS_ORG_ROLE\", \"Admin\");\n    containers.add(grafana);\n\n    // This is an arbitrary small image that has curl installed\n    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    // Use same version as in docker/examples/docker-compose-prometheus.yml\n    GenericContainer<?> grafanaDashboards =\n      new GenericContainer<>(parse(\"quay.io/cilium/alpine-curl:v1.10.0\"))\n        .withNetwork(Network.SHARED)\n        .withWorkingDirectory(\"/tmp\")\n        .withLogConsumer(new Slf4jLogConsumer(LOG))\n        .withCreateContainerCmdModifier(it -> it.withEntrypoint(\"/tmp/create.sh\"))\n        .withCopyFileToContainer(\n          MountableFile.forClasspathResource(\"create-datasource-and-dashboard.sh\", 555),\n          \"/tmp/create.sh\");\n    containers.add(grafanaDashboards);\n\n    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    GenericContainer<?> wrk = new GenericContainer<>(parse(\"quay.io/dim/wrk:stable\"))\n      .withNetwork(Network.SHARED)\n      .withCreateContainerCmdModifier(it -> it.withEntrypoint(\"wrk\"))\n      .withCommand(\"-t4 -c128 -d100s http://frontend:8081 --latency\");\n    containers.add(wrk);\n\n    grafanaDashboards.dependsOn(grafana);\n    wrk.dependsOn(frontend, backend, prometheus, grafanaDashboards, zipkin);\n    if (storage != null) wrk.dependsOn(storage);\n\n    Startables.deepStart(Stream.of(wrk)).join();\n\n    System.out.println(\"Benchmark started.\");\n    if (zipkin != null) printContainerMapping(zipkin);\n    if (storage != null) printContainerMapping(storage);\n    printContainerMapping(backend);\n    printContainerMapping(frontend);\n    printContainerMapping(prometheus);\n    printContainerMapping(grafana);\n\n    while (wrk.isRunning()) {\n      Thread.sleep(1000);\n    }\n\n    // Wait for prometheus to do a final scrape.\n    Thread.sleep(5000);\n\n    System.out.println(\"Benchmark complete, wrk output:\");\n    System.out.println(wrk.getLogs().replace(\"\\n\\n\", \"\\n\"));\n\n    WebClient prometheusClient = WebClient.of(\n      \"h1c://\" + prometheus.getContainerIpAddress() + \":\" + prometheus.getFirstMappedPort());\n\n    System.out.printf(\"Messages received: %s%n\", prometheusValue(\n      prometheusClient, \"sum(zipkin_collector_messages_total)\"));\n    System.out.printf(\"Spans received: %s%n\", prometheusValue(\n      prometheusClient, \"sum(zipkin_collector_spans_total)\"));\n    System.out.printf(\"Spans dropped: %s%n\", prometheusValue(\n      prometheusClient, \"sum(zipkin_collector_spans_dropped_total)\"));\n\n    System.out.println(\"Memory quantiles:\");\n    printQuartiles(prometheusClient, \"jvm_memory_used_bytes{area=\\\"heap\\\"}\");\n    printQuartiles(prometheusClient, \"jvm_memory_used_bytes{area=\\\"nonheap\\\"}\");\n\n    System.out.printf(\n      \"Total GC time (s): %s%n\",\n      prometheusValue(prometheusClient, \"sum(jvm_gc_pause_seconds_sum)\"));\n    System.out.printf(\n      \"Number of GCs: %s%n\", prometheusValue(prometheusClient, \"sum(jvm_gc_pause_seconds_count)\"));\n\n    System.out.println(\"POST Spans latency (s)\");\n    printHistogram(prometheusClient, \"\"\"\n      http_server_requests_seconds_bucket{\n      method=\"POST\",status=\"202\",uri=\"/api/v2/spans\"}\n      \"\"\");\n\n    if (WAIT_AFTER_BENCHMARK) {\n      System.out.println(\"\"\"\n        Keeping containers running until explicit termination. \\\n        Feel free to poke around in grafana.\\\n        \"\"\");\n      Thread.sleep(Long.MAX_VALUE);\n    }\n  }\n\n  GenericContainer<?> createZipkinContainer(@Nullable GenericContainer<?> storage)\n    throws Exception {\n    Map<String, String> env = new LinkedHashMap<>();\n    if (storage != null) {\n      String name = storage.getLabels().get(\"name\");\n      String host = name;\n      int port = storage.getExposedPorts().get(0);\n      String address = host + \":\" + port;\n\n      env.put(\"STORAGE_TYPE\", storage.getLabels().get(\"storageType\"));\n      switch (name) {\n        case \"elasticsearch\":\n          env.put(\"ES_HOSTS\", \"http://\" + address);\n          break;\n        case \"cassandra\":\n        case \"cassandra3\":\n          env.put(\"CASSANDRA_CONTACT_POINTS\", address);\n          break;\n        case \"mysql\":\n          env.put(\"MYSQL_HOST\", host);\n          env.put(\"MYSQL_TCP_PORT\", Integer.toString(port));\n          env.put(\"MYSQL_USER\", \"zipkin\");\n          env.put(\"MYSQL_PASS\", \"zipkin\");\n          break;\n        default:\n          throw new IllegalArgumentException(\"Unknown storage \" + name +\n            \". Update startZipkin to map it to properties.\");\n      }\n    }\n\n    final GenericContainer<?> zipkin;\n    if (RELEASE_VERSION == null) {\n      zipkin = new GenericContainer<>(parse(\"ghcr.io/openzipkin/java:21.0.6_p7\"));\n      List<String> classpath = new ArrayList<>();\n      for (String item : System.getProperty(\"java.class.path\").split(File.pathSeparator)) {\n        Path path = Paths.get(item);\n        final String containerPath;\n        if (Files.isDirectory(path)) {\n          Path root = path.getParent();\n          while (root != null) {\n            try (Stream<Path> f = Files.list(root)) {\n              if (f.anyMatch(p -> p.getFileName().toString().equals(\"mvnw\"))) {\n                break;\n              }\n            }\n            root = root.getParent();\n          }\n          containerPath = root.relativize(path).toString().replace('\\\\', '/');\n        } else {\n          containerPath = path.getFileName().toString();\n        }\n        // Test containers currently doesn't support copying in a path with subdirectories that\n        // need to be created, so we mangle directory structure into a single directory with\n        // hyphens.\n        String classPathItem = \"/classpath-\" + containerPath.replace('/', '-');\n        zipkin.withCopyFileToContainer(MountableFile.forHostPath(item), classPathItem);\n        classpath.add(classPathItem);\n      }\n      zipkin.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint(\"java\"));\n      zipkin.setCommand(\"-cp\", String.join(\":\", classpath), \"zipkin.server.ZipkinServer\");\n      // Don't fail on classpath problem from missing lens, as we don't use it.\n      env.put(\"ZIPKIN_UI_ENABLED\", \"false\");\n    } else {\n      zipkin = new GenericContainer<>(parse(\"ghcr.io/openzipkin/zipkin:\" + RELEASE_VERSION));\n    }\n\n    zipkin\n      .withNetwork(Network.SHARED)\n      .withNetworkAliases(\"zipkin\")\n      .withExposedPorts(9411)\n      .withEnv(env)\n      .waitingFor(Wait.forHttp(\"/health\"));\n    containers.add(zipkin);\n    return zipkin;\n  }\n\n  static void printContainerMapping(GenericContainer<?> container) {\n    System.out.printf(\n      \"Container %s ports exposed at %s%n\", container.getDockerImageName(),\n      container.getExposedPorts().stream()\n        .map(port -> Map.entry(port,\n          \"http://\" + container.getContainerIpAddress() + \":\" + container.getMappedPort(port)))\n        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));\n  }\n\n  static void printQuartiles(WebClient prometheus, String metric) throws Exception {\n    for (double quantile : Arrays.asList(0.0, 0.25, 0.5, 0.75, 1.0)) {\n      String value = prometheusValue(prometheus, \"quantile(\" + quantile + \", \" + metric + \")\");\n      System.out.printf(\"%s[%s] = %s%n\", metric, quantile, value);\n    }\n  }\n\n  static void printHistogram(WebClient prometheus, String metric) throws Exception {\n    for (double quantile : Arrays.asList(0.5, 0.9, 0.99)) {\n      String value =\n        prometheusValue(prometheus, \"histogram_quantile(\" + quantile + \", \" + metric + \")\");\n      System.out.printf(\"%s[%s] = %s%n\", metric, quantile, value);\n    }\n  }\n\n  static String prometheusValue(WebClient prometheus, String query) throws Exception {\n    QueryStringEncoder encoder = new QueryStringEncoder(\"/api/v1/query\");\n    encoder.addParam(\"query\", query);\n    String response = prometheus.get(encoder.toString()).aggregate().join().contentUtf8();\n    return OBJECT_MAPPER.readTree(response).at(\"/data/result/0/value/1\").asText();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/java/zipkin2/server/internal/throttle/ThrottledCallBenchmarks.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport com.linecorp.armeria.common.metric.NoopMeterRegistry;\nimport com.netflix.concurrency.limits.limit.FixedLimit;\nimport com.netflix.concurrency.limits.limiter.SimpleLimiter;\nimport java.io.IOException;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Predicate;\nimport org.openjdk.jmh.annotations.Benchmark;\nimport org.openjdk.jmh.annotations.BenchmarkMode;\nimport org.openjdk.jmh.annotations.Fork;\nimport org.openjdk.jmh.annotations.Measurement;\nimport org.openjdk.jmh.annotations.Mode;\nimport org.openjdk.jmh.annotations.OutputTimeUnit;\nimport org.openjdk.jmh.annotations.Scope;\nimport org.openjdk.jmh.annotations.Setup;\nimport org.openjdk.jmh.annotations.State;\nimport org.openjdk.jmh.annotations.TearDown;\nimport org.openjdk.jmh.annotations.Threads;\nimport org.openjdk.jmh.annotations.Warmup;\nimport org.openjdk.jmh.runner.Runner;\nimport org.openjdk.jmh.runner.RunnerException;\nimport org.openjdk.jmh.runner.options.Options;\nimport org.openjdk.jmh.runner.options.OptionsBuilder;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\n@Measurement(iterations = 5, time = 1)\n@Warmup(iterations = 10, time = 1)\n@Fork(3)\n@BenchmarkMode(Mode.SampleTime)\n@OutputTimeUnit(TimeUnit.MICROSECONDS)\n@State(Scope.Thread)\n@Threads(2)\npublic class ThrottledCallBenchmarks {\n  ExecutorService fakeCallExecutor = Executors.newSingleThreadExecutor();\n  ExecutorService executor = Executors.newSingleThreadExecutor();\n  ThrottledCall call;\n\n  @Setup public void setup() {\n    executor = Executors.newSingleThreadExecutor();\n    fakeCallExecutor = Executors.newSingleThreadExecutor();\n    SimpleLimiter<Void> limiter = SimpleLimiter.newBuilder().limit(FixedLimit.of(1)).build();\n    LimiterMetrics metrics = new LimiterMetrics(NoopMeterRegistry.get());\n    Predicate<Throwable> isOverCapacity = RejectedExecutionException.class::isInstance;\n    call =\n      new ThrottledCall(new FakeCall(fakeCallExecutor), executor, limiter, metrics, isOverCapacity);\n  }\n\n  @TearDown public void tearDown() {\n    executor.shutdown();\n    fakeCallExecutor.shutdown();\n  }\n\n  @Benchmark public Object execute() throws IOException {\n    return call.clone().execute();\n  }\n\n  @Benchmark public void execute_overCapacity() throws IOException {\n    ThrottledCall overCapacity = (ThrottledCall) call.clone();\n    ((FakeCall) overCapacity.delegate).overCapacity = true;\n\n    try {\n      overCapacity.execute();\n    } catch (RejectedExecutionException e) {\n      assert e == OVER_CAPACITY;\n    }\n  }\n\n  @Benchmark public void execute_throttled() throws IOException {\n    call.limiter.acquire(null); // capacity is 1, so this will overdo it.\n    call.clone().execute();\n  }\n\n  static final RejectedExecutionException OVER_CAPACITY = new RejectedExecutionException();\n\n  static final class FakeCall extends Call.Base<Void> {\n    final Executor executor;\n    boolean overCapacity = false;\n\n    FakeCall(Executor executor) {\n      this.executor = executor;\n    }\n\n    @Override public Void doExecute() throws IOException {\n      if (overCapacity) throw OVER_CAPACITY;\n      return null;\n    }\n\n    @Override public void doEnqueue(Callback<Void> callback) {\n      executor.execute(() -> {\n        if (overCapacity) {\n          callback.onError(OVER_CAPACITY);\n        } else {\n          callback.onSuccess(null);\n        }\n      });\n    }\n\n    @Override public FakeCall clone() {\n      return new FakeCall(executor);\n    }\n  }\n\n  // Convenience main entry-point\n  public static void main(String[] args) throws RunnerException {\n    Options opt = new OptionsBuilder()\n      .addProfiler(\"gc\")\n      .include(\".*\" + ThrottledCallBenchmarks.class.getSimpleName())\n      .build();\n\n    new Runner(opt).run();\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/resources/create-datasource-and-dashboard.sh",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -xeuo pipefail\n\nif ! curl --retry 5 --retry-connrefused --retry-delay 0 -sf http://grafana:3000/api/datasources/name/prom; then\n    curl -sf -X POST -H \"Content-Type: application/json\" \\\n         --data-binary '{\"name\":\"prom\",\"type\":\"prometheus\",\"url\":\"http://prometheus:9090\",\"access\":\"proxy\",\"isDefault\":true}' \\\n         http://grafana:3000/api/datasources\nfi\n\ndashboard_id=1598\nlast_revision=$(curl -sf https://grafana.com/api/dashboards/${dashboard_id}/revisions | grep '\"revision\":' | sed 's/ *\"revision\": \\([0-9]*\\),/\\1/' | sort -n | tail -1)\n\necho '{\"dashboard\": ' > data.json\ncurl -s https://grafana.com/api/dashboards/${dashboard_id}/revisions/${last_revision}/download >> data.json\necho ', \"inputs\": [{\"name\": \"DS_PROMETHEUS\", \"pluginId\": \"prometheus\", \"type\": \"datasource\", \"value\": \"prom\"}], \"overwrite\": false}' >> data.json\ncurl --retry-connrefused --retry 5 --retry-delay 0 -sf \\\n     -X POST -H \"Content-Type: application/json\" \\\n     --data-binary @data.json \\\n     http://grafana:3000/api/dashboards/import\n"
  },
  {
    "path": "benchmarks/src/test/resources/prometheus.yml",
    "content": "global:\n  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n\nscrape_configs:\n  - job_name: 'prometheus'\n    static_configs:\n      - targets: ['localhost:9090']\n  - job_name: 'zipkin'\n    scrape_interval: 5s\n    metrics_path: '/prometheus'\n    static_configs:\n      - targets: ['zipkin:9411']\n"
  },
  {
    "path": "benchmarks/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# uncomment to include kafka consumer configuration in test logs\n#logger.org.apache.kafka.clients.level=info\n"
  },
  {
    "path": "benchmarks/src/test/resources/zipkin2-chinese.json",
    "content": "{\n  \"traceId\": \"4d1e00c0db9010db86154a4ba6e91385\",\n  \"parentId\": \"86154a4ba6e91385\",\n  \"id\": \"4d1e00c0db9010db\",\n  \"kind\": \"CLIENT\",\n  \"name\": \"个人信息查询\",\n  \"timestamp\": 1472470996199000,\n  \"duration\": 207000,\n  \"localEndpoint\": {\n    \"serviceName\": \"订单维护服务\",\n    \"ipv6\": \"2001:db8::c001\"\n  },\n  \"remoteEndpoint\": {\n    \"serviceName\": \"个人信息服务\",\n    \"ipv4\": \"192.168.99.101\",\n    \"port\": 9000\n  },\n  \"tags\": {\n    \"http.path\": \"/person/profile/query\",\n    \"http.status_code\": \"403\",\n    \"error\": \"此用户没有操作权限\"\n  }\n}\n"
  },
  {
    "path": "benchmarks/src/test/resources/zipkin2-client.json",
    "content": "{\n  \"traceId\": \"4d1e00c0db9010db86154a4ba6e91385\",\n  \"parentId\": \"86154a4ba6e91385\",\n  \"id\": \"4d1e00c0db9010db\",\n  \"kind\": \"CLIENT\",\n  \"name\": \"get\",\n  \"timestamp\": 1472470996199000,\n  \"duration\": 207000,\n  \"localEndpoint\": {\n    \"serviceName\": \"frontend\",\n    \"ipv6\": \"2001:db8::c001\"\n  },\n  \"remoteEndpoint\": {\n    \"serviceName\": \"backend\",\n    \"ipv4\": \"192.168.99.101\",\n    \"port\": 9000\n  },\n  \"annotations\": [\n    {\n      \"timestamp\": 1472470996238000,\n      \"value\": \"foo\"\n    },\n    {\n      \"timestamp\": 1472470996403000,\n      \"value\": \"bar\"\n    }\n  ],\n  \"tags\": {\n    \"http.path\": \"/api\",\n    \"clnt/finagle.version\": \"6.45.0\"\n  }\n}\n"
  },
  {
    "path": "build-bin/README.md",
    "content": "# Test and Deploy scripts\n\nThis is a Maven+Docker project, which uses standard conventions for test and deploy with some\nexceptions.\n\nOn [../zipkin-lens]:\n* [maybe_install_npm] is used to build on an unsupported node.js architecture.\n* [maven_go_offline] additionally seeds the NPM cache\n\nOn test:\n* [test], used by [../.github/workflows/test.yml] runs Maven unit and integration tests.\n  * Its \"test\" job skips docker, as they are run in parallel in \"test_docker\"\n* [../.github/workflows/readme_test.yml] tests build commands in [../zipkin-server] and [../docker]\n  * zipkin, zipkin-lens and zipkin-slim Docker builds use `RELEASE_FROM_MAVEN_BUILD=true`\n    * this avoids invoking Maven builds from within Docker, which are costly and fragile\n  * Docker tests run in sequence to avoid queueing delays, which take longer than builds themselves.\n\nOn deploy:\n* [deploy], used by [../.github/workflows/deploy.yml] publishes jars and Docker images.\n* [javadoc_to_gh_pages] pushes Javadoc to the gh-pages branch on MAJOR.MINOR.PATCH branch, but not master.\n  * gh-pages is addressable via https://zipkin.io/zipkin/\n* Besides production Docker images, this project includes [../docker/test-images].\n  * [docker_push] pushes test-images, but only to ghcr.io\n\n[//]: # (Below here should be standard for all projects)\n\n## Build Overview\n`build-bin` holds portable scripts used in CI to test and deploy the project.\n\nThe scripts here are portable. They do not include any CI provider-specific logic or ENV variables.\nThis helps `test.yml` (GitHub Actions) and alternatives contain nearly the same contents, even if\ncertain OpenZipkin projects need slight adjustments here. Portability has proven necessary, as\nOpenZipkin has had to transition CI providers many times due to feature and quota constraints.\n\nThese scripts serve a second purpose, which is to facilitate manual releases, which has also\nhappened many times due usually to service outages of CI providers. While tempting to use\nCI-provider specific tools, doing so can easily create a dependency where no one knows how to\nrelease anymore. Do not use provider-specific mechanisms to implement release flow. Instead,\nautomate triggering of the scripts here.\n\nThe only scripts that should be modified per project are in the base directory. Those in\nsubdirectories, such as [docker](docker), should not vary project to project except accident of\nversion drift. Intentional changes in subdirectories should be relevant and tested on multiple\nprojects to ensure they can be blindly copy/pasted.\n\nConversely, the files in the base directory are project specific entry-points for test and deploy\nactions and are entirely appropriate to vary per project. Here's an overview:\n\n## Lint\n\nLint makes sure that documentation and workflows are in-tact. CI providers should be configured to\nrun lint on pull requests or pushes to the master branch, notably when the tag is blank. Linters\nshould only run on documentation-only commits or those who affect workflow files. Linters must not\ndepend on authenticated resources, as running lint can leak credentials.\n\n* [configure_lint](configure_lint) - Ensures linters are installed\n* [lint](lint) - Runs the linters\n\nWe minimally check the following:\n\n* [markdown-link-check](https://github.com/tcort/markdown-link-check) on our Markdown content.\n  * we maintain [mlc_config.json](mlc_config.json) for exceptions\n* [yamllint](https://github.com/adrienverge/yamllint) on our GitHub Actions Workflow YAML.\n  * occasionally need line length exceptions via `# yamllint disable-line rule:line-length`\n\n### Example GitHub Actions setup\n\nA simplest GitHub Actions `lint.yml` runs linters after configuring them, but only on relevant event\nconditions. The name `lint.yml` and job `lint` allows easy references to status badges and parity of\nthe scripts it uses.\n\nThe `on:` section obviates job creation and resource usage for irrelevant events. Notably, GitHub\nActions includes the ability to skip documentation-only jobs.\n\nHere's a partial `lint.yml` including only the aspects mentioned above.\n```yaml\n---\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths:\n      - '**/*.md'\n      - '.github/workflows/*.yml'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths:\n      - '**/*.md'\n      - '.github/workflows/*.yml'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    # skip commits made by the release plugin\n    if: \"!contains(github.event.head_commit.message, 'maven-release-plugin')\"\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Lint\n        run: |\n          build-bin/configure_lint\n          build-bin/lint\n```\n\n## Test\n\nTest builds and runs any tests of the project, including integration tests. CI providers should be\nconfigured to run tests on pull requests or pushes to the master branch, notably when the tag is\nblank. Tests should not run on documentation-only commits. Tests must not depend on authenticated\nresources, as running tests can leak credentials.\n\n * [configure_test](configure_test) - Sets up build environment for tests.\n * [test](test) - Builds and runs tests for this project.\n\n### Example GitHub Actions setup\n\nA simplest GitHub Actions `test.yml` runs tests after configuring them, but only on relevant event\nconditions. The name `test.yml` and job `test` allows easy references to status badges and parity of\nthe scripts it uses.\n\nThe `on:` section obviates job creation and resource usage for irrelevant events. Notably, GitHub\nActions includes the ability to skip documentation-only jobs.\n\nHere's a partial `test.yml` including only the aspects mentioned above.\n```yaml\non:  # yamllint disable-line rule:truthy\n  push:  # non-tagged pushes to master\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n  pull_request:  # pull requests targeted at the master branch.\n    branches:\n      - master\n    paths-ignore:\n      - '**/*.md'\n      - './build-bin/*lint'\n      - ./build-bin/mlc_config.json\n\njobs:\n  test:\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Test\n        run: |\n          build-bin/configure_test\n          build-bin/test\n```\n\n## Deploy\n\nDeploy builds and pushes artifacts to a remote repository for master and release commits on it. CI\nproviders deploy pushes to master on when the tag is blank, but not on documentation-only commits.\nReleases should deploy on version tags (ex `/^[0-9]+\\.[0-9]+\\.[0-9]+/`), without consideration of if\nthe commit is documentation only or not.\n\n * [configure_deploy](configure_deploy) - Sets up environment and logs in.\n * [deploy](deploy) - deploys the project, with arg1 being \"master\" or a release commit like \"1.2.3\"\n\n### Example GitHub Actions setup\n\nA simplest GitHub Actions `deploy.yml` deploys after logging in, but only on relevant event\nconditions. The name `deploy.yml` and job `deploy` allows easy references to status badges and\nparity of the scripts it uses.\n\nThe `on:` section obviates job creation and resource usage for irrelevant events. GitHub Actions\ncannot implement \"master, except documentation only-commits\" in the same file. Hence, deployments of\nmaster will happen even on README change.\n\nHere's a partial `deploy.yml` including only the aspects mentioned above. Notice env variables are\nexplicitly defined and `on.tags` is a [glob pattern](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet).\n\n```yaml\non:  # yamllint disable-line rule:truthy\n  push:\n    branches:\n      - master\n    # Don't deploy tags because the same commit for MAJOR.MINOR.PATCH is also\n    # on master: Redundant deployment of a release version will fail uploading.\n    tags-ignore:\n      - '*'\n\njobs:\n  deploy:\n    runs-on: ubuntu-24.04  # newest available distribution, aka noble\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1  # only needed to get the sha label\n      - name: Deploy\n        env:\n          GH_USER: ${{ secrets.GH_USER }}\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n        run: |  # GITHUB_REF = refs/heads/master or refs/tags/MAJOR.MINOR.PATCH\n          build-bin/configure_deploy &&\n          build-bin/deploy $(echo ${GITHUB_REF} | cut -d/ -f 3)\n```\n"
  },
  {
    "path": "build-bin/configure_deploy",
    "content": "#!/bin/sh -ue\n\n# This script sets up anything needed for `./deploy`. Do not assume `configure_test` was called.\n#\n# See [README.md] for an explanation of this and how CI should use it.\nbuild-bin/docker/configure_docker_push\nbuild-bin/gpg/configure_gpg\n\n# When arch isn't amd64 zipkin-lens needs offline install\nbuild-bin/maybe_install_npm\nbuild-bin/maven_go_offline\n\n# openzipkin/zipkin publishes Javadoc to https://zipkin.io/zipkin/ on release\nbuild-bin/git/login_git\n"
  },
  {
    "path": "build-bin/configure_lint",
    "content": "#!/bin/sh -ue\n\n# Attempt to install markdown-link-check if absent\n# Pinned until https://github.com/tcort/markdown-link-check/issues/369\nmarkdown-link-check -V || npm install -g markdown-link-check@3.12.2\n\n# Attempt to install yamllint if absent\nyamllint -v || pip install --user yamllint\n"
  },
  {
    "path": "build-bin/configure_test",
    "content": "#!/bin/sh -ue\n\n# This script sets up anything needed for `./test`. This should not login to anything, as that\n# should be done in `configure_deploy`.\n#\n# See [README.md] for an explanation of this and how CI should use it.\nbuild-bin/docker/configure_docker\n\n# When arch isn't amd64 zipkin-lens needs offline install\nbuild-bin/maybe_install_npm\nbuild-bin/maven_go_offline\n"
  },
  {
    "path": "build-bin/deploy",
    "content": "#!/bin/sh -ue\n\n# This script deploys a master or release version.\n#\n# See [README.md] for an explanation of this and how CI should use it.\nversion=${1:-master}\n\n# Use implicit version when master, if we can..\nif [ \"${version}\" = \"master\" ]; then\n  version=$(sed -En 's/.*<version>(.*)<\\/version>.*/\\1/p' pom.xml| head -1)\nfi\n\nbuild-bin/maven/maven_deploy\nexport RELEASE_FROM_MAVEN_BUILD=true\nbuild-bin/docker_push ${version}\n\n# openzipkin/zipkin publishes Javadoc to gh-pages (https://zipkin.io/zipkin/) on release\ncase ${version} in\n  *-SNAPSHOT )\n    ;;\n  * )\n    build-bin/javadoc_to_gh_pages ${version}\n    ;;\nesac\n"
  },
  {
    "path": "build-bin/docker/configure_docker",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Defends against build outages caused by Docker Hub (docker.io) pull rate limits.\n#\n# It should not login to anything, as that should be done in `configure_docker_push`\n\nset -ue\n\n# The below sets up testcontainers configuration, which will be ignored if it isn't used. Even if\n# this is Docker related, it is coupled to integration tests configuration invoked with Maven.\n# * See https://www.testcontainers.org/supported_docker_environment/image_registry_rate_limiting/\n# * checks.disable=true - saves time and a docker.io pull of alpine\n# * ryuk doesn't count against docker.io rate limits because Docker approved testcontainers as OSS\necho checks.disable=true >> ~/.testcontainers.properties\n\n# We don't use any docker.io images, but add a Google's mirror in case something implicitly does\n# * See https://cloud.google.com/container-registry/docs/pulling-cached-images\necho '{ \"registry-mirrors\": [\"https://mirror.gcr.io\"] }' | sudo tee /etc/docker/daemon.json\nsudo service docker restart\n"
  },
  {
    "path": "build-bin/docker/configure_docker_push",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Ensures Docker is logged in and it can build multi-architecture.\n# This should be used instead of `configure_docker` when a push will occur.\n#\n# This should only happen when we are publishing multi-arch builds, as otherwise the setup could use\n# Docker Hub pull quota and possibly cause a build outage.\n\nset -ue\n\n# Verify we are on an arch that can publish multi-arch images\narch=$($(dirname \"$0\")/docker_arch)\nif [ \"${arch}\" != \"amd64\" ]; then\n  >&2 echo \"multiarch/qemu-user-static doesn't support arch ${arch}\"\n  exit 1\nfi\n\n# Enable experimental features on the server (multi-arch)\necho '{ \"experimental\":true, \"registry-mirrors\": [\"https://mirror.gcr.io\"] }' | sudo tee /etc/docker/daemon.json\n\nsudo service docker restart\n# Enable experimental client features (multi-arch)\nmkdir -p ${HOME}/.docker && echo '{\"experimental\":\"enabled\"}' > ${HOME}/.docker/config.json\n\n# Log in to GitHub Container Registry and Docker Hub for releasing images\n# This effects ${HOME}/.docker/config.json, which was created above\n\n# All images push to ghcr.io\necho \"${GH_TOKEN}\" | docker login ghcr.io -u \"${GH_USER}\" --password-stdin\n\n# Some images push to docker.io: check first if credentials exist or not.\nif [ -n \"${DOCKERHUB_USER:-}\" ]; then\n  echo \"${DOCKERHUB_TOKEN}\" | docker login -u \"${DOCKERHUB_USER}\" --password-stdin\nfi\n\n# Enable execution of different multi-architecture containers by QEMU and binfmt_misc\n# See https://github.com/multiarch/qemu-user-static\n#\n# Mirrored image use to avoid docker.io pulls:\n# docker tag multiarch/qemu-user-static:7.2.0-1 ghcr.io/openzipkin/multiarch-qemu-user-static:latest\n#\n# Note: This image only works on x86_64/amd64 architecture.\n# See: https://github.com/multiarch/qemu-user-static#supported-host-architectures\ndocker run --rm --privileged ghcr.io/openzipkin/multiarch-qemu-user-static --reset -p yes\n"
  },
  {
    "path": "build-bin/docker/docker-healthcheck",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# HEALTHCHECK for use in `docker ps`, `docker compose ps`, or a readiness probe in k8s.\n#\n# The following variables are read from ENV in the Dockerfile or env readable from pid 1.\n# * HEALTHCHECK_KIND - must be \"http\" or \"tcp\". Defaults to \"http\"\n#   * When \"http\" the GET path to check is /health unless overridden by HEALTHCHECK_PATH\n# * HEALTHCHECK_IP - Defaults to $(hostname -i || 127.0.0.1)\n# * HEALTHCHECK_PORT - Not always the service port, but in Zipkin it typically is. No default\n#\n# Setup like this:\n# # We use a 30s start period to avoid marking the container unhealthy on slow or contended CI hosts\n#  HEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\n#\n# Note: this is named docker-healthcheck, not docker_healthcheck like our other scripts. That's due\n# to conventions in https://github.com/docker-library/healthcheck/\n\n# Fail on unset variables, but don't quit on rc!=0, so we can log what happened\nset -u +e\n\n# Export healthcheck variables we can read from pid 1. Some processes such as nginx wipe env. In\n# that case, this will still be able to see variables set as ENV instructions in the Dockerfile.\nexport `cat /proc/1/environ| tr '\\0' '\\n'| \\\n  egrep '^(HEALTHCHECK_KIND|HEALTHCHECK_IP|HEALTHCHECK_PATH|HEALTHCHECK_PORT)='`\n\nkind=${HEALTHCHECK_KIND:-http}\nip=${HEALTHCHECK_IP:-$(hostname -i || echo '127.0.0.1')}\nport=${HEALTHCHECK_PORT?-is required}\n\ncase ${kind} in\n  http )\n    path=${HEALTHCHECK_PATH:-/health}\n    endpoint=\"http://${ip}:${port}${path}\"\n    # Use b3:0 to ensure health checks aren't traced\n    # Use timeout 1s (-T 1) as the health check interval is often 1s\n    out=$(wget -T 1 -qO- \"${endpoint}\" --header=b3:0 2>&1)\n    rc=$?\n    ;;\n  tcp )\n    # Use timeout 1s (-w 1) as the health check interval is often 1s\n    out=$(nc -w 1 -z ${ip} ${port} 2>&1)\n    rc=$?\n    ;;\n  * )\n    >&2 echo \"Invalid HEALTHCHECK_KIND: ${kind}\"\n    exit 1\n    ;;\nesac\n\nif [ \"$rc\" = \"0\" ]; then exit 0; fi\n>&2 echo \"Health check failed with code ${rc} response: ${out}\"\nexit 1\n"
  },
  {
    "path": "build-bin/docker/docker_arch",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script gets a normalized name for the architecture as used in Docker. This will be a subset\n# of ones supported by buildx: https://github.com/docker/buildx/releases. This is a subset because\n# for us to support a platform implies also supporting things like running NPM on it, so it should\n# be an explicit act to add a platform.\n\nset -ue\n\n# Normalize docker_arch to what's available\n#\n# Note: s390x and ppc64le were added for Knative\ndocker_arch=${DOCKER_ARCH:-$(uname -m)}\ncase ${docker_arch} in\n  amd64* )\n    docker_arch=amd64\n    ;;\n  x86_64* )\n    docker_arch=amd64\n    ;;\n  arm64* )\n    docker_arch=arm64\n    ;;\n  aarch64* )\n    docker_arch=arm64\n    ;;\n  s390x* )\n    docker_arch=s390x\n    ;;\n  ppc64le* )\n    docker_arch=ppc64le\n    ;;\n  * )\n    >&2 echo \"Unsupported DOCKER_ARCH: ${docker_arch}\"\n    exit 1;\nesac\n\necho ${docker_arch}\n"
  },
  {
    "path": "build-bin/docker/docker_args",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This builds common docker arguments used by docker_build and docker_push.\n\n# This script checks each variable value, so it isn't important to fail on unbound (set -u)\nset -e\n\ndocker_file=${DOCKER_FILE:-docker/Dockerfile}\nif ! test -f ${docker_file}; then\n  >&2 echo \"${docker_file} doesn't exist\"\n  exit 1\nfi\n\n# Add default labels for the day and the SHA we're building from\ndocker_args=\"${DOCKER_ARGS:-} -f ${docker_file} \\\n--label org.opencontainers.image.created=$(date +%Y-%m-%d) \\\n--label org.opencontainers.image.revision=$(git rev-parse --short HEAD)\"\n\nversion=${1:-}\nif [ -n \"${version}\" ]; then\n  docker_args=\"${docker_args} --build-arg version=${version}\"\nfi\n\n# When true, the artifact searched with `build-bin/maven/unjar` is in the context root\n# Ensure .dockerignore allows the artifacts intended.\nif [ \"${RELEASE_FROM_MAVEN_BUILD}\" = \"true\" ]; then\n  docker_args=\"${docker_args} --build-arg release_from_maven_build=true\"\nfi\n\n# When non-empty the target to build.\nif [ -n \"${DOCKER_TARGET}\" ]; then\n  docker_args=\"${docker_args} --target ${DOCKER_TARGET}\"\nfi\n\n# When non-empty, becomes the base layer including tag appropriate for the image being built.\n# e.g. ghcr.io/openzipkin/java:21.0.6_p7-jre\n#\n# This is not required to be a base (FROM scratch) image like ghcr.io/openzipkin/alpine:3.12.3\n# See https://docs.docker.com/glossary/#parent-image\nif [ -n \"${DOCKER_PARENT_IMAGE}\" ]; then\n  docker_args=\"${docker_args} --build-arg docker_parent_image=${DOCKER_PARENT_IMAGE}\"\nfi\n\n# When non-empty, becomes the build-arg alpine_version. e.g. \"3.12.3\"\n# Used to align base layers from https://github.com/orgs/openzipkin/packages/container/package/alpine\nif [ -n \"${ALPINE_VERSION}\" ]; then\n  docker_args=\"${docker_args} --build-arg alpine_version=${ALPINE_VERSION}\"\nfi\n\n# When non-empty, becomes the build-arg java_version. e.g. \"21.0.6_p7\"\n# Used to align base layers from https://github.com/orgs/openzipkin/packages/container/package/java\nif [ -n \"${JAVA_VERSION}\" ]; then\n  docker_args=\"${docker_args} --build-arg java_version=${JAVA_VERSION}\"\n\n  # Only set java_home build arg when we control or can verify it.\n  java_major_version=$(echo ${JAVA_VERSION}| cut -f1 -d .)\n  case ${java_major_version} in\n    8) java_home=/usr/lib/jvm/java-1.8-openjdk;;\n    1?|2?|3?) java_home=/usr/lib/jvm/java-${java_major_version}-openjdk;;\n  esac\n\n  if [ -n \"${java_home}\" ]; then docker_args=\"${docker_args} --build-arg java_home=${java_home}\"; fi\nfi\n\n# When non-empty, becomes the build-arg maven_classifier. e.g. \"module\" or \"exec\"\n# Used as the classifier arg to ./build-bin/maven/maven_unjar. Allows building two images with the\n# same Dockerfile, varying on classifier, like openzipkin/zipkin vs openzipkin/zipkin-slim\nif [ -n \"${MAVEN_CLASSIFIER}\" ]; then\n  docker_args=\"${docker_args} --build-arg maven_classifier=${MAVEN_CLASSIFIER}\"\nfi\n\necho ${docker_args}\n"
  },
  {
    "path": "build-bin/docker/docker_block_on_health",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Blocks until a named docker container with a valid HEALTHCHECK instruction is healthy or not:\n\nset -ue\n\ncontainer_name=${1?container_name is required}\ncontainer_id=$(docker ps  -q -f name=${container_name})\n\nwhile status=\"$(docker inspect --format=\"{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}\" \"${container_id}\")\"; do\n  case $status in\n    starting) sleep 1;;\n    healthy) exit 0;;\n    unhealthy) exit 1;;\n  esac\ndone\nexit 1\n"
  },
  {
    "path": "build-bin/docker/docker_build",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\ndocker_tag=${1?full docker_tag is required. Ex openzipkin/zipkin:test}\nversion=${2:-}\ndocker_args=$($(dirname \"$0\")/docker_args ${version})\n\n# We don't need build kit, but Docker 20.10 no longer accepts --platform\n# without it. It is simpler to always enable it vs require maintainers to use\n# alternate OCI tools. See https://github.com/moby/moby/issues/41552\nexport DOCKER_BUILDKIT=1\n\necho \"Building image ${docker_tag}\"\ndocker build --network=host --pull ${docker_args} --tag ${docker_tag} .\n"
  },
  {
    "path": "build-bin/docker/docker_push",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script pushes images to GitHub Container Registry (ghcr.io).\n#\n# When a release, and DOCKER_RELEASE_REPOS is unset they also push to Docker Hub (docker.io).\n#\n# Note: In CI, `configure_docker_push` must be called before invoking this.\n#\n# Avoid buildx on push for reasons including:\n#  * Caching is more complex as builder instances must be considered\n#  * Platform builds run in parallel, leading to port conflict failures (ex in cassandra)\n#  * 0.4.2 multi-platform builds have failed due to picking the wrong image for a FROM instruction\nset -ue\n\ndocker_image=${1?docker_image is required, notably without a tag. Ex openzipkin/zipkin}\nversion=${2:-master}\n\n# We don't need build kit, but Docker 20.10 no longer accepts --platform\n# without it. It is simpler to always enable it vs require maintainers to use\n# alternate OCI tools. See https://github.com/moby/moby/issues/41552\nexport DOCKER_BUILDKIT=1\n\ncase ${version} in\n  master )\n    is_release=false\n    ;;\n  *-SNAPSHOT )\n    is_release=false\n    ;;\n  * )\n    is_release=true\n    ;;\nesac\n\nif [ \"${is_release}\" = \"true\" ]; then\n  docker_tags=${DOCKER_TAGS:-}\n  if [ -z \"${docker_tags}\" ]; then\n    major_tag=$(echo \"${version}\" | cut -f1 -d. -s)\n    minor_tag=$(echo \"${version}\" | cut -f1-2 -d. -s)\n    subminor_tag=\"${version}\"\n    docker_tags=\"$subminor_tag $minor_tag $major_tag latest\"\n  fi\n  docker_repos=${DOCKER_RELEASE_REPOS:-ghcr.io docker.io}\nelse\n  docker_tags=master\n  docker_repos=ghcr.io\nfi\n\ntags=\"\"\nfor repo in ${docker_repos}; do\n  tags=\"${tags}\\n\"\n  for tag in ${docker_tags}; do\n    tags=\"${tags} ${repo}/${docker_image}:${tag}\"\n  done\ndone\n\ndocker_args=$($(dirname \"$0\")/docker_args ${version})\n# Note: s390x and ppc64le were added for Knative\ndocker_archs=${DOCKER_ARCHS:-amd64 arm64 s390x ppc64le}\n\necho \"Will build the following architectures: ${docker_archs}\"\n\ndocker_tag0=\"$(echo ${docker_tags} | awk '{print $1;}')\"\ndocker_arch0=\"$(echo ${docker_archs} | awk '{print $1;}')\"\narch_tags=\"\"\nfor docker_arch in ${docker_archs}; do\n  arch_tag=${docker_image}:${docker_tag0}-${docker_arch}\n  echo \"Building tag ${arch_tag}...\"\n  docker build --pull ${docker_args} --platform linux/${docker_arch} --tag ${arch_tag} .\n  arch_tags=\"${arch_tags} ${arch_tag}\"\ndone\n\necho \"Will push the following tags:\\n${tags}\"\n\nif [ \"${docker_arch0}\" = \"${docker_archs}\" ]; then\n  # single architecture\n  arch_tag=${docker_image}:${docker_tag0}-${docker_arch0}\n\n  for tag in $(echo ${tags} | xargs); do\n    docker tag ${arch_tag} ${tag}\n    echo \"Pushing tag ${tag}...\"\n    docker push ${tag}\n  done\n\nelse\n  # multi-architecture: make a manifest\n  for tag in $(echo ${tags} | xargs); do\n    manifest_tags=\"\"\n    for arch_tag in ${arch_tags}; do\n      docker_arch=$(echo ${arch_tag} | sed 's/.*-//g')\n      manifest_tag=${tag}-${docker_arch}\n      docker tag ${arch_tag} ${manifest_tag}\n      echo \"Pushing tag ${manifest_tag}...\"\n      docker push ${manifest_tag}\n      manifest_tags=\"${manifest_tags} ${manifest_tag}\"\n    done\n\n    docker manifest create ${tag} ${manifest_tags}\n\n    for manifest_tag in ${manifest_tags}; do\n      docker_arch=$(echo ${manifest_tag} | sed 's/.*-//g')\n      docker manifest annotate ${tag} ${manifest_tag} --os linux --arch ${docker_arch}\n    done\n\n    echo \"Pushing manifest ${manifest_tag}...\"\n    docker manifest push -p ${tag}\n  done\nfi\n"
  },
  {
    "path": "build-bin/docker/docker_test_image",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Tests a an image by awaiting its HEALTHCHECK.\n#\n# This can be made more sophisticated via docker-compose. For example, you can set a utility\n# container that issues `wget` against your actual container in its HEALTHCHECK.\n\nset -ue\n\n# export this variable so that docker compose can use it\nexport DOCKER_IMAGE=${1?full docker_tag is required. Ex openzipkin/zipkin:test}\n# The two options are to run a single container of the image, or docker compose which includes it.\ndocker_compose_file=${2:-build-bin/docker-compose-$(echo ${DOCKER_IMAGE}| sed 's~.*/\\(.*\\):.*~\\1~g').yml}\ndocker_container=${3:-sut}\n\n# First try to run the intended containers.\nhealth_rc=0\nif test -f \"${docker_compose_file}\"; then\n  docker compose -f \"${docker_compose_file}\" up --remove-orphans -d --quiet-pull || health_rc=1\nelse\n  docker run --name ${docker_container} -d ${DOCKER_IMAGE} || health_rc=1\nfi\n\n# Next, inspect the health. This is a blocking command which waits until HEALTHCHECK passes or not.\n# This will fail if the container isn't healthy or doesn't exist (ex compose failed before creation)\nif [ \"${health_rc}\" = \"1\" ] || ! build-bin/docker/docker_block_on_health ${docker_container}; then\n  >&2 echo \"*** Failed waiting for health of ${docker_container}\"\n\n  # Sadly, we can't `docker compose inspect`. This means we may not see the inspect output of the\n  # container that failed in docker compose until this is revised to work around compose/issues/4155\n  docker inspect --format='{{json .State.Health.Log}}' ${docker_container} || true\n\n  # Log any containers output to console before we remove them.\n  if test -f \"${docker_compose_file}\"; then\n    docker compose -f \"${docker_compose_file}\" logs\n  else\n    docker logs ${docker_container} || true\n  fi\n  health_rc=1\nfi\n\n# Clean up any containers, so that we can run this again without conflict.\nif test -f \"${docker_compose_file}\"; then\n  docker compose -f \"${docker_compose_file}\" down\nelse\n  docker kill ${docker_container} && docker rm ${docker_container}\nfi\n\nexit ${health_rc}\n\n"
  },
  {
    "path": "build-bin/docker-compose-zipkin-eureka.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# uses 2.4 so we can use condition: service_healthy\nversion: \"2.4\"\n\n# Test both authenticated and unauthenticated, as if there is a Spring problem,\n# the latter will crash. We only need to use HEALTHCHECK for this.\nservices:\n  eureka:\n    image: openzipkin/zipkin-eureka:test\n    container_name: eureka\n  sut:\n    image: openzipkin/zipkin-eureka:test\n    container_name: sut\n    environment:\n      EUREKA_USERNAME: testuser\n      EUREKA_PASSWORD: testpassword\n    depends_on:\n      eureka:\n        condition: service_healthy\n"
  },
  {
    "path": "build-bin/docker-compose-zipkin-ui.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# uses 2.4 so we can use condition: service_healthy\nversion: \"2.4\"\n\nservices:\n  zipkin:\n    # Use last build of Zipkin instead of adding a matrix build dependency\n    image: ghcr.io/openzipkin/zipkin-slim:master\n    container_name: zipkin\n  # Use fixed service and container name 'sut; so our test script can copy/pasta\n  sut:\n    # This is the image just built. It is not in a remote repository.\n    image: openzipkin/zipkin-ui:test\n    container_name: sut\n    environment:\n      # This is the default value, set explicitly here for visibility\n      ZIPKIN_BASE_URL: http://zipkin:9411\n    depends_on:\n      zipkin:\n        condition: service_healthy\n"
  },
  {
    "path": "build-bin/docker-compose-zipkin-uiproxy.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# uses 2.4 so we can use condition: service_healthy\nversion: \"2.4\"\n\nservices:\n  zipkin:\n    # Use last build of Zipkin instead of adding a matrix build dependency\n    image: ghcr.io/openzipkin/zipkin-slim:master\n    container_name: zipkin\n    environment:\n      ZIPKIN_UI_BASEPATH: /admin/zipkin\n  # Use fixed service and container name 'sut; so our test script can copy/pasta\n  sut:\n    # This is the image just built. It is not in a remote repository.\n    image: openzipkin/zipkin-ui:test\n    container_name: sut\n    environment:\n      ZIPKIN_UI_BASEPATH: /admin/zipkin\n    depends_on:\n      zipkin:\n        condition: service_healthy\n"
  },
  {
    "path": "build-bin/docker-compose-zipkin.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# uses 2.4 so we can use condition: service_healthy\nversion: \"2.4\"\n\nservices:\n  zipkin:\n    # Use last build of Zipkin instead of adding a matrix build dependency\n    image: openzipkin/zipkin:test\n    container_name: zipkin\n  # Use fixed service and container name 'sut; so our test script can copy/pasta\n  sut:\n    container_name: sut\n    image: ghcr.io/openzipkin/alpine:3.21.2\n    entrypoint: /bin/sh\n    # Keep the container running until HEALTHCHECK passes\n    command: \"-c \\\"sleep 5m\\\"\"\n    healthcheck:\n      # Return 0 when we can load the UI resources\n      test: wget -qO- --spider http://zipkin:9411/zipkin/\n    depends_on:\n      zipkin:\n        condition: service_healthy\n"
  },
  {
    "path": "build-bin/docker_push",
    "content": "#!/bin/sh -ue\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n\n# Pushes docker as part of `deploy` or from a trigger tag\nversion=${1:-master}\n\n# handle trigger pattern like /^docker-[0-9]+\\.[0-9]+\\.[0-9]+$/\ncase ${version} in\n  docker-* )\n    version=$(build-bin/git/version_from_trigger_tag docker- ${version})\n    ;;\nesac\n\nbuild-bin/docker/docker_push openzipkin/zipkin ${version}\nDOCKER_TARGET=zipkin-slim build-bin/docker/docker_push openzipkin/zipkin-slim ${version}\n\n# testing images only push to ghcro.io\nexport DOCKER_RELEASE_REPOS=ghcr.io\n# Don't attempt unfamiliar archs on test images\nexport DOCKER_ARCHS=\"amd64 arm64\"\n\nfor name in $(ls docker/test-images/*/Dockerfile|cut -f3 -d/); do\n  DOCKER_FILE=docker/test-images/${name}/Dockerfile build-bin/docker/docker_push openzipkin/${name} ${version}\ndone\n"
  },
  {
    "path": "build-bin/git/login_git",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\n# Allocate commits to CI, not the owner of the deploy key\ngit config user.name \"zipkinci\"\ngit config user.email \"zipkinci+zipkin-dev@googlegroups.com\"\n\n# Setup https authentication credentials, used by ./mvnw release:prepare\ngit config credential.helper \"store --file=.git/credentials\"\necho \"https://$GH_TOKEN:@github.com\" > .git/credentials\n"
  },
  {
    "path": "build-bin/git/version_from_trigger_tag",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script echos a `MAJOR.MINOR.PATCH` version tag based on..\n#  * arg1: XXXXX- prefix\n#  * arg2: XXXXX-MAJOR.MINOR.PATCH git trigger tag\n#\n# The script exits 1 if the prefix doesn't match. On success, the tag is deleted if it exists.\n#\n# Note: In CI, `build-bin/git/login_git` must be called before invoking this.\n\nset -ue\n\ntrigger_tag_prefix=${1?required. Ex docker- to match docker-1.2.3}\ntrigger_tag=${2?trigger_tag is required. Ex ${trigger_tag_prefix}1.2.3}\n\n# Checking sed output to determine success as exit code handling in sed or awk is awkward\nversion=$(echo \"${trigger_tag}\" | sed -En \"s/^${trigger_tag_prefix}([0-9]+\\.[0-9]+\\.[0-9]+)$/\\1/p\")\n\nif [ -z \"$version\" ]; then\n  >&2 echo invalid trigger tag: ${trigger_tag}\n  exit 1;\nfi\n\n# try to cleanup the trigger tag if it exists, but don't fail if it doesn't\ngit tag -d ${trigger_tag} 2>&- >&- || true\ngit push origin :${trigger_tag} 2>&- >&- || true\n\necho $version\n"
  },
  {
    "path": "build-bin/gpg/configure_gpg",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script prepares GPG, needed to sign jars for Sonatype deployment during `maven_deploy`\n\nset -ue\n\n# ensure GPG commands work non-interactively\nexport GPG_TTY=$(tty)\n# import signing key used for jar files\necho ${GPG_SIGNING_KEY} | base64 --decode | gpg --batch --passphrase ${GPG_PASSPHRASE} --import\n"
  },
  {
    "path": "build-bin/javadoc_to_gh_pages",
    "content": "#!/bin/sh -ue\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script creates JavaDoc for a version and pushes an update to gh-pages. This fails if there\n# are no javadoc jars in the current directory.\n#\n# This leaves the git context on gh-pages when done. For this reason, it is better to run this at\n# the end of deployment.\nversion=${1?version is required. Ex 2.22.2}\n\nrm -rf javadoc-builddir\nbuilddir=\"javadoc-builddir/${version}\"\n\njavadoc_jars=$(find . -name \"*${version}-javadoc.jar\")\nif [ -z \"${javadoc_jars}\"]; then\n  >&2 echo Incorrect state. javadoc jars should have been built before invoking this.\n  exit 1\nfi\n\n# Collect javadoc for all modules\nfor jar in ${javadoc_jars}; do\n  module=\"$(echo \"$jar\" | sed \"s~.*/\\(.*\\)-${version}-javadoc.jar~\\1~\")\"\n  this_builddir=\"$builddir/$module\"\n  if [ -d \"$this_builddir\" ]; then\n      # Skip modules we've already processed.\n      # We may find multiple instances of the same javadoc jar because of, for instance,\n      # integration tests copying jars around.\n      continue\n  fi\n  mkdir -p \"$this_builddir\"\n  unzip \"$jar\" -d \"$this_builddir\"\n  # Build a simple module-level index\n  echo \"<li><a href=\\\"${module}/index.html\\\">${module}</a></li>\" >> \"${builddir}/index.html\"\ndone\n\n# Update gh-pages\ngit fetch origin gh-pages:gh-pages\ngit checkout gh-pages\nrm -rf \"${version}\"\nmv \"javadoc-builddir/${version}\" ./\nrm -rf \"javadoc-builddir\"\n\n# Update simple version-level index\nif ! grep \"${version}\" index.html 2>/dev/null; then\n  echo \"<li><a href=\\\"${version}/index.html\\\">${version}</a></li>\" >> index.html\nfi\n\n# Ensure links are ordered by versions, latest on top\nsort -rV index.html > index.html.sorted\nmv index.html.sorted index.html\n\ngit add \"${version}\"\ngit add index.html\ngit commit -m \"Automatically updated javadocs for ${version}\"\ngit push origin gh-pages\n"
  },
  {
    "path": "build-bin/lint",
    "content": "#!/bin/sh -ue\n\nyamllint --format github .github/workflows/*.yml\nfind . -name \\*.md |grep -v node|xargs markdown-link-check -c ./build-bin/mlc_config.json\n"
  },
  {
    "path": "build-bin/maven/maven_build",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\nexport MAVEN_OPTS=\"$($(dirname \"$0\")/maven_opts)\"\nmaven_goal=${MAVEN_GOAL:-package}\nif [ -x ./mvnw ]; then alias mvn=${PWD}/mvnw; fi\n\n(\n  if [ \"${MAVEN_PROJECT_BASEDIR:-.}\" != \".\" ]; then cd ${MAVEN_PROJECT_BASEDIR}; fi\n  mvn -T1C -q --batch-mode -DskipTests \"${maven_goal}\" \"$@\"\n)\n"
  },
  {
    "path": "build-bin/maven/maven_build_or_unjar",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\n# Like `maven_unjar`, except when the version ends in SNAPSHOT and the artifact isn't in the $PWD,\n# it is built on-demand using `mvn package`. MAVEN_PROJECT_BASEDIR is used when the version arg or\n# artifact is missing.\n#\n# Note: Be careful building on-demand within Docker, as it can cause high bandwidth usage, pulling\n# dependencies for the project without the benefit of caching\n\ngroup_id=${1?group_id is required}\nartifact_id=${2?artifact_id is required}\nversion=${3?version is required}\nclassifier=${4:-}\n\npom=\"${MAVEN_PROJECT_BASEDIR:-.}/pom.xml\"\n\n# Use implicit version when master, if we can..\nif [ \"${version}\" = \"master\" ] && [ -f \"${pom}\" ]; then\n  version=$(sed -En 's/.*<version>(.*)<\\/version>.*/\\1/p' ${pom}| head -1)\nfi\n\n# rebuild the args with the version set.\nargs=\"${group_id} ${artifact_id} ${version} ${classifier}\"\n\n# Fail on unset variables, but don't quit on rc!=0, so we can log what happened\nset -u +e\n\nunjar_out=$($(dirname \"$0\")/maven_unjar $args 2>&1)\nunjar_rc=$?\n\nif [ \"${unjar_rc}\" = \"0\" ] || [ -z \"${pom:-}\" ]; then\n  exit 0;\nfi\n\ncase ${version} in\n  *-SNAPSHOT )\n    ;;\n  * )\n    >&2 echo ${unjar_out}\n    exit ${unjar_rc}\n    ;;\nesac\n\nset -ue\n\necho \"*** Building snapshot from source...\"\n$(dirname \"$0\")/maven_build -pl :${artifact_id} --am\n$(dirname \"$0\")/maven_unjar $args\n"
  },
  {
    "path": "build-bin/maven/maven_deploy",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\nexport MAVEN_OPTS=\"$($(dirname \"$0\")/maven_opts)\"\n\n# This script deploys a SNAPSHOT or release version to Sonatype.\n#\n# Note: In CI, `configure_maven_deploy` must be called before invoking this.\n./mvnw --batch-mode -s ./.settings.xml -Prelease -nsu -DskipTests clean deploy $@\n"
  },
  {
    "path": "build-bin/maven/maven_go_offline",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This is a go-offline that properly works with multi-module builds\n\nset -ue\n\nexport MAVEN_OPTS=\"$($(dirname \"$0\")/maven_opts)\"\nif [ -x ./mvnw ]; then alias mvn=${PWD}/mvnw; fi\n\n(\n  if [ \"${MAVEN_PROJECT_BASEDIR:-.}\" != \".\" ]; then cd ${MAVEN_PROJECT_BASEDIR}; fi\n  mvn -q --batch-mode -nsu -Prelease de.qaware.maven:go-offline-maven-plugin:resolve-dependencies \"$@\"\n)\n"
  },
  {
    "path": "build-bin/maven/maven_opts",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script checks each variable value, so it isn't important to fail on unbound (set -u)\nset -e\n\nmaven_project_basedir=${MAVEN_PROJECT_BASEDIR:-.}\npom=\"${maven_project_basedir}/pom.xml\"\n\n# fail if there's no pom\ntest -f \"${pom}\"\n\narch=$(uname -m)\ncase ${arch} in\n  arm64* )\n    arch=arm64\n    ;;\n  aarch64* )\n    arch=arm64\n    ;;\nesac\n\nmaven_opts=\"${MAVEN_OPTS:-}\"\nif [ ${arch} = \"arm64\" ] && [ -f /etc/alpine-release ]; then\n  # Defensively avoid arm64+alpine problems with posix_spawn\n  maven_opts=\"${maven_opts} -Djdk.lang.Process.launchMechanism=vfork\"\nfi\n\necho ${maven_opts}\n"
  },
  {
    "path": "build-bin/maven/maven_release",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -ue\n\n# This script creates a git `MAJOR.MINOR.PATCH` version tag which later will have `deploy` run against it.\n#\n# In CI..\n#  * trigger pattern: tag =~ /^release-[0-9]+\\.[0-9]+\\.[0-9]+/\n#  * build-bin/git/login_git must be called before invoking this.\n\nexport MAVEN_OPTS=\"$($(dirname \"$0\")/maven_opts)\"\n\ntrigger_tag=${1?trigger_tag is required. Ex release-1.2.3}\nrelease_version=$(build-bin/git/version_from_trigger_tag release- ${trigger_tag})\nrelease_branch=${2:-master}\n\n# Checkout master, as we release from master, not a tag ref\ngit fetch --no-tags --prune --depth=1 origin +refs/heads/${release_branch}:refs/remotes/origin/${release_branch}\ngit checkout ${release_branch}\n\n# Ensure no one pushed commits since this release tag as it would fail later commands\ncommit_local_release_branch=$(git show --pretty='format:%H' ${release_branch})\ncommit_remote_release_branch=$(git show --pretty='format:%H' origin/${release_branch})\nif [ \"$commit_local_release_branch\" != \"$commit_remote_release_branch\" ]; then\n  >&2 echo \"${release_branch} on remote 'origin' has commits since the version to release, aborting\"\n  exit 1\nfi\n\n# Prepare and push release commits and the version tag (N.N.N), which triggers deployment.\n./mvnw --batch-mode -nsu -DreleaseVersion=${release_version} -Denforcer.fail=false -Darguments=\"-DskipTests -Denforcer.fail=false\" release:prepare\n"
  },
  {
    "path": "build-bin/maven/maven_unjar",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script gets one jar from Maven, most typically an exec or module jar, extracting its contents\n# to a directory corresponding to the artifact_id parameter.\n#\n# Ex. ./maven_unjar io.zipkin zipkin-server 2.22.2 slim\n#\n# This searches ${MAVEN_PROJECT_BASEDIR} when set instead of resolving remotely.\n#\n# Note: If you are running this within Docker, ensure you have .dockerignore setup properly to pass\n# the artifact you are looking for.\n#\n# Ex.\n# !./module/target/zipkin-module-*-module.jar\n\nset -eu\n\nexport MAVEN_OPTS=\"$($(dirname \"$0\")/maven_opts)\"\nif [ -x ./mvnw ]; then alias mvn=${PWD}/mvnw; fi\n\ngroup_id=${1?group_id is required}\nartifact_id=${2?artifact_id is required}\nversion=${3?version is required}\nclassifier=${4:-}\n\nif [ -n \"${classifier}\" ]; then\n  qualified_jar=${artifact_id}-${version}-${classifier}.jar\nelse\n  qualified_jar=${artifact_id}-${version}.jar\nfi\n\ncase ${version} in\n  *-SNAPSHOT )\n    is_release=false\n    ;;\n  * )\n    is_release=true\n    ;;\nesac\n\n# Parse MAVEN_OPTS as it may have overridden .m2/repository\nlocal_repo=$(echo ${MAVEN_OPTS} | sed -n 's/.*maven.repo.local=\\([^ ]*\\).*/\\1/p')\nif [ \"${local_repo}\" = \"\" ]; then local_repo=$HOME/.m2/repository; fi\n\nlocal_repo_path=${local_repo}/$(echo ${group_id} | tr '.' '/')/${artifact_id}/${version}/${qualified_jar}\n\nif test -f ${local_repo_path}; then\n  echo \"*** Reusing ${qualified_jar} from Maven local repository...\"\n  cp ${local_repo_path} ${artifact_id}.jar\nelif [ -n \"${MAVEN_PROJECT_BASEDIR:-}\" ]; then\n  echo \"*** Searching for ${qualified_jar} in ${MAVEN_PROJECT_BASEDIR}\"\n  find ${MAVEN_PROJECT_BASEDIR} -name ${qualified_jar} -exec cp {} ${artifact_id}.jar \\;\nfi\n\nif ! test -f ${artifact_id}.jar && [ ${is_release} = \"true\" ]; then\n  mvn_get=\"mvn -q --batch-mode -Denforcer.fail=false \\\n  org.apache.maven.plugins:maven-dependency-plugin:3.8.1:get \\\n  -Dtransitive=false -DgroupId=${group_id} -DartifactId=${artifact_id} -Dversion=${version}\"\n\n  if [ -n \"${classifier}\" ]; then\n    mvn_get=\"${mvn_get} -Dclassifier=${classifier}\"\n  fi\n\n  echo \"*** Resolving ${qualified_jar} from Maven default repositories...\"\n  ${mvn_get} || true\n\n  # Don't add load to Sonatype releases repository except when central sync is delayed\n  if ! test -f ${local_repo_path}; then\n    echo \"*** Resolving ${qualified_jar} from Maven Sonatype releases repository...\"\n    ${mvn_get} -DremoteRepositories=https://oss.sonatype.org/content/repositories/releases\n  fi\n\n  cp ${local_repo_path} ${artifact_id}.jar\nfi\n\nif ! test -f ${artifact_id}.jar; then\n  >&2 echo \"*** Failed to build or get ${qualified_jar}\"\n  exit 1\nfi\n\n(mkdir ${artifact_id} && cd ${artifact_id} && jar -xf ../${artifact_id}.jar) && rm ${artifact_id}.jar\n"
  },
  {
    "path": "build-bin/maven_go_offline",
    "content": "#!/bin/sh -ue\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n\nbuild-bin/maven/maven_go_offline\nexport MAVEN_OPTS=\"$(build-bin/maven/maven_opts)\"\n# Prefetch dependencies used by zipkin-ui (NPM and NodeJS binary and dependencies of our build)\n./mvnw -q --batch-mode -nsu -pl zipkin-lens generate-resources\n"
  },
  {
    "path": "build-bin/maybe_install_npm",
    "content": "#!/bin/sh -ue\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This script hydrates the Maven and NPM cache to make later processes operate with less chance of\n# network problems.\narch=$(uname -m)\ncase ${arch} in\n  arm64* )\n    arch=arm64\n    ;;\n  aarch64* )\n    arch=arm64\n    ;;\nesac\n\n# ARM64 is not supported with musl, yet https://github.com/nodejs/node/blob/master/BUILDING.md\n# Workaround this by installing node and npm directly. See issue #3166\nif [ ${arch} = \"arm64\" ] && [ -f /etc/alpine-release ]; then\n\n  export MAVEN_OPTS=\"$($(dirname \"$0\")/maven/maven_opts)\"\n  if [ -x ./mvnw ]; then alias mvn=${PWD}/mvnw; fi\n  if [ \"${MAVEN_PROJECT_BASEDIR:-.}\" != \".\" ]; then cd ${MAVEN_PROJECT_BASEDIR}; fi\n\n  # Get the version of nodejs the build uses. Note: this takes time as it downloads Maven plugins.\n  node_version=$(mvn help:evaluate -Dexpression=node.version -DskipTests -q -DforceStdout -pl zipkin-lens)\n\n  set -x\n  # Repos for https://pkgs.alpinelinux.org/packages?name=nodejs are already in the base image.\n  apk add --update --no-cache nodejs=~${node_version} npm\nfi\n"
  },
  {
    "path": "build-bin/mlc_config.json",
    "content": "{\n  \"ignorePatterns\": [\n    {\n      \"pattern\": \"https://chromewebstore.google.com/detail/*\"\n    },\n    {\n      \"pattern\": \"https://stackoverflow.com/questions/tagged/spring-boot\"\n    },\n    {\n      \"pattern\": \"https://oss.sonatype.org/content/repositories/snapshots\"\n    },\n    {\n      \"pattern\": \"http://localhost:9411/api/v[12]/spans\"\n    },\n    {\n      \"pattern\": \"http://localhost:9411/zipkin\"\n    },\n    {\n      \"pattern\": \"http://localhost/admin/zipkin/\"\n    },\n    {\n      \"pattern\": \"http://zipkin:9411\"\n    },\n    {\n      \"pattern\": \"http://localhost:9411/zipkin?serviceName=backend\"\n    },\n    {\n      \"pattern\": \"http://localhost:8081\"\n    },\n    {\n      \"pattern\": \"http://localhost:9000/api\"\n    },\n    {\n      \"pattern\": \"http://localhost:3000\"\n    }\n  ]\n}\n"
  },
  {
    "path": "build-bin/test",
    "content": "#!/bin/sh -ue\n\n# This script runs the tests of the project.\n#\n# See [README.md] for an explanation of this and how CI should use it.\n\n# -DskipActuator ensures no tests rely on the actuator library\n./mvnw -T1C verify -nsu -DskipActuator \"$@\"\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime base layers of zipkin and zipkin-slim.\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/start-zipkin /docker-bin/\nCOPY . /code/\n\n# This version is only used during the install process. Try to be consistent as it reduces layers,\n# which reduces downloads.\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /code\n# Conditions aren't supported in Dockerfile instructions, so we copy source even if it isn't used.\nCOPY --from=scratch /code/ .\n\nWORKDIR /install\n\n# When true, build-bin/maven/unjar searches /code for the artifact instead of resolving remotely.\n# /code contains what is allowed in .dockerignore. On problem, ensure .dockerignore is correct.\nARG release_from_maven_build=false\nENV RELEASE_FROM_MAVEN_BUILD=$release_from_maven_build\n# Version of the artifact to unjar. Ex. \"2.4.5\" or \"2.4.5-SNAPSHOT\" \"master\" to use the pom version.\nARG version=master\nENV VERSION=$version\nENV MAVEN_PROJECT_BASEDIR=/code\nARG TARGETARCH\nRUN test \"${TARGETARCH}\" != \"\" && \\\n    if [ \"${RELEASE_FROM_MAVEN_BUILD}\" == \"false\" ]; then /code/build-bin/maybe_install_npm; fi; \\\n    /code/build-bin/maven/maven_build_or_unjar io.zipkin zipkin-server ${VERSION} exec && \\\n    mv zipkin-server zipkin && \\\n    /code/build-bin/maven/maven_build_or_unjar io.zipkin zipkin-server ${VERSION} slim && \\\n    mv zipkin-server zipkin-slim && \\\n    # Copy tcnative deps to slim: 1.1MB more libs when pared to current platform.\n    cp zipkin/BOOT-INF/lib/*tcnative*.jar zipkin-slim/BOOT-INF/lib/ && \\\n    # Remove any unused platform-specific jars. This results in none for s390x or non-linux.\n    rm */BOOT-INF/lib/*windows* */BOOT-INF/lib/*osx* && \\\n    if [ \"${TARGETARCH}\" != \"amd64\" ]; then rm */BOOT-INF/lib/*x86*; fi && \\\n    if [ \"${TARGETARCH}\" != \"arm64\" ]; then rm */BOOT-INF/lib/*a64* */BOOT-INF/lib/*aarch*; fi\n\n# Almost everything is common between the slim and normal build\nFROM ghcr.io/openzipkin/java:${java_version}-jre as base-server\n\n# All content including binaries and logs write under WORKDIR\nARG USER=zipkin\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts.\n#\n# If in production, you have a 30s startup, please report to https://gitter.im/openzipkin/zipkin\n# including the values of the /health and /info endpoints as this would be unexpected.\nHEALTHCHECK --interval=5s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\n\nENTRYPOINT [\"start-zipkin\"]\n\n# Switch to the runtime user\nUSER ${USER}\n\nFROM base-server as zipkin-slim\nLABEL org.opencontainers.image.description=\"Zipkin slim distribution on OpenJDK and Alpine Linux\"\n\nCOPY --from=install --chown=${USER} /install/zipkin-slim/ /zipkin/\n\nEXPOSE 9411\n\nFROM base-server as zipkin\nLABEL org.opencontainers.image.description=\"Zipkin full distribution on OpenJDK and Alpine Linux\"\n\n# 3rd party modules like zipkin-aws will apply profile settings with this\nENV MODULE_OPTS=\n\nCOPY --from=install --chown=${USER} /install/zipkin/ /zipkin/\n\n# Zipkin's full distribution includes Scribe support (albeit disabled)\nEXPOSE 9410 9411\n"
  },
  {
    "path": "docker/RATIONALE.md",
    "content": "# zipkin-docker rationale\n\n## Why do we add HEALTHCHECK?\nWhile most of our images are not production, we still add HEALTHCHECK for a better ad-hoc\nand automation experience. Many of our setups will not operate without a service\ndependency: having HEALTHCHECK present makes triage and scripting a bit easier.\n\nHEALTHCHECK on our test image serves primarily three purposes:\n * ad-hoc or scripted status of health using docker ps instead of knowing Kafka commands\n * to allow manual usage of the docker compose v2 service_healthy condition\n * support Docker Hub automated test service\n\nEx. The following command can be used ad-hoc or in scripts without the user knowing Kafka:\n```bash\n$ docker inspect --format='{{json .State.Health.Status}}' kafka-zookeeper\n\"unhealthy\"\n```\n\n### Why do we change default timeouts?\nWe changed timeouts in order to mark success faster.\n\nBy default, the Docker health check runs after 30s, and if a failure occurs,\nit waits 30s to try again. This implies a minimum of 30s before the server is\nmarked healthy.\n\nhttps://docs.docker.com/engine/reference/builder/#healthcheck\n\nWe expect the server startup to take less than 10 seconds, even in a fresh\nstart. Some health checks will trigger a slow \"first request\" due to schema\nsetup (ex this is the case in Elasticsearch and Cassandra). However, we don't\nwant to force an initial delay of 30s as defaults would.\n\nInstead, we lower the interval and timeout from 30s to 5s. If a server starts\nin 7s and takes another 2s to install schema, it can still pass in 10s vs 30s.\n\nWe retain the 30s even if it would be an excessively long startup. This is to\naccommodate test containers, which can boot slower than production sites, and\nany knock-on effects of that, like slow dependent storage containers which are\nsimultaneously bootstrapping.\n\n### Why default timeout to 5s for Kafka HEALTHCHECK?\n\nDocker `HEALTHCHECK` marks a container unhealthy on the first failure that occurs past its start\nperiod. It is important to avoid false negatives that are host contention in nature, as they can\nbreak orchestration such as docker-compose.\n\nCommands like `nc` will almost never timeout launching due to contention, even if they might timeout\non network connections themselves for the same reason.\n\n`kafka-topics.sh` uses Java and a relatively large classpath. It can be slow when many other\nprocesses are starting at the same time. For example, when launching many containers in Docker\nCompose, `kafka-topics.sh` timed out after 2s even though the prior run succeeded. This broke a condition\nwhich broke the rest of the automation. A 5s timeout is excessive usually, but avoided this problem.\n"
  },
  {
    "path": "docker/README.md",
    "content": "## zipkin Docker images\nThis directory contains assets used to build and release Zipkin's Docker images.\n\n## Production images\nThe only Zipkin production images built here:\n* openzipkin/zipkin: The core server image that hosts the Zipkin UI, Api and Collector features.\n  * Mirrored as ghcr.io/openzipkin/zipkin\n* openzipkin/zipkin-slim: The stripped server image that hosts the Zipkin UI and Api features, but only supports in-memory or Elasticsearch storage with HTTP or gRPC span collectors.\n  * Mirrored as ghcr.io/openzipkin/zipkin-slim\n\n## Testing images\n\nWe also provide a number images that are not for production, rather to simplify demos and\nintegration tests. We designed these to be small and start easily. We did this by re-using the same\nbase layer `openzipkin/zipkin`, and setting up schema where relevant.\n\n* [ghcr.io/openzipkin/zipkin-activemq](test-images/zipkin-activemq/README.md) - runs ActiveMQ Classic\n* [ghcr.io/openzipkin/zipkin-cassandra](test-images/zipkin-cassandra/README.md) - runs Cassandra initialized with Zipkin's schema\n* [ghcr.io/openzipkin/zipkin-elasticsearch7](test-images/zipkin-elasticsearch7/README.md) - runs Elasticsearch 7.x\n* [ghcr.io/openzipkin/zipkin-elasticsearch8](test-images/zipkin-elasticsearch8/README.md) - runs Elasticsearch 8.x\n* [ghcr.io/openzipkin/zipkin-opensearch2](test-images/zipkin-opensearch2/README.md) - runs OpenSearch 2.x\n* [ghcr.io/openzipkin/zipkin-eureka](test-images/zipkin-eureka/README.md) - runs Eureka\n* [ghcr.io/openzipkin/zipkin-kafka](test-images/zipkin-kafka/README.md) - runs both Kafka+ZooKeeper\n* [ghcr.io/openzipkin/zipkin-mysql](test-images/zipkin-mysql/README.md) - runs MySQL initialized with Zipkin's schema\n* [ghcr.io/openzipkin/zipkin-pulsar](test-images/zipkin-pulsar/README.md) - runs Pulsar\n* [ghcr.io/openzipkin/zipkin-rabbitmq](test-images/zipkin-rabbitmq/README.md) - runs RabbitMQ\n* [ghcr.io/openzipkin/zipkin-ui](test-images/zipkin-ui/README.md) - serves the (Lens) UI directly with NGINX\n\n## Getting started\n\nZipkin has no dependencies, for example you can run an in-memory zipkin server like so:\n\n```bash\n# Note: this is mirrored as ghcr.io/openzipkin/zipkin-slim\n$ docker run -d -p 9411:9411 openzipkin/zipkin-slim\n```\n\nSee the ui at (docker ip):9411\n\nIn the UI - click zipkin-server, then click \"Find Traces\".\n\nWe also provide [example compose files](examples/README.md) that integrate collectors and storage,\nsuch as Kafka or Elasticsearch.\n\n## Configuration\nConfiguration is via environment variables, defined by [zipkin-server](../zipkin-server/README.md). Notably, you'll want to look at the `STORAGE_TYPE` environment variables, which\ninclude \"cassandra\", \"mysql\" and \"elasticsearch\".\n\nNote: the `openzipkin/zipkin-slim` image only supports \"elasticsearch\" storage. To use other storage types, you must use the main image `openzipkin/zipkin`.\n\nWhen in Docker, the following environment variables also apply\n\n* `JAVA_OPTS`: Use to set java arguments, such as heap size or trust store location.\n  * By default, `openzipkin/zipkin` sets max heap to 64m while `openzipkin/zipkin-slim` 32m\n* `STORAGE_PORT_9042_TCP_ADDR` -- A Cassandra node listening on port 9042. This\n  environment variable is typically set by linking a container running\n  `zipkin-cassandra` as \"storage\" when you start the container.\n* `STORAGE_PORT_3306_TCP_ADDR` -- A MySQL node listening on port 3306. This\n  environment variable is typically set by linking a container running\n  `zipkin-mysql` as \"storage\" when you start the container.\n* `STORAGE_PORT_9200_TCP_ADDR` -- An Elasticsearch node listening on port 9200. This\n  environment variable is typically set by linking a container running\n  `zipkin-elasticsearch` as \"storage\" when you start the container. This is ignored\n  when `ES_HOSTS` or `ES_AWS_DOMAIN` are set.\n* `KAFKA_PORT_2181_TCP_ADDR` -- A zookeeper node listening on port 2181. This\n  environment variable is typically set by linking a container running\n  `zipkin-kafka` as \"kafka\" when you start the container.\n\nFor example, to increase heap size, set `JAVA_OPTS` as shown in our [docker-compose](examples/docker-compose.yml) file:\n```yaml\n    environment:\n      - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError\n```\n\nFor example, to add debug logging, set `command` as shown in our [docker-compose](examples/docker-compose.yml) file:\n```yaml\n    command: --logging.level.zipkin2=DEBUG\n```\n\n## Runtime user\nThe `openzipkin/zipkin` and `openzipkin/zipkin-slim` images run under a nologin\nuser named 'zipkin' with a home directory of '/zipkin'. As this is Alpine Linux\nimage, you won't find many utilities installed, but you can browse contents\nwith a shell like below:\n\n```bash\n$ docker run -it --rm --entrypoint /bin/sh openzipkin/zipkin\n/zipkin $ ls\nBOOT-INF  META-INF  org       run.sh\n```\n\n## Notes\n\n### Container links\nIf using Docker's deprecated container links, you need to set env variables\naccordingly.\n\nEx. If your link name is \"storage\" for an Elasticsearch container:\n```\n  ES_HOSTS=http://$STORAGE_PORT_9200_TCP_ADDR:9200\n```\n\nThe above is mentioned only for historical reasons. The OpenZipkin community\ndo not support Docker's deprecated container links.\n\n### MySQL\nIf using an external MySQL server or image, ensure schema and other parameters match the [docs](../zipkin-storage/mysql-v1/README.md#applying-the-schema).\n\n## Building images\n\nTo build `openzipkin/zipkin:test`, from the top-level of the repository, run:\n```bash\n$ build-bin/docker/docker_build openzipkin/zipkin:test\n```\n\nIf you want the slim distribution (openzipkin/zipkin-slim:test), run:\n```bash\n$ DOCKER_TARGET=zipkin-slim build-bin/docker/docker_build openzipkin/zipkin-slim:test\n```\n"
  },
  {
    "path": "docker/examples/.dockerignore",
    "content": "# https://docs.docker.com/engine/reference/builder/#dockerignore-file\n**/.*\n\n!./prometheus/create-datasource-and-dashboard.sh\n!./prometheus/prometheus.yml\n"
  },
  {
    "path": "docker/examples/README.md",
    "content": "# Zipkin Docker Examples\n\nThis project is configured to run docker containers using\n[docker-compose](https://docs.docker.com/compose/). Note that the default\nconfiguration requires docker compose 1.6.0+ and docker-engine 1.10.0+.\n\nTo start the default docker compose configuration, run:\n\n```bash\n# To use the last released version of zipkin\n$ docker compose up\n# To use the last built version of zipkin\n$ TAG=master docker compose up\n```\n\nView the web UI at $(docker ip):9411. Traces are stored in memory.\n\nTo see specific traces in the UI, select \"zipkin-server\" in the dropdown and\nthen click the \"Find Traces\" button.\n\n## ActiveMQ\n\nYou can collect traces from [ActiveMQ](../test-images/zipkin-activemq/README.md) in addition to HTTP, using the\n`docker-compose-activemq.yml` file. This configuration starts `zipkin` and `zipkin-activemq` in their\nown containers.\n\nTo add ActiveMQ configuration, run:\n```bash\n$ docker compose -f docker-compose-activemq.yml up\n```\n\nThen configure the [ActiveMQ sender](https://github.com/openzipkin/zipkin-reporter-java/blob/master/activemq-client/src/main/java/zipkin2/reporter/activemq/ActiveMQSender.java)\nusing a `brokerUrl` value of `failover:tcp://localhost:61616` or a non-local hostname if in docker.\n\n## Cassandra\n\nYou can store traces in [Cassandra](../test-images/zipkin-cassandra/README.md) instead of memory, using the\n`docker-compose-cassandra.yml` file. This configuration starts `zipkin`, `zipkin-cassandra` and\n`zipkin-dependencies` (cron job) in their own containers.\n\nTo start the Cassandra-backed configuration, run:\n\n```bash\n$ docker compose -f docker-compose-cassandra.yml up\n```\n\nThe `zipkin-dependencies` container is a scheduled task that runs every hour.\nIf you want to see the dependency graph before then, you can run it manually\nin another terminal like so:\n\n```bash\n$ docker compose -f docker-compose-cassandra.yml run --rm --no-deps --entrypoint start-zipkin-dependencies dependencies\n```\n\n## Elasticsearch\n\nYou can store traces in [Elasticsearch](../test-images/zipkin-elasticsearch8/README.md) instead of memory,\nusing the `docker-compose-elasticsearch.yml` file. This configuration starts `zipkin`,\n`zipkin-elasticsearch` and `zipkin-dependencies` (cron job) in their own containers.\n\nTo start the Elasticsearch-backed configuration, run:\n\n```bash\n$ docker compose -f docker-compose-elasticsearch.yml up\n```\n\nThe `zipkin-dependencies` container is a scheduled task that runs every hour.\nIf you want to see the dependency graph before then, you can run it manually\nin another terminal like so:\n\n```bash\n$ docker compose -f docker-compose-elasticsearch.yml run --rm --no-deps --entrypoint start-zipkin-dependencies dependencies\n```\n\n## Kafka\n\nYou can collect traces from [Kafka](../test-images/zipkin-kafka/README.md) in addition to HTTP, using the\n`docker-compose-kafka.yml` file. This configuration starts `zipkin` and `zipkin-kafka` in their\nown containers.\n\nTo add Kafka configuration, run:\n```bash\n$ docker compose -f docker-compose-kafka.yml up\n```\n\nThen configure the [Kafka sender](https://github.com/openzipkin/zipkin-reporter-java/blob/master/kafka/src/main/java/zipkin2/reporter/kafka/KafkaSender.java) using a `bootstrapServers` value of `host.docker.internal:9092` if your application is inside the same docker network or `localhost:19092` if not, but running on the same host.\n\nIn other words, if you are running a sample application on your laptop, you would use `localhost:19092` bootstrap server to send spans to the Kafka broker running in Docker.\n\n### Docker machine and Kafka\n\nIf you are using Docker machine, adjust `KAFKA_ADVERTISED_HOST_NAME` in `docker-compose-kafka.yml`\nand the `bootstrapServers` configuration of the kafka sender to match your Docker host IP (ex. 192.168.99.100:19092).\n\n## MySQL\n\nYou can store traces in [MySQL](../test-images/zipkin-mysql/README.md) instead of memory, using the\n`docker-compose-mysql.yml` file. This configuration starts `zipkin`, `zipkin-mysql` and\n`zipkin-dependencies` (cron job) in their own containers.\n\nTo start the MySQL-backed configuration, run:\n\n```bash\n$ docker compose -f docker-compose-mysql.yml up\n```\n\n## RabbitMQ\n\nYou can collect traces from [RabbitMQ](../test-images/zipkin-rabbitmq/README.md) in addition to HTTP, using the\n`docker-compose-rabbitmq.yml` file. This configuration starts `zipkin` and `zipkin-rabbitmq` in their\nown containers.\n\nTo add RabbitMQ configuration, run:\n```bash\n$ docker compose -f docker-compose-rabbitmq.yml up\n```\n\nThen configure the [RabbitMQ sender](https://github.com/openzipkin/zipkin-reporter-java/blob/master/amqp-client/src/main/java/zipkin2/reporter/amqp/RabbitMQSender.java)\nusing a `host` value of `localhost` or a non-local hostname if in docker.\n\n\n## Pulsar\n\nYou can collect traces from [Pulsar](../test-images/zipkin-pulsar/README.md) in addition to HTTP, using the\n`docker-compose-pulsar.yml` file. This configuration starts `zipkin` and `zipkin-pulsar` in their\nown containers.\n\nTo add Pulsar configuration, run:\n```bash\n$ docker compose -f docker-compose-pulsar.yml up\n```\n\n## Eureka\n\nYou can register Zipkin for service discovery in [Eureka](../test-images/zipkin-eureka/README.md)\nusing the `docker-compose-eureka.yml` file. This configuration starts `zipkin` and `zipkin-eureka`\nin their own containers.\n\nWhen `zipkin` starts, it registers its endpoint into `eureka`. Then, the two [example services](#example)\ndiscover zipkin's endpoint from `eureka` and use it to send spans.\n\nTo try this out, run:\n```bash\n$ docker compose -f docker-compose.yml -f docker-compose-eureka.yml up\n```\n\n## Example\n\nThe docker compose configuration can be extended to host an [example application](https://github.com/openzipkin/brave-example)\nusing the `docker-compose-example.yml` file. That file employs [docker compose overrides](https://docs.docker.com/compose/extends/#multiple-compose-files)\nto add a \"frontend\" and \"backend\" service.\n\nTo add the example configuration, run:\n```bash\n$ docker compose -f docker-compose.yml -f docker-compose-example.yml up\n```\n\nOnce the services start, open http://localhost:8081/\n* This calls the backend (http://localhost:9000/api) and shows its result: a formatted date.\n\nAfterward, you can view traces that went through the backend via http://localhost:9411/zipkin?serviceName=backend\n\n## UI\n\nThe docker compose configuration can be extended to [host the UI](../test-images/zipkin-ui/README.md) on port 80\nusing the `docker-compose-ui.yml` file. That file employs\n[docker compose overrides](https://docs.docker.com/compose/extends/#multiple-compose-files)\nto add an NGINX container and relevant settings.\n\nTo start the NGINX configuration, run:\n\n```bash\n$ docker compose -f docker-compose.yml -f docker-compose-ui.yml up\n```\n\nThis container doubles as a skeleton for creating proxy configuration around\nZipkin like authentication, dealing with CORS with zipkin-js apps, or\nterminating SSL.\n\nIf you want to run the zipkin-ui standalone against a remote zipkin server, you\nneed to set `ZIPKIN_BASE_URL` accordingly:\n\n```bash\n$ docker run -d -p 80:80 \\\n  -e ZIPKIN_BASE_URL=http://myfavoritezipkin:9411 \\\n  openzipkin/zipkin-ui\n```\n\n## UI Proxy\n\nThe docker compose configuration can be extended to [proxy the UI](../test-images/zipkin-uiproxy/README.md) on port 80\nusing the `docker-compose-uiproxy.yml` file. That file employs\n[docker compose overrides](https://docs.docker.com/compose/extends/#multiple-compose-files) to add an NGINX container and relevant settings.\n\nTo start the NGINX configuration, run:\n\n```bash\n$ docker compose -f docker-compose.yml -f docker-compose-uiproxy.yml up\n```\n\nThis container helps verify the `ZIPKIN_UI_BASEPATH` variable by setting it to\n\"/admin/zipkin\". This means when the compose configuration is up, you can\naccess Zipkin UI at http://localhost/admin/zipkin/\n\n## Prometheus\n\nZipkin comes with a built-in Prometheus metric exporter. The\n`docker-compose-prometheus.yml` file starts Prometheus configured to scrape\nZipkin, exposes it on port `9090`. You can open `$DOCKER_HOST_IP:9090` and\nstart exploring metrics (available on the `/prometheus` endpoint of Zipkin).\n\n`docker-compose-prometheus.yml` also starts a Grafana with authentication\ndisabled, exposing it on port 3000. On startup it's configured with the\nPrometheus instance started by `docker-compose` as a data source, and imports\nthe dashboard published at https://grafana.com/dashboards/1598. This means that,\nafter running `docker-compose  ... -f docker-compose-prometheus.yml up`, you\ncan open `$DOCKER_IP:3000/dashboard/db/zipkin-prometheus` and play around with\nthe dashboard.\n"
  },
  {
    "path": "docker/examples/docker-compose-activemq.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to add a test\n# activemq server, which is used as a span transport.\n\nversion: '2.4'\n\nservices:\n  activemq:\n    image: ghcr.io/openzipkin/zipkin-activemq:${TAG:-latest}\n    container_name: activemq\n    ports:  # expose the ActiveMQ port so apps can publish spans.\n      - \"61616:61616\"\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include Activemq support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - ACTIVEMQ_URL=failover:tcp://activemq:61616\n    depends_on:\n      activemq:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-cassandra.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to run the\n# zipkin-cassandra container instead of the zipkin-mysql container.\n\nversion: '2.4'\n\nservices:\n  storage:\n    image: ghcr.io/openzipkin/zipkin-cassandra:${TAG:-latest}\n    # Uncomment to use DSE instead (minimum version 5.1)\n    # image: datastax/dse-server:5.1.20\n    # environment:\n    #  - DS_LICENSE=accept\n    container_name: cassandra\n    # Uncomment to expose the storage port for testing\n    # ports:\n    #   - 9042:9042\n\n  # Use Cassandra instead of in-memory storage\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include Cassandra support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - STORAGE_TYPE=cassandra3\n      # When using the test docker image, or have schema pre-installed, you don't need to ensure it\n      - CASSANDRA_ENSURE_SCHEMA=false\n      # When overriding this value, note the minimum supported version is 3.11.3\n      - CASSANDRA_CONTACT_POINTS=cassandra\n      # Uncomment to configure authentication\n      # - CASSANDRA_USERNAME=cassandra\n      # - CASSANDRA_PASSWORD=cassandra\n    # Uncomment to enable request logging (TRACE shows query values)\n    # command: --logging.level.com.datastax.oss.driver.internal.core.tracker.RequestLogger=TRACE\n    depends_on:\n      storage:\n        condition: service_healthy\n\n  dependencies:\n    extends:\n      file: docker-compose-dependencies.yml\n      service: dependencies\n    environment:\n      - STORAGE_TYPE=${STORAGE_TYPE:-cassandra3}\n      - CASSANDRA_CONTACT_POINTS=cassandra\n    depends_on:\n      storage:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-dependencies.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nversion: '2.4'\n\nservices:\n  # Adds a cron to process spans since midnight every hour, and all spans each day\n  # This data is served by http://192.168.99.100:8080/dependency\n  #\n  # For more details, see https://github.com/openzipkin/docker-zipkin-dependencies\n  dependencies:\n    image: ghcr.io/openzipkin/zipkin-dependencies\n    container_name: dependencies\n    user: root\n    entrypoint: /usr/sbin/crond -f\n    # environment:\n      # Uncomment to see dependency processing logs\n      # - ZIPKIN_LOG_LEVEL=DEBUG\n      # Uncomment to adjust memory used by the dependencies job\n      # - JAVA_OPTS=-verbose:gc -Xms1G -Xmx1G\n"
  },
  {
    "path": "docker/examples/docker-compose-elasticsearch.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to run the\n# zipkin-elasticsearch container instead of the zipkin-mysql container.\n\nversion: '2.4'\n\nservices:\n  storage:\n    image: ghcr.io/openzipkin/zipkin-elasticsearch8:${TAG:-latest}\n    container_name: elasticsearch\n    # Uncomment to expose the storage port for testing\n    # ports:\n    #   - 9200:9200\n\n  # Use Elasticsearch instead of in-memory storage\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    environment:\n      - STORAGE_TYPE=elasticsearch\n      # Point the zipkin at the storage backend\n      - ES_HOSTS=elasticsearch:9200\n      # Uncomment to see requests to and from elasticsearch\n      # - ES_HTTP_LOGGING=BODY\n    depends_on:\n      storage:\n        condition: service_healthy\n\n  dependencies:\n    extends:\n      file: docker-compose-dependencies.yml\n      service: dependencies\n    environment:\n      - STORAGE_TYPE=elasticsearch\n      - ES_HOSTS=elasticsearch\n    depends_on:\n      storage:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-eureka.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to register\n# zipkin into a test eureka server.\n\nversion: '2.4'\n\nservices:\n  eureka:\n    image: ghcr.io/openzipkin/zipkin-eureka:${TAG:-latest}\n    container_name: eureka\n# Uncomment to require authentication\n#    environment:\n#      - EUREKA_USERNAME=username\n#      - EUREKA_PASSWORD=password\n# Uncomment to expose the eureka port for testing\n#    ports:\n#      - 8761:8761\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    environment:\n      - EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2\n      # Uncomment to authenticate eureka\n      #- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2\n      - EUREKA_HOSTNAME=zipkin\n    depends_on:\n      eureka:\n        condition: service_healthy\n\n  # Generate traffic by hitting http://localhost:8081\n  frontend:\n    image: ghcr.io/openzipkin/brave-example:armeria\n    container_name: frontend\n    entrypoint: start-frontend\n    environment:\n      - EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2\n      # Uncomment to authenticate eureka\n      #- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2\n    ports:\n      - 8081:8081\n    depends_on:\n      backend:\n        condition: service_healthy\n      zipkin:\n        condition: service_healthy\n\n  # Serves the /api endpoint the frontend uses\n  backend:\n    image: ghcr.io/openzipkin/brave-example:armeria\n    container_name: backend\n    entrypoint: start-backend\n    environment:\n      - EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2\n      # Uncomment to authenticate eureka\n      #- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2\n    depends_on:\n      zipkin:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-example.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Format version 2.1 was introduced with Docker Compose v1.9\n# We need Docker Compose v1.9+ for unset variable interpolation\nversion: \"2.1\"\n\nservices:\n  # Generate traffic by hitting http://localhost:8081\n  frontend:\n    container_name: frontend\n    image: ghcr.io/openzipkin/brave-example:${PROJECT:-armeria}\n    entrypoint: start-frontend\n    ports:\n      - 8081:8081\n    depends_on:\n      backend:\n        condition: service_healthy\n      zipkin:\n        condition: service_started\n  # Serves the /api endpoint the frontend uses\n  backend:\n    container_name: backend\n    image: ghcr.io/openzipkin/brave-example:${PROJECT:-armeria}\n    entrypoint: start-backend\n    depends_on:\n      zipkin:\n        condition: service_started\n"
  },
  {
    "path": "docker/examples/docker-compose-kafka.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to add a test\n# kafka server, which is used as a span transport.\n\nversion: '2.4'\n\nservices:\n  kafka:\n    image: ghcr.io/openzipkin/zipkin-kafka:${TAG:-latest}\n    container_name: kafka\n    # If using docker machine, uncomment the below and set your bootstrap\n    # server list to 192.168.99.100:19092\n    # environment:\n      # - KAFKA_ADVERTISED_HOST_NAME=192.168.99.100\n    ports:\n      # Processes on the Docker host can set bootstrap server list to localhost:19092\n      - 19092:19092\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include Kafka support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - KAFKA_BOOTSTRAP_SERVERS=kafka:9092\n    depends_on:\n      kafka:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-mysql.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# This runs the zipkin and zipkin-mysql containers, using docker-compose's\n# default networking to wire the containers together.\n#\n# Note that this file is meant for learning Zipkin, not production deployments.\n\nversion: '2.4'\n\nservices:\n  storage:\n    image: ghcr.io/openzipkin/zipkin-mysql:${TAG:-latest}\n    container_name: mysql\n    # Uncomment to expose the storage port for testing\n    # ports:\n    #   - 3306:3306\n\n  # Use MySQL instead of in-memory storage\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include MySQL support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - STORAGE_TYPE=mysql\n      - MYSQL_HOST=storage\n      # Add the baked-in username and password for the zipkin-mysql image\n      - MYSQL_USER=zipkin\n      - MYSQL_PASS=zipkin\n    depends_on:\n      storage:\n        condition: service_healthy\n\n  dependencies:\n    extends:\n      file: docker-compose-dependencies.yml\n      service: dependencies\n    environment:\n      - STORAGE_TYPE=mysql\n      - MYSQL_HOST=storage\n      # Add the baked-in username and password for the zipkin-mysql image\n      - MYSQL_USER=zipkin\n      - MYSQL_PASS=zipkin\n    depends_on:\n      storage:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-prometheus.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# This runs containers that collect data for our Grafana dashboard\n#\n# Note that this file is meant for learning Zipkin, not production deployments.\n\nversion: '2.4'\n\nservices:\n  prometheus:\n    # Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    # Use latest from https://quay.io/repository/prometheus/prometheus?tab=tags\n    image: quay.io/prometheus/prometheus:v2.55.1\n    container_name: prometheus\n    ports:\n      - 9090:9090\n    depends_on:\n      zipkin:\n        condition: service_healthy\n    volumes:\n      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml\n\n  grafana:\n    # Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    # Use latest from https://quay.io/repository/giantswarm/grafana?tab=tags\n    image: quay.io/giantswarm/grafana:7.5.12\n    container_name: grafana\n    ports:\n      - 3000:3000\n    depends_on:\n      - prometheus\n    environment:\n      - GF_AUTH_ANONYMOUS_ENABLED=true\n      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin\n\n  setup_grafana_datasource:\n    # This is an arbitrary small image that has curl installed\n    # Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas\n    # Use latest from https://quay.io/repository/cilium/alpine-curl?tab=tags\n    image: quay.io/cilium/alpine-curl:v1.10.0\n    container_name: setup_grafana_datasource\n    depends_on:\n      - grafana\n    volumes:\n      - ./prometheus/create-datasource-and-dashboard.sh:/tmp/create.sh:ro\n    working_dir: /tmp\n    entrypoint: /tmp/create.sh\n"
  },
  {
    "path": "docker/examples/docker-compose-pulsar.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to add a test\n# pulsar server, which is used as a span transport.\n\nversion: '2.4'\n\nservices:\n  pulsar:\n    image: ghcr.io/openzipkin/zipkin-pulsar:${TAG:-latest}\n    container_name: pulsar\n    ports: # expose the pulsar port so apps can publish spans.\n      - \"6650:6650\"\n      # - \"8080:8080\" # uncomment to expose the pulsar http port.\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include Pulsar support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - PULSAR_SERVICE_URL=pulsar://pulsar:6650\n    depends_on:\n      pulsar:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-rabbitmq.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml to add a test\n# rabbitmq server, which is used as a span transport.\n\nversion: '2.4'\n\nservices:\n  rabbitmq:\n    image: ghcr.io/openzipkin/zipkin-rabbitmq:${TAG:-latest}\n    container_name: rabbitmq\n    ports:  # expose the rabbitmq port so apps can publish spans.\n      - \"5672:5672\"\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    # slim doesn't include RabbitMQ support, so switch to the larger image\n    image: ghcr.io/openzipkin/zipkin:${TAG:-latest}\n    environment:\n      - RABBIT_ADDRESSES=rabbitmq:5672\n    depends_on:\n      rabbitmq:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-ui.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml, hosting the\n# ui on port 80 using NGINX\n\nversion: '2.4'\n\nservices:\n  zipkin-ui:\n    image: ghcr.io/openzipkin/zipkin-ui:${TAG:-latest}\n    container_name: zipkin-ui\n    environment:\n      # Change this if connecting to a different zipkin server\n      - ZIPKIN_BASE_URL=http://zipkin:9411\n    ports:\n      - 80:80\n    depends_on:\n      zipkin:\n        condition: service_healthy\n"
  },
  {
    "path": "docker/examples/docker-compose-uiproxy.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# It extends the default configuration from docker-compose.yml, hosting the\n# ui on port 80 using NGINX\n\nversion: '2.4'\n\nservices:\n  zipkin-uiproxy:\n    image: ghcr.io/openzipkin/zipkin-uiproxy:${TAG:-latest}\n    container_name: zipkin-uiproxy\n    environment:\n      # This allows hitting the UI on the host by http://localhost/admin/zipkin\n      - ZIPKIN_UI_BASEPATH=/admin/zipkin\n    ports:\n      - 80:80\n    depends_on:\n      zipkin:\n        condition: service_healthy\n\n  zipkin:\n    extends:\n      file: docker-compose.yml\n      service: zipkin\n    environment:\n      # This must match what's set in zipkin-uiproxy\n      - ZIPKIN_UI_BASEPATH=/admin/zipkin\n"
  },
  {
    "path": "docker/examples/docker-compose.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# This file uses the version 2 docker compose file format, described here:\n# https://docs.docker.com/compose/compose-file/#version-2\n#\n# This runs the zipkin slim container, using docker-compose's default networking\n# to wire other containers together.\n#\n# Note that this file is meant for learning Zipkin, not production deployments.\n\nversion: '2.4'\n\nservices:\n  # The zipkin process services the UI, and also exposes a POST endpoint that\n  # instrumentation can send trace data to.\n  zipkin:\n    image: ghcr.io/openzipkin/zipkin-slim:${TAG:-latest}\n    container_name: zipkin\n    # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables\n    environment:\n      - STORAGE_TYPE=mem\n      # Uncomment to enable self-tracing\n      # - SELF_TRACING_ENABLED=true\n      # Uncomment to increase heap size\n      # - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError\n    ports:\n      # Port used for the Zipkin UI and HTTP Api\n      - 9411:9411\n    # Uncomment to enable debug logging\n    # command: --logging.level.zipkin2=DEBUG\n\n"
  },
  {
    "path": "docker/examples/prometheus/create-datasource-and-dashboard.sh",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -xeuo pipefail\n\nif ! curl --retry 5 --retry-connrefused --retry-delay 0 -sf http://grafana:3000/api/datasources/name/prom; then\n    curl -sf -X POST -H \"Content-Type: application/json\" \\\n         --data-binary '{\"name\":\"prom\",\"type\":\"prometheus\",\"url\":\"http://prometheus:9090\",\"access\":\"proxy\",\"isDefault\":true}' \\\n         http://grafana:3000/api/datasources\nfi\n\ndashboard_id=1598\nlast_revision=$(curl -sf https://grafana.com/api/dashboards/${dashboard_id}/revisions | grep '\"revision\":' | sed 's/ *\"revision\": \\([0-9]*\\),/\\1/' | sort -n | tail -1)\n\necho '{\"dashboard\": ' > data.json\ncurl -s https://grafana.com/api/dashboards/${dashboard_id}/revisions/${last_revision}/download >> data.json\necho ', \"inputs\": [{\"name\": \"DS_PROMETHEUS\", \"pluginId\": \"prometheus\", \"type\": \"datasource\", \"value\": \"prom\"}], \"overwrite\": false}' >> data.json\ncurl --retry-connrefused --retry 5 --retry-delay 0 -sf \\\n     -X POST -H \"Content-Type: application/json\" \\\n     --data-binary @data.json \\\n     http://grafana:3000/api/dashboards/import\n"
  },
  {
    "path": "docker/examples/prometheus/prometheus.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nglobal:\n  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n\nscrape_configs:\n  - job_name: 'prometheus'\n    static_configs:\n      - targets: ['localhost:9090']\n  - job_name: 'zipkin'\n    scrape_interval: 5s\n    metrics_path: '/prometheus'\n    static_configs:\n      - targets: ['zipkin:9411']\n"
  },
  {
    "path": "docker/start-zipkin",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts Zipkin\nset -eu\n\nSTORAGE_TYPE=${STORAGE_TYPE:-mem}\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=${QUERY_PORT:-9411}\n\nif [ \"${STORAGE_TYPE}\" = \"mem\" ]; then\n  # When using in-memory provider, allocate 160m, most of which for trace storage\n  JAVA_OPTS=${JAVA_OPTS:-\"-Xms160m -Xmx160m -XX:+ExitOnOutOfMemoryError\"}\nfi\n\n# MODULE_OPTS is not set in the zipkin-slim dist, so use it to detect if this is a slim build\nif [ -z \"${MODULE_OPTS+x}\" ]; then\n  # Allocate less memory when using the slim build\n  JAVA_OPTS=${JAVA_OPTS:-\"-Xms64m -Xmx64m -XX:+ExitOnOutOfMemoryError\"}\n\n  # Use main class directly if there are no modules, as it measured 14% faster from JVM running to\n  # available verses PropertiesLauncher when using Zipkin was based on Spring Boot 2.1\n  exec java ${JAVA_OPTS} -cp '.:BOOT-INF/lib/*:BOOT-INF/classes' zipkin.server.ZipkinServer \"$@\"\nelse\n  JAVA_OPTS=${JAVA_OPTS:-\"-Xms96m -Xmx96m -XX:+ExitOnOutOfMemoryError\"}\n\n  # Disable Log4j2 JMX extensions when running the full build\n  exec java ${MODULE_OPTS} ${JAVA_OPTS} -cp . \\\n  -Dlog4j2.disable.jmx=true \\\n  org.springframework.boot.loader.launch.PropertiesLauncher \"$@\"\nfi\n"
  },
  {
    "path": "docker/test-images/zipkin-activemq/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-activemq\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-activemq/start-activemq /docker-bin/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /install\n\n# Use latest version from https://activemq.apache.org/components/classic/download/\nARG activemq_version=6.1.5\n\n# Download the distribution\nRUN \\\n# Connection resets are frequent in GitHub Actions workflows \\\nwget --random-wait --tries=5 -qO- \\\n# Remove junk from the distribution while downloading it\nhttps://archive.apache.org/dist/activemq/${activemq_version}/apache-activemq-${activemq_version}-bin.tar.gz| tar xz \\\n    --wildcards --exclude=examples --exclude=webapps-demo --strip=1\n\n# Note: this uses the JDK image as ActiveMQ has a module dependency on JMX,\n# which isn't in our JRE.\nFROM ghcr.io/openzipkin/java:${java_version} as zipkin-activemq\nLABEL org.opencontainers.image.description=\"ActiveMQ Classic on OpenJDK and Alpine Linux\"\nARG activemq_version=6.1.5\nLABEL activemq-version=$activemq_version\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-activemq\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=activemq\nWORKDIR /${USER}\nENV ACTIVEMQ_HOME=/${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# Use to set heap, trust store or other system properties.\nENV JAVA_OPTS=\"-Xms64m -Xmx64m -XX:+ExitOnOutOfMemoryError\"\nEXPOSE 1883 5672 8161 61613 61614 61616\n"
  },
  {
    "path": "docker/test-images/zipkin-activemq/README.md",
    "content": "## zipkin-activemq Docker image\n\nThe `zipkin-activemq` testing image runs ActiveMQ Classic for [ActiveMQ collector](../../../zipkin-collector/activemq)\nintegration.\n\nTo build `openzipkin/zipkin-activemq:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-activemq/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-activemq:test\n```\n\nYou can use the env variable `JAVA_OPTS` to change settings such as heap size for ActiveMQ.\n"
  },
  {
    "path": "docker/test-images/zipkin-activemq/start-activemq",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts ActiveMQ\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Configure the Docker HEALTHCHECK\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=61616\nexport HEALTHCHECK_KIND=tcp\n\necho Starting ActiveMQ\nexec bin/activemq console\n\n"
  },
  {
    "path": "docker/test-images/zipkin-cassandra/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-cassandra\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-cassandra/start-cassandra /docker-bin/\nCOPY docker/test-images/zipkin-cassandra/install.sh /install/\nCOPY zipkin-storage/cassandra/src/main/resources/*.cql /zipkin-schemas/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\n# Use latest stable version: https://cassandra.apache.org/download/\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG cassandra_version=4.1.8\nENV CASSANDRA_VERSION=$cassandra_version\nWORKDIR /install\n\nCOPY --from=scratch /zipkin-schemas/* ./zipkin-schemas/\nCOPY --from=scratch /install/install.sh /tmp/\nRUN /tmp/install.sh && rm /tmp/install.sh\n\nFROM ghcr.io/openzipkin/java:${java_version}-jre as zipkin-cassandra\nLABEL org.opencontainers.image.description=\"Cassandra on OpenJDK and Alpine Linux with Zipkin keyspaces pre-installed\"\nARG cassandra_version=4.1.8\nLABEL cassandra-version=$cassandra_version\nENV CASSANDRA_VERSION=$cassandra_version\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-cassandra\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=cassandra\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# Set variables Cassandra's start script wants\nENV JAVA_OPTS=\"-Xms256m -Xmx256m -XX:+ExitOnOutOfMemoryError -Djava.net.preferIPv4Stack=true\"\nENV LOGGING_LEVEL=WARN\n\nEXPOSE 9042\n"
  },
  {
    "path": "docker/test-images/zipkin-cassandra/README.md",
    "content": "## zipkin-cassandra Docker image\n\nThe `zipkin-cassandra` testing image runs Cassandra 4.x initialized with Zipkin's schema for\n[Cassandra storage](../../../zipkin-storage/cassandra) integration.\n\nBesides norms defined in [docker-java](https://github.com/openzipkin/docker-java), this accepts the\nfollowing environment variables:\n\n * `LOGGING_LEVEL`: Root Log4J logging level sent to stdout. Defaults to \"WARN\"\n\nTo build `openzipkin/zipkin-cassandra:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-cassandra/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-cassandra:test\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-cassandra/install.sh",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# install script used only in building the docker image, but not at runtime.\n# This uses relative path so that you can change the home dir without editing this file.\n# This also trims dependencies to only those used at runtime.\nset -eux\n\necho \"*** Installing Cassandra\"\n\n# Create directories for the Java classpath\nmkdir classes lib\n\ncat > pom.xml <<-'EOF'\n<project>\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>io.zipkin.cassandra</groupId>\n  <artifactId>get-cassandra</artifactId>\n  <version>0.1.0-SNAPSHOT</version>\n  <packaging>pom</packaging>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.apache.cassandra</groupId>\n      <artifactId>cassandra-all</artifactId>\n      <version>${cassandra.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>net.java.dev.jna</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>com.github.jbellis</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>org.slf4j</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>ch.qos.logback</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <!-- Use latest to support alpine and additional architecture -->\n    <dependency>\n      <groupId>net.java.dev.jna</groupId>\n      <artifactId>jna</artifactId>\n      <version>5.16.0</version>\n    </dependency>\n    <!-- Use latest to work with JRE 21 per CASSANDRA-18329 -->\n    <dependency>\n      <groupId>com.github.jbellis</groupId>\n      <artifactId>jamm</artifactId>\n      <version>0.4.0</version>\n    </dependency>\n    <!-- log4j not logback -->\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-log4j12</artifactId>\n      <version>1.7.36</version>\n    </dependency>\n  </dependencies>\n</project>\nEOF\nmvn -q --batch-mode -DoutputDirectory=lib \\\n    -Dcassandra.version=${CASSANDRA_VERSION} \\\n    org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy-dependencies\nrm pom.xml\n\n# Get a version of ObjectSizes.java that compiles with jamm 0.4.0\nwget --random-wait --tries=5 -qO ObjectSizes.java \\\n  https://raw.githubusercontent.com/apache/cassandra/refs/tags/cassandra-5.0.3/src/java/org/apache/cassandra/utils/ObjectSizes.java\n# Rename a public method back to the same name as used in Cassandra 4.1.\nsed -i 's/sizeOnHeapExcludingDataOf/sizeOnHeapExcludingData/g' ObjectSizes.java\n# Compile it into classes, which overrides the same class from Cassandra 4.1.\njavac -cp 'lib/*' -d classes ObjectSizes.java\n\n# Make sure you use relative paths in references like this, so that installation\n# is decoupled from runtime\nmkdir -p conf data commitlog saved_caches hints triggers\n\n# Generate basic required configuration from default values\ncat > conf/cassandra.yaml <<-'EOF'\npartitioner: org.apache.cassandra.dht.Murmur3Partitioner\ncommitlog_sync: periodic\ncommitlog_sync_period_in_ms: 10000\nendpoint_snitch: SimpleSnitch\n\n# override via -Dcassandra.storage_port=7000\nstorage_port: 7000\n# override via -Dcassandra.native_transport_port=9042\nnative_transport_port: 9042\nlisten_address: 127.0.0.1\nstart_native_transport: true\nseed_provider:\n    - class_name: org.apache.cassandra.locator.SimpleSeedProvider\n      parameters:\n          - seeds: \"127.0.0.1\"\n\n# Disabled by default in Cassandra 4\nenable_sasi_indexes: true\nEOF\n\ntemp_storage_port=7010\ntemp_native_transport_port=9052\n\n# Keep INFO logs as if this fails in CI, we'll get more insight. These aren't displayed unless we\n# have a crash.\ncat > conf/log4j.properties <<-'EOF'\nlog4j.rootLogger=INFO, stdout\n\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n\nlog4j.appender.stdout.Target=System.out\nEOF\n\ncat zipkin-schemas/zipkin2-schema.cql zipkin-schemas/zipkin2-schema-indexes.cql > schema\n\n# read_repair_chance options were removed and make Cassandra crash starting in v4\n# See https://cassandra.apache.org/doc/latest/operating/read_repair.html#background-read-repair\nsed -i '/read_repair_chance/d' schema\n\n# Run cassandra on a different port temporarily in order to setup the schema.\n#\n# We also add exports and opens from both Cassandra v4 and v5, except for\n# attach, compiler and rmi because our JRE excludes these modules.\n#\n# Merging makes adding Cassandra v5 easier and lets us share a common JRE 17+\n# with other test images even if Cassandra v4 will never officially support it.\n# https://github.com/apache/cassandra/blob/cassandra-4.1.8/conf/jvm11-server.options\n# https://github.com/apache/cassandra/blob/cassandra-5.0.3/conf/jvm17-server.options\n#\n# Finally, we allow security manager to prevent JRE 21 crashing when Cassandra\n# attempts ThreadAwareSecurityManager.install()\njava -cp 'classes:lib/*' -Xms64m -Xmx64m -XX:+ExitOnOutOfMemoryError -verbose:gc \\\n  -Djava.security.manager=allow \\\n  -Djdk.attach.allowAttachSelf=true \\\n  --add-exports java.base/jdk.internal.misc=ALL-UNNAMED \\\n  --add-exports java.base/jdk.internal.ref=ALL-UNNAMED \\\n  --add-exports java.base/sun.nio.ch=ALL-UNNAMED \\\n  --add-exports java.sql/java.sql=ALL-UNNAMED \\\n  --add-exports java.base/java.lang.ref=ALL-UNNAMED \\\n  --add-exports jdk.unsupported/sun.misc=ALL-UNNAMED \\\n  --add-opens java.base/java.lang.module=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.loader=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.ref=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.math=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.module=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED \\\n  --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED \\\n  --add-opens java.base/java.io=ALL-UNNAMED \\\n  --add-opens java.base/java.nio=ALL-UNNAMED \\\n  --add-opens java.base/sun.nio.ch=ALL-UNNAMED \\\n  --add-opens java.base/java.io=ALL-UNNAMED \\\n  --add-opens java.base/java.lang.reflect=ALL-UNNAMED \\\n  --add-opens java.base/java.lang=ALL-UNNAMED \\\n  --add-opens java.base/java.util=ALL-UNNAMED \\\n  --add-opens java.base/java.nio=ALL-UNNAMED \\\n  --add-opens java.base/java.util.concurrent=ALL-UNNAMED \\\n  --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED \\\n  -Dcassandra.storage_port=${temp_storage_port} \\\n  -Dcassandra.native_transport_port=${temp_native_transport_port} \\\n  -Dcassandra.storagedir=${PWD} \\\n  -Dcassandra.triggers_dir=${PWD}/triggers \\\n  -Dcassandra.config=file:${PWD}/conf/cassandra.yaml \\\n  -Dlog4j.configuration=file:${PWD}/conf/log4j.properties \\\n  org.apache.cassandra.service.CassandraDaemon > temp_cassandra.out 2>&1 &\ntemp_cassandra_pid=$!\n\nis_cassandra_alive() {\n  if ! kill -0 ${temp_cassandra_pid}; then\n    cat temp_cassandra.out\n    maybe_crash_file=hs_err_pid${temp_cassandra_pid}.log\n    test -f $maybe_crash_file && cat $maybe_crash_file\n    return 1\n  fi\n  return 0\n}\n\nis_cassandra_alive || exit 1\n\necho \"*** Installing cqlsh\"\n# cqlsh 4.x is not compatible with Python 3.12 by default.\n# See https://issues.apache.org/jira/browse/CASSANDRA-19206\npython3_version=3.12\napk add --update --no-cache python3=~${python3_version}\n# Installing cqlsh requires cffi package. Normally this doesn't need\n# to be compiled, but something isn't right with aarch64 when installing\n# cqlsh it needs to build cffi. To unblock support for aarch64, adding\n# the following are necessary for compiling cffi. If pip someday changes and\n# doesn't compile cffi on aarch64 then we can remove these dependencies.\n# libev is required when using python 3.12\n# TODO: remove git when https://github.com/jeffwidman/cqlsh/pull/37 is released\napk add --update --no-cache gcc python3-dev=~${python3_version} musl-dev libffi-dev libev libev-dev git\n# PEP 668 protects against mixing system and pip packages. Setup virtual env to avoid this.\npython3 -m venv .venv\n. .venv/bin/activate\npython3 -m ensurepip --upgrade\npip install --upgrade pip setuptools wheel\n# Installing from trunk since released versions have dependency on old setup tools\npip install -Iq git+https://github.com/apache/cassandra-python-driver@trunk\npip install cqlsh\ncql() {\n  cqlsh \"$@\" 127.0.0.1 ${temp_native_transport_port}\n}\n\n# Excessively long timeout to avoid having to create an ENV variable, decide its name, etc.\ntimeout=180\necho \"Will wait up to ${timeout} seconds for Cassandra to come up before installing Schema\"\nwhile [ \"$timeout\" -gt 0 ] && ! cql -e 'SHOW VERSION' > /dev/null 2>&1; do\n    is_cassandra_alive || exit 1\n    sleep 1\n    timeout=$(($timeout - 1))\ndone\n\necho \"*** Importing Scheme\"\ncat schema | cql --debug && rm schema\n\necho \"*** Stopping Cassandra\"\nkill ${temp_cassandra_pid}\nwait\n\n# The image will use a less chatty Log4J conf which only logs warnings (to stdout).\ncat > conf/log4j.properties <<-'EOF'\nlog4j.rootLogger=WARN, stdout\n\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n\nlog4j.appender.stdout.Target=System.out\n\n# Ignore that we are using log4j as we aren't starting JMX anyway\nlog4j.logger.org.apache.cassandra.utils.logging=ERROR\n# Ignore that we cannot increase RLIMIT_MEMLOCK or run Cassandra as root\nlog4j.logger.org.apache.cassandra.utils.NativeLibrary=ERROR\n# Ignore that we disabled JMX and haven't installed jemalloc (not available on Alpine)\nlog4j.logger.org.apache.cassandra.service.StartupChecks=OFF\n# Ignore C* 3.x java.lang.NoSuchMethodError: 'sun.misc.Cleaner sun.nio.ch.DirectBuffer.cleaner()'\nlog4j.logger.org.apache.cassandra.io.util.FileUtils=OFF\n# Ignore warnings about less than 64GB disk\nlog4j.logger.org.apache.cassandra.config.DatabaseDescriptor=ERROR\nEOF\n\n# Take a backup so that we can safely mount an empty volume over the data directory and maintain the schema\ncp -R data/ data-backup/\n\necho \"*** Cleaning Up\"\nrm -rf zipkin-schemas/ temp_cassandra.out\n\necho \"*** Image build complete\"\n"
  },
  {
    "path": "docker/test-images/zipkin-cassandra/start-cassandra",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts Cassandra\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Apply one-time deferred configuration that relies on ENV variables\n#\n# If the schema has been removed due to mounting, restore from our backup. see: install\nif [ ! -d data/zipkin2 ]; then\n  cp -rf data-backup/* data/\nfi\n\nIP=\"$(hostname -i || echo '127.0.0.1')\"\nsed -i \"s/127.0.0.1/${IP}/g\" conf/cassandra.yaml\n\n# Replace the logging level\nsed -i \"s/log4j.rootLogger.*/log4j.rootLogger=${LOGGING_LEVEL}, stdout/\" conf/log4j.properties\n\n# Use agent to allow instrumentation of a lambda: CASSANDRA-16304\nJAMM_JAR=$(ls lib/jamm-*.jar)\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_IP=${IP}\nexport HEALTHCHECK_PORT=9042\nexport HEALTHCHECK_KIND=tcp\n\necho Starting Cassandra\n# -cp 'classes:lib/*' allows layers to patch the image without packaging or\n# overwriting jars.\n#\n# We also add exports and opens from both Cassandra v4 and v5, except for\n# attach, compiler and rmi because our JRE excludes these modules.\n#\n# Merging makes adding Cassandra v5 easier and lets us share a common JRE 17+\n# with other test images even if Cassandra v4 will never officially support it.\n# https://github.com/apache/cassandra/blob/cassandra-4.0.11/conf/jvm11-server.options\n# https://github.com/apache/cassandra/blob/cassandra-5.0/conf/jvm17-server.options\n#\n# Finally, we allow security manager to prevent JRE 21 crashing when Cassandra\n# attempts ThreadAwareSecurityManager.install()\nexec java -cp 'classes:lib/*' ${JAVA_OPTS} \\\n  -Djava.security.manager=allow \\\n  -Xbootclasspath/a:${JAMM_JAR} -javaagent:${JAMM_JAR} \\\n  -Djdk.attach.allowAttachSelf=true \\\n  --add-exports java.base/jdk.internal.misc=ALL-UNNAMED \\\n  --add-exports java.base/jdk.internal.ref=ALL-UNNAMED \\\n  --add-exports java.base/sun.nio.ch=ALL-UNNAMED \\\n  --add-exports java.sql/java.sql=ALL-UNNAMED \\\n  --add-exports java.base/java.lang.ref=ALL-UNNAMED \\\n  --add-exports jdk.unsupported/sun.misc=ALL-UNNAMED \\\n  --add-opens java.base/java.lang.module=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.loader=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.ref=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.math=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.module=ALL-UNNAMED \\\n  --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED \\\n  --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED \\\n  --add-opens java.base/java.io=ALL-UNNAMED \\\n  --add-opens java.base/java.nio=ALL-UNNAMED \\\n  --add-opens java.base/sun.nio.ch=ALL-UNNAMED \\\n  --add-opens java.base/java.io=ALL-UNNAMED \\\n  --add-opens java.base/java.lang.reflect=ALL-UNNAMED \\\n  --add-opens java.base/java.lang=ALL-UNNAMED \\\n  --add-opens java.base/java.util=ALL-UNNAMED \\\n  --add-opens java.base/java.nio=ALL-UNNAMED \\\n  --add-opens java.base/java.util.concurrent=ALL-UNNAMED \\\n  --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED \\\n  -Djava.io.tmpdir=/tmp \\\n  -Dcassandra-foreground=yes \\\n  -Dcassandra.storagedir=${PWD} \\\n  -Dcassandra.triggers_dir=${PWD}/triggers \\\n  -Dcassandra.config=file:${PWD}/conf/cassandra.yaml \\\n  -Dlog4j.configuration=file:${PWD}/conf/log4j.properties \\\n  org.apache.cassandra.service.CassandraDaemon \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch7/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-elasticsearch7\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-elasticsearch7/start-elasticsearch /docker-bin/\nCOPY docker/test-images/zipkin-elasticsearch7/config/ /config/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /install\n\n# Use latest 7.x version from https://www.elastic.co/downloads/past-releases#elasticsearch-no-jdk\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG elasticsearch7_version=7.17.27\n\n# Download only the OSS distribution (lacks X-Pack)\nRUN \\\n# Connection resets are frequent in GitHub Actions workflows \\\nwget --random-wait --tries=5 -qO- \\\n# We don't download bin scripts as we customize for reasons including BusyBox problems\nhttps://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearch7_version}-no-jdk-linux-x86_64.tar.gz| tar xz \\\n    --wildcards --strip=1 --exclude=*/bin && mkdir classes\nCOPY --from=scratch /config/ ./config/\n\n# Use a full Java distribution rather than adding test modules to the\n# production -jre base layer used by zipkin and zipkin-slim.\nFROM ghcr.io/openzipkin/java:${java_version} as zipkin-elasticsearch7\nLABEL org.opencontainers.image.description=\"Elasticsearch distribution on OpenJDK and Alpine Linux\"\nARG elasticsearch7_version=7.17.27\nLABEL elasticsearch-version=$elasticsearch7_version\n\n# The full license is also included in the image at /elasticsearch/LICENSE.txt.\nLABEL org.opencontainers.image.licenses=\"Elastic-License-2.0\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-elasticsearch\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=elasticsearch\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# Use to set heap, trust store or other system properties.\nENV JAVA_OPTS=\"-Xms512m -Xmx512m -XX:+ExitOnOutOfMemoryError\"\nENV LIBFFI_TMPDIR=/tmp\nEXPOSE 9200\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch7/README.md",
    "content": "## zipkin-elasticsearch7 Docker image\n\nThe `zipkin-elasticsearch7` testing image runs Elasticsearch 7.x for [Elasticsearch storage](../../../zipkin-storage/elasticsearch)\nintegration.\n\nTo build `openzipkin/zipkin-elasticsearch7:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-elasticsearch7/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-elasticsearch7:test\n```\n\nYou can use the env variable `JAVA_OPTS` to change settings such as heap size for Elasticsearch.\n\n#### Host setup\n\nElasticsearch is [strict](https://github.com/docker-library/docs/tree/master/elasticsearch#host-setup)\nabout virtual memory. You will need to adjust accordingly (especially if you notice Elasticsearch crash!)\n\n```bash\n# If docker is running on your host machine, adjust the kernel setting directly\n$ sudo sysctl -w vm.max_map_count=262144\n\n# If using docker-machine/Docker Toolbox/Boot2Docker, remotely adjust the same\n$ docker-machine ssh default \"sudo sysctl -w vm.max_map_count=262144\"\n\n# If using colima, it is similar as well\n$ colima ssh \"sudo sysctl -w vm.max_map_count=262144\"\n```\n\n#### License\n\nThis Elasticsearch image is only made for testing features supported by Zipkin,\nand is subject to [Elastic-License-2.0](https://www.elastic.co/licensing/elastic-license).\nFor more details, inspect the LICENSE.txt and NOTICE.txt in the /elasticsearch\ndirectory of this image.\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch7/config/elasticsearch.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\ncluster.name: \"docker-cluster\"\nnetwork.host: 0.0.0.0\nnode.name: zipkin-elasticsearch\n\n# Enable development mode and disable bootstrap checks\n# See https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html\ndiscovery.type: single-node\n# Avoid deprecation errors: as of 8.x the only accepted value is true.\ncluster.routing.allocation.disk.watermark.enable_for_single_data_node: true\n\n# This is a test image, so we are not afraid to delete all indices. Avoids:\n#   Wildcard expressions or all indices are not allowed\naction.destructive_requires_name: false\n# We don't use geoip\ningest.geoip.downloader.enabled: false\n\n# disable all xpack features than can be disabled\nxpack.graph.enabled: false\nxpack.ml.enabled: false\nxpack.security.enabled: false\nxpack.watcher.enabled: false\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch7/config/log4j2.properties",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nstatus = error\n\nappender.console.type = Console\nappender.console.name = console\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%node_name]%marker %m%n\n\nrootLogger.level = info\nrootLogger.appenderRef.console.ref = console\n\n# unsolved https://github.com/sherifabdlnaby/elastdocker/issues/108\nlogger.aws.name = com.amazonaws.auth.profile.internal.BasicProfileConfigFileLoader\nlogger.aws.level = error\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch7/start-elasticsearch",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts Elasticsearch\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# This file inlines what's done by the Elasticsearch script machinery, which doesn't work here due\n# to our images using busybox (not bash). See #3044\n#\n# Notable settings:\n# * lower heap size\n# * tmpdir manual as https://github.com/elastic/elasticsearch/pull/31003 was closed won't fix\n# * disable log4j JMX not just because we don't use it...\n#  * ES enables security manager https://github.com/elastic/elasticsearch/issues/21932#issuecomment-264435034\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=9200\nexport HEALTHCHECK_PATH=/_cluster/health\n\n# -cp 'classes:lib/*' allows layers to patch the image without packaging or overwriting jars\n# We allow security manager (via flag to prevent JRE 21 crash) as Elasticsearch.main needs it.\nexec java -cp 'classes:lib/*' ${JAVA_OPTS} \\\n  -Djava.security.manager=allow \\\n  -Djava.io.tmpdir=/tmp \\\n  -Dlog4j2.disable.jmx=true \\\n  -Des.path.home=$PWD -Des.path.conf=$PWD/config \\\n  org.elasticsearch.bootstrap.Elasticsearch \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch8/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-elasticsearch8\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-elasticsearch8/start-elasticsearch /docker-bin/\nCOPY docker/test-images/zipkin-elasticsearch7/config/ /config/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /install\n\n# Use latest 8.x version from https://www.elastic.co/downloads/past-releases#elasticsearch\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG elasticsearch8_version=8.17.2\n\n# Download only the OSS distribution (lacks X-Pack)\nRUN \\\n# Connection resets are frequent in GitHub Actions workflows \\\nwget --random-wait --tries=5 -qO- \\\n# We don't download bin scripts as we customize for reasons including BusyBox problems\nhttps://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearch8_version}-linux-x86_64.tar.gz| tar xz \\\n    --wildcards --strip=1 --exclude=jdk --exclude=*/bin\nCOPY --from=scratch /config/ ./config/\n\n# Use a full Java distribution rather than adding test modules to the\n# production -jre base layer used by zipkin and zipkin-slim.\nFROM ghcr.io/openzipkin/java:${java_version} as zipkin-elasticsearch8\nLABEL org.opencontainers.image.description=\"Elasticsearch distribution on OpenJDK and Alpine Linux\"\nARG elasticsearch8_version=8.17.2\nLABEL elasticsearch-version=$elasticsearch8_version\n\n# The full license is also included in the image at /elasticsearch/LICENSE.txt.\nLABEL org.opencontainers.image.licenses=\"Elastic-License-2.0\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-elasticsearch\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=elasticsearch\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# Use to set heap, trust store or other system properties.\nENV ES_JAVA_OPTS=\"-Xms512m -Xmx512m -XX:+ExitOnOutOfMemoryError\"\nENV LIBFFI_TMPDIR=/tmp\nEXPOSE 9200\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch8/README.md",
    "content": "## zipkin-elasticsearch8 Docker image\n\nThe `zipkin-elasticsearch8` testing image runs Elasticsearch 8.x for [Elasticsearch storage](../../../zipkin-storage/elasticsearch)\nintegration.\n\nTo build `openzipkin/zipkin-elasticsearch8:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-elasticsearch8/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-elasticsearch8:test\n```\n\nYou can use the env variable `ES_JAVA_OPTS` to change settings such as heap size for Elasticsearch.\n\n#### Host setup\n\nElasticsearch is [strict](https://github.com/docker-library/docs/tree/master/elasticsearch#host-setup)\nabout virtual memory. You will need to adjust accordingly (especially if you notice Elasticsearch crash!)\n\n```bash\n# If docker is running on your host machine, adjust the kernel setting directly\n$ sudo sysctl -w vm.max_map_count=262144\n\n# If using docker-machine/Docker Toolbox/Boot2Docker, remotely adjust the same\n$ docker-machine ssh default \"sudo sysctl -w vm.max_map_count=262144\"\n\n# If using colima, it is similar as well\n$ colima ssh \"sudo sysctl -w vm.max_map_count=262144\"\n```\n\n#### License\n\nThis Elasticsearch image is only made for testing features supported by Zipkin,\nand is subject to [Elastic-License-2.0](https://www.elastic.co/licensing/elastic-license).\nFor more details, inspect the LICENSE.txt and NOTICE.txt in the /elasticsearch\ndirectory of this image.\n"
  },
  {
    "path": "docker/test-images/zipkin-elasticsearch8/start-elasticsearch",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts Elasticsearch\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=9200\nexport HEALTHCHECK_PATH=/_cluster/health\n\n# This loads the ES launcher because configuration of the actual process is\n# in binary and not documented for external use.\n# See https://github.com/elastic/elasticsearch/blob/v8.11.3/server/src/main/java/org/elasticsearch/bootstrap/ServerArgs.java#L57\n#\n# Notably, this means that just like the default image, the ES daemon is not\n# pid 1\nexec java -cp 'lib/*:lib/cli-launcher/*' -XX:+UseSerialGC \\\n  -Dcli.name=server \\\n  -Dcli.script=$PWD/bin/elasticsearch \\\n  -Dcli.libs=lib/tools/server-cli \\\n  -Des.path.home=$PWD \\\n  -Des.path.conf=$PWD/config \\\n  -Des.distribution.type=docker \\\n  org.elasticsearch.launcher.CliToolLauncher\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime base layers of eureka and eureka-slim.\n#\n# Use latest version here: https://github.com/orgs/openeureka/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-eureka/start-eureka /docker-bin/\n\n# We needsource because as of Eureka 2.0, there is no distribution in the\n# source repository anymore, and the war isn't functional either. The current\n# approach is to build your own with Spring Cloud.\n#\n# See https://github.com/Netflix/eureka/releases/tag/v2.0.0\nCOPY build-bin/maven /code/\nCOPY docker/test-images/zipkin-eureka /code/\n\n# This version is only used during the install process. Try to be consistent as it reduces layers,\n# which reduces downloads.\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /code\nCOPY --from=scratch /code/ .\n\n# Build the custom Eureka server\nRUN /code/maven_build && \\\n    mvn -q --batch-mode -DoutputDirectory=/install/lib dependency:copy-dependencies && \\\n    cp -r target/classes /install/\n\n# Use a full Java distribution rather than adding test modules to the\n# production -jre base layer used by zipkin and zipkin-slim.\n#\n# Specifically, this is about NoClassDefFoundError: org/ietf/jgss/GSSException\nFROM ghcr.io/openzipkin/java:${java_version} as zipkin-eureka\nLABEL org.opencontainers.image.description=\"Eureka on OpenJDK and Alpine Linux\"\n\n# All content including binaries and logs write under WORKDIR\nARG USER=eureka\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENV HEALTHCHECK_PATH=/actuator/health\nENV HEALTHCHECK_PORT=8761\n\nENTRYPOINT [\"start-eureka\"]\n\n# Switch to the runtime user\nUSER ${USER}\n\n# Use to set heap, trust store or other system properties.\nENV JAVA_OPTS=\"-Xms64m -Xmx64m -XX:+ExitOnOutOfMemoryError\"\nEXPOSE 8761\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/README.md",
    "content": "## zipkin-eureka Docker image\n\nThe `zipkin-eureka` testing image runs Eureka Server for service discovery\nintegration of the Zipkin server. This listens on port 8761.\n\nBesides norms defined in [docker-java](https://github.com/openzipkin/docker-java), this accepts the\nfollowing environment variables:\n\n* `EUREKA_USERNAME`: username for authenticating endpoints under \"/eureka\".\n* `EUREKA_PASSWORD`: password for authenticating endpoints under \"/eureka\".\n* `JAVA_OPTS`: to change settings such as heap size for Eureka.\n\nTo build `openzipkin/zipkin-eureka:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-eureka/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-eureka:test\n$ docker run -p 8761:8761 --rm openzipkin/zipkin-eureka:test\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/pom.xml",
    "content": "<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>io.zipkin.test</groupId>\n  <artifactId>eureka</artifactId>\n  <version>1.0-SNAPSHOT</version>\n  <packaging>jar</packaging>\n\n  <name>Netflix Eureka test binary</name>\n  <description>Netflix Eureka test binary</description>\n\n  <properties>\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <maven.compiler.release>17</maven.compiler.release>\n  </properties>\n\n  <!-- Get rid of transitive CVE warnings, except commons-jxpath which is abandoned -->\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>com.fasterxml.jackson</groupId>\n        <artifactId>jackson-bom</artifactId>\n        <version>2.21.1</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-dependencies</artifactId>\n        <version>3.5.11</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n      <!-- CVE fix versions -->\n      <dependency>\n        <groupId>com.google.guava</groupId>\n        <artifactId>guava</artifactId>\n        <version>33.4.0-jre</version>\n      </dependency>\n      <dependency>\n        <groupId>com.thoughtworks.xstream</groupId>\n        <artifactId>xstream</artifactId>\n        <version>1.4.21</version>\n      </dependency>\n      <dependency>\n        <groupId>org.apache.httpcomponents</groupId>\n        <artifactId>httpclient</artifactId>\n        <version>4.5.14</version>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.springframework.cloud</groupId>\n      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>\n      <version>4.3.1</version>\n      <exclusions>\n        <!-- dodge spring-jcl conflict error -->\n        <exclusion>\n          <groupId>commons-logging</groupId>\n          <artifactId>commons-logging</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>software.amazon.ion</groupId>\n          <artifactId>ion-java</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <!-- dodge CVE -->\n    <dependency>\n      <groupId>com.amazon.ion</groupId>\n      <artifactId>ion-java</artifactId>\n      <version>1.11.10</version>\n    </dependency>\n\n    <!-- Get rid of log warning saying to use Caffeine -->\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-cache</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.github.ben-manes.caffeine</groupId>\n      <artifactId>caffeine</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/src/main/java/zipkin/test/EurekaProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin.test;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n/** Properties for configuring and building a {@link EurekaServer}. */\n@ConfigurationProperties(\"eureka\")\nclass EurekaProperties {\n\n  /** Optional username to require. */\n  private String username;\n\n  /** Optional password to require. */\n  private String password;\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = username;\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = password;\n  }\n}\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/src/main/java/zipkin/test/EurekaSecurity.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin.test;\n\nimport jakarta.servlet.FilterChain;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Base64;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.boot.web.servlet.FilterRegistrationBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.filter.OncePerRequestFilter;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/** This enables security, particularly only BASIC auth, when {@code EUREKA_USERNAME} is set. */\n@Configuration\n@ConditionalOnProperty(\"eureka.username\")\n@EnableConfigurationProperties(EurekaProperties.class)\npublic class EurekaSecurity {\n  @Bean FilterRegistrationBean<BasicAuthFilter> authFilter(EurekaProperties props) {\n    FilterRegistrationBean<BasicAuthFilter> registrationBean = new FilterRegistrationBean<>();\n    registrationBean.setFilter(new BasicAuthFilter(props.getUsername(), props.getPassword()));\n    registrationBean.addUrlPatterns(\"/eureka/*\"); // Auth /eureka, though only v2 is valid\n    registrationBean.setOrder(2);\n    return registrationBean;\n  }\n\n  /** Implements BASIC instead of spring-security + CORS, CSRF and management exclusions. */\n  static final class BasicAuthFilter extends OncePerRequestFilter {\n    final String expectedAuthorization;\n\n    BasicAuthFilter(String username, String password) {\n      expectedAuthorization =\n        \"Basic \" + Base64.getEncoder().encodeToString((username + ':' + password).getBytes(UTF_8));\n    }\n\n    @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,\n      FilterChain chain) throws ServletException, IOException {\n      String authHeader = req.getHeader(\"Authorization\");\n      if (expectedAuthorization.equals(authHeader)) {\n        chain.doFilter(req, res); // Pass on the supplied credentials\n        return;\n      }\n      res.setHeader(\"WWW-Authenticate\", \"Basic realm=\\\"Realm'\\\"\");\n      res.sendError(HttpServletResponse.SC_UNAUTHORIZED); // Return 401 otherwise.\n    }\n  }\n}\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/src/main/java/zipkin/test/EurekaServer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin.test;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;\nimport org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;\nimport org.springframework.context.annotation.Import;\n\n/**\n * This disables automatic security configuration, deferring to {@linkplain EurekaSecurity}.\n * Doing so allows Eureka to start as if spring-security wasn't in the classpath.\n */\n@SpringBootApplication(\n  exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class}\n)\n@EnableEurekaServer\n@Import(EurekaSecurity.class)\npublic class EurekaServer {\n\n  public static void main(String[] args) {\n    SpringApplication.run(EurekaServer.class, args);\n  }\n}\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/src/main/resources/application.yaml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Add configuration to disable as much server caching as possible. Note that\n# not all configuration here are defined by netflix/eureka, rather some are\n# specific to spring-cloud/spring-cloud-netflix.\neureka:\n  client:\n    registerWithEureka: false\n    fetchRegistry: false  # in netflix/eureka this is shouldFetchRegistry\n    registryFetchIntervalSeconds: 1\n  server:\n    useReadOnlyResponseCache: false\n    # We could set myUrl to avoid this server thinking it is also an\n    # unavailable replica. However, the effect of doing so is worse.\n    # See https://github.com/spring-cloud/spring-cloud-netflix/issues/4251\nserver:\n  port: 8761\nspring:\n  jmx:\n    # reduce startup time by excluding unexposed JMX service\n    enabled: false\n  main:\n    banner-mode: \"off\"\n  profiles:\n    active: \"default\"\nlogging:\n  level:\n    # reduce chattiness\n    root: 'WARN'\n    # hush initialization warnings from BeanPostProcessorChecker\n    org.springframework.context.support: 'OFF'\n    # show startup completion\n    zipkin.test.EurekaServer: 'INFO'\n"
  },
  {
    "path": "docker/test-images/zipkin-eureka/start-eureka",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts Eureka\nset -eu\n\nexec java -cp 'classes:lib/*' ${JAVA_OPTS} zipkin.test.EurekaServer \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-kafka/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-kafka\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-kafka/start-kafka-zookeeper /docker-bin/\nCOPY docker/test-images/zipkin-kafka/install.sh /install/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\n# Use latest release from: https://kafka.apache.org/downloads\n#\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG kafka_version=3.9.1\nENV KAFKA_VERSION=$kafka_version\n# Note: Scala 2.13+ supports JRE 14\nARG scala_version=2.13\nENV SCALA_VERSION=$scala_version\n\nWORKDIR /install\nCOPY --from=scratch /install/install.sh /tmp/\nRUN /tmp/install.sh && rm /tmp/install.sh\n\n# Share the same base image to reduce layers used in testing\nFROM ghcr.io/openzipkin/java:${java_version}-jre as zipkin-kafka\nLABEL org.opencontainers.image.description=\"Kafka and ZooKeeper on OpenJDK and Alpine Linux\"\nARG kafka_version=3.9.0\nLABEL kafka-version=$kafka_version\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-kafka-zookeeper\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=kafka\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# ${KAFKA_ADVERTISED_HOST_NAME}:19092 is for connections from the Docker host\nENV KAFKA_ADVERTISED_HOST_NAME=localhost\n# Use to set heap, trust store or other system properties.\nENV ZOOKEEPER_JAVA_OPTS=\"-Xms32m -Xmx32m -XX:+ExitOnOutOfMemoryError\"\nENV JAVA_OPTS=\"-Xms256m -Xmx256m -XX:+ExitOnOutOfMemoryError\"\nENV LOGGING_LEVEL=WARN\n\nEXPOSE 2181 9092 19092\n"
  },
  {
    "path": "docker/test-images/zipkin-kafka/README.md",
    "content": "## zipkin-kafka Docker image\n\nThe `zipkin-kafka` testing image runs both Kafka+ZooKeeper for the [Kafka collector](../../../zipkin-collector/kafka)\nand the upcoming [Kafka storage](https://github.com/openzipkin-contrib/zipkin-storage-kafka).\n\nBesides norms defined in [docker-java](https://github.com/openzipkin/docker-java), this accepts the\nfollowing environment variables:\n\n * `LOGGING_LEVEL`: Root Log4J logging level sent to stdout. Defaults to \"WARN\"\n\n\nTo build `openzipkin/zipkin-kafka:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-kafka/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-kafka:test\n```\n\nThen configure the [Kafka sender](https://github.com/openzipkin/zipkin-reporter-java/blob/master/kafka/src/main/java/zipkin2/reporter/kafka/KafkaSender.java) using a `bootstrapServers` value of `host.docker.internal:9092` if your application is inside the same docker network or `localhost:19092` if not, but running on the same host.\n\nIn other words, if you are running a sample application on your laptop, you would use `localhost:19092` bootstrap server to send spans to the Kafka broker running in Docker.\n"
  },
  {
    "path": "docker/test-images/zipkin-kafka/install.sh",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# install script used only in building the docker image, but not at runtime.\n# This uses relative path so that you can change the home dir without editing this file.\n# This also trims dependencies to only those used at runtime.\nset -eux\n\necho \"*** Installing Kafka and dependencies\"\n# Create directories for the Java classpath\nmkdir classes lib\n\n# Dist includes large dependencies needed by streams and connect: retain only broker and ZK.\n# We can do this because broker is independent from both kafka-streams and connect modules.\n# See KAFKA-10380\n#\n# TODO: MDEP-723 if addressed can remove the pom.xml here\ncat > pom.xml <<-'EOF'\n<project>\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>io.zipkin.kafka</groupId>\n  <artifactId>get-kafka</artifactId>\n  <version>0.1.0-SNAPSHOT</version>\n  <packaging>pom</packaging>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.apache.kafka</groupId>\n      <artifactId>kafka_${scala.version}</artifactId>\n      <version>${kafka.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-log4j12</artifactId>\n      <version>1.7.36</version>\n    </dependency>\n  </dependencies>\n</project>\nEOF\nmvn -q --batch-mode -DoutputDirectory=lib \\\n    -Dscala.version=${SCALA_VERSION} -Dkafka.version=${KAFKA_VERSION} \\\n    org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy-dependencies\nrm pom.xml\n\n# Make sure you use relative paths in references like this, so that installation\n# is decoupled from runtime\nmkdir -p bin config data/kafka data/zookeeper\n\n# Make a basic log4j config which only logs warnings (to stdout)\n#\n# NOTE: Two unavoidable log WARN messages remain:\n# 1. Either no config or no quorum defined in config, running  in standalone mode (org.apache.zookeeper.server.quorum.QuorumPeerMain)\n#   * https://github.com/apache/zookeeper/blob/e91455c1e3c50405666cd8afad71d99dceb7b340/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java#L138-L140\n# 2. No meta.properties file under dir /kafka/./data/kafka/meta.properties (kafka.server.BrokerMetadataCheckpoint)\n#   * meta.properties file is generated when broker joins the cluster, using an auto-generated cluster id:\ncat > config/log4j.properties <<-'EOF'\nlog4j.rootLogger=WARN, stdout\n\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n\nlog4j.appender.stdout.Target=System.out\nEOF\n\n# Set explicit, basic configuration\ncat > config/zookeeper.properties <<-'EOF'\ndataDir=./data/zookeeper\nclientPort=2181\nmaxClientCnxns=0\nadmin.enableServer=false\n# allow ruok command for testing ZK health\n4lw.commands.whitelist=srvr,ruok\nadmin.enableServer=false\nEOF\n\ncat > config/server.properties <<-'EOF'\nbroker.id=0\nzookeeper.connect=127.0.0.1:2181\nreplica.socket.timeout.ms=1500\n# log.dirs is about Kafka's data not Log4J\nlog.dirs=./data/kafka\nauto.create.topics.enable=true\noffsets.topic.replication.factor=1\nlisteners=PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:19092\nlistener.security.protocol.map=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\nEOF\n\n# Make a basic script for launching Kafka commands\ncat > bin/kafka-run-class.sh <<-'EOF'\n#!/bin/sh\nset -eu\n# classes allows layers to patch the image without packaging or overwriting jars\nexec java -cp 'classes:lib/*' ${JAVA_OPTS} \\\n  -Djava.io.tmpdir=/tmp \\\n  -Dlog4j.configuration=file:./config/log4j.properties \\\n  \"$@\"\nEOF\nchmod 755 bin/kafka-run-class.sh\n\necho \"*** Image build complete\"\n"
  },
  {
    "path": "docker/test-images/zipkin-kafka/start-kafka-zookeeper",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts ZooKeeper and then Kafka\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Apply one-time deferred configuration that relies on ENV variables\n#\n# Internal docker producers and consumers use the normal hostname:9092, and outside docker the advertised host on port 19092\nADVERTISED_LISTENERS=\"advertised.listeners=PLAINTEXT://${HOSTNAME}:9092,PLAINTEXT_HOST://${KAFKA_ADVERTISED_HOST_NAME}:19092\"\nKAFKA_CONFIG=./config/server.properties\ngrep -qF -- \"$ADVERTISED_LISTENERS\" $KAFKA_CONFIG || echo \"$ADVERTISED_LISTENERS\" >> $KAFKA_CONFIG\n\n# Replace the logging level\nsed -i \"s/log4j.rootLogger.*/log4j.rootLogger=${LOGGING_LEVEL}, stdout/\" config/log4j.properties\n\necho Starting ZooKeeper\n# -cp 'classes:lib/*' allows layers to patch the image without packaging or overwriting jars\nexec java -cp 'classes:lib/*' ${ZOOKEEPER_JAVA_OPTS} \\\n  -Djava.io.tmpdir=/tmp \\\n  -Dzookeeper.jmx.log4j.disable=true \\\n  -Dlog4j.configuration=file:./config/log4j.properties \\\n  org.apache.zookeeper.server.quorum.QuorumPeerMain ./config/zookeeper.properties &\n\n# Wait for ZooKeeper to be ok\nuntil echo ruok | nc 127.0.0.1 2181 > /dev/null; do sleep 1; done\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=9092\nexport HEALTHCHECK_KIND=tcp\n\necho Starting Kafka\nexec bin/kafka-run-class.sh kafka.Kafka ./config/server.properties \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-mysql/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/alpine\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG alpine_version=3.21.3\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-mysql/start-mysql /docker-bin/\nCOPY docker/test-images/zipkin-mysql/install.sh /install/\nCOPY zipkin-storage/mysql-v1/src/main/resources/mysql.sql /zipkin-schemas/\n\nFROM ghcr.io/openzipkin/alpine:${alpine_version} as zipkin-mysql\nLABEL org.opencontainers.image.description=\"MySQL on Alpine Linux with Zipkin schema pre-installed\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-mysql\"]\n\n# Use latest from https://pkgs.alpinelinux.org/packages?name=mysql (without the -r[0-9])\nARG mysql_version=11.8.5-r1\nLABEL mysql-version=$mysql_version\nENV MYSQL_VERSION=$mysql_version\n\nWORKDIR /tmp\nCOPY --from=scratch /zipkin-schemas/* ./install/zipkin-schemas/\nCOPY --from=scratch /install/install.sh ./install\nRUN (cd install && ./install.sh) && rm -rf ./install\n\n# All content including binaries and logs write under WORKDIR\nARG USER=mysql\nWORKDIR /${USER}\n\nEXPOSE 3306\n"
  },
  {
    "path": "docker/test-images/zipkin-mysql/README.md",
    "content": "## zipkin-mysql Docker image\n\nThe `zipkin-mysql` testing image runs MySQL 10.x initialized with Zipkin's schema for\n[MySQL storage](../../../zipkin-storage/mysql-v1) integration.\n\nTo build `openzipkin/zipkin-mysql:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-mysql/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-mysql:test\n```\n\nWhen running with docker-machine, you can connect like so:\n\n```bash\n$ mysql -h $(docker-machine ip) -u zipkin -pzipkin -D zipkin\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-mysql/install.sh",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nset -eux\n\necho \"*** Installing MySQL\"\napk add --update --no-cache mysql=~${MYSQL_VERSION} mysql-client=~${MYSQL_VERSION}\n# Fake auth tools install as 10.4.0 install dies otherwise\nmkdir -p /auth_pam_tool_dir/auth_pam_tool\n\nmysql_opts=\"--user=mysql --basedir=${PWD} --datadir=${PWD}/data --tmpdir=/tmp\"\n\n# Create the database (stored in the data directory)\nln -s /usr/bin bin\nln -s /usr/share share\nmysql_install_db ${mysql_opts} --force\n\n# Enable networking\necho skip-networking=0 >> /etc/my.cnf\necho bind-address=0.0.0.0 >> /etc/my.cnf\n\n# Prevent \"Bind on unix socket: No such file or directory\"\nmkdir -p /run/mysqld/ && chown mysql /run/mysqld/\n\necho \"*** Starting MySQL\"\nmysqld ${mysql_opts} &\ntemp_mysql_pid=$!\n\n# Excessively long timeout to avoid having to create an ENV variable, decide its name, etc.\ntimeout=180\necho \"Will wait up to ${timeout} seconds for MySQL to come up before installing Schema\"\nwhile [ \"$timeout\" -gt 0 ] && kill -0 ${temp_mysql_pid} && ! mysql --protocol=socket -uroot -e 'SELECT 1' > /dev/null 2>&1; do\n    sleep 1\n    timeout=$(($timeout - 1))\ndone\n\necho \"*** Installing Schema\"\nmysql --verbose --user=mysql --protocol=socket -uroot <<-'EOF'\nUSE mysql ;\n\nDELETE FROM mysql.user ;\nDROP DATABASE IF EXISTS test ;\n\nCREATE DATABASE zipkin ;\n\nUSE zipkin;\nSOURCE zipkin-schemas/mysql.sql ;\n\nGRANT ALL PRIVILEGES ON zipkin.* TO zipkin@'%' IDENTIFIED BY 'zipkin' WITH GRANT OPTION ;\nFLUSH PRIVILEGES ;\nEOF\n\necho \"*** Stopping MySQL\"\nkill ${temp_mysql_pid}\nwait\n\necho \"*** Copying database to /mysql/data\"\nmkdir /mysql\nmv data /mysql/\nchown -R mysql /mysql\n\necho \"*** Cleaning Up\"\napk del mysql-client\nrm bin share\n# Remove large binaries\n(cd /usr/bin; rm -f mysql_* aria_* mysqlbinlog myis* test-connect-t mysqlslap innochecksum resolve* my_print_defaults sst_dump)\n"
  },
  {
    "path": "docker/test-images/zipkin-mysql/start-mysql",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts MySQL\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=3306\nexport HEALTHCHECK_KIND=tcp\n\necho Starting MySQL\nMYSQL_OPTS=\"--user=mysql --basedir=${PWD} --datadir=${PWD}/data --tmpdir=/tmp\"\nexec mysqld_safe ${MYSQL_OPTS}\n"
  },
  {
    "path": "docker/test-images/zipkin-opensearch2/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# java_version is used for install and runtime layers of zipkin-opensearch2\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-opensearch2/start-opensearch /docker-bin/\nCOPY docker/test-images/zipkin-opensearch2/config/ /config/\n\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nWORKDIR /install\n\n# Use latest 2.x version from https://opensearch.org/downloads.html\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG opensearch2_version=2.19.0\n\n# Download only the OSS distribution (lacks X-Pack)\nRUN \\\n# Connection resets are frequent in GitHub Actions workflows \\\nwget --random-wait --tries=5 -qO- \\\n# We don't download bin scripts as we customize for reasons including BusyBox problems\nhttps://artifacts.opensearch.org/releases/bundle/opensearch/${opensearch2_version}/opensearch-${opensearch2_version}-linux-x64.tar.gz| tar xz \\\n    --wildcards --strip=1 --exclude=jdk\nCOPY --from=scratch /config/ ./config/\n\n# Use a full Java distribution rather than adding test modules to the\n# production -jre base layer used by zipkin and zipkin-slim.\nFROM ghcr.io/openzipkin/java:${java_version} as zipkin-opensearch2\nLABEL org.opencontainers.image.description=\"OpenSearch distribution on OpenJDK and Alpine Linux\"\nARG opensearch2_version=2.19.0\nLABEL opensearch-version=$opensearch2_version\n\n# The full license is also included in the image at /opensearch/LICENSE.txt.\nLABEL org.opencontainers.image.licenses=\"Apache-2.0\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"sh\", \"/usr/local/bin/start-opensearch\"]\n\n# All content including binaries and logs write under WORKDIR\nARG USER=opensearch\nWORKDIR /${USER}\n\n# Ensure the process doesn't run as root\nRUN adduser -g '' -h ${PWD} -D ${USER}\nUSER ${USER}\n\n# Copy binaries and config we installed earlier\nCOPY --from=install --chown=${USER} /install .\n\n# Use to set heap, trust store or other system properties.\nENV OPENSEARCH_JAVA_OPTS=\"-Xms512m -Xmx512m -XX:+ExitOnOutOfMemoryError\"\nENV LIBFFI_TMPDIR=/tmp\nEXPOSE 9200\n"
  },
  {
    "path": "docker/test-images/zipkin-opensearch2/README.md",
    "content": "## zipkin-opensearch2 Docker image\n\nThe `zipkin-opensearch2` testing image runs OpenSearch 2.x for [Elasticsearch storage](../../../zipkin-storage/elasticsearch)\nintegration.\n\nTo build `openzipkin/zipkin-opensearch2:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-opensearch2/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-opensearch2:test\n```\n\nYou can use the env variable `OPENSEARCH_JAVA_OPTS` to change settings such as heap size for OpenSearch.\n\n#### Host setup\n\nOpenSearch is [strict](https://github.com/docker-library/docs/tree/master/elasticsearch#host-setup)\nabout virtual memory. You will need to adjust accordingly (especially if you notice OpenSearch crash!)\n\n```bash\n# If docker is running on your host machine, adjust the kernel setting directly\n$ sudo sysctl -w vm.max_map_count=262144\n\n# If using docker-machine/Docker Toolbox/Boot2Docker, remotely adjust the same\n$ docker-machine ssh default \"sudo sysctl -w vm.max_map_count=262144\"\n\n# If using colima, it is similar as well\n$ colima ssh \"sudo sysctl -w vm.max_map_count=262144\"\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-opensearch2/config/log4j2.properties",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nstatus = error\n\nappender.console.type = Console\nappender.console.name = console\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%node_name]%marker %m%n\n\nrootLogger.level = info\nrootLogger.appenderRef.console.ref = console\n"
  },
  {
    "path": "docker/test-images/zipkin-opensearch2/config/opensearch.yml",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\ncluster.name: \"docker-cluster\"\nnetwork.host: 0.0.0.0\nnode.name: zipkin-opensearch\n\n# Enable development mode and disable bootstrap checks\n# See https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html\ndiscovery.type: single-node\n# Avoid deprecation errors: as of 2.x the only accepted value is true.\ncluster.routing.allocation.disk.watermark.enable_for_single_data_node: true\n\n# This is a test image, so we are not afraid to delete all indices. Avoids:\n#   Wildcard expressions or all indices are not allowed\naction.destructive_requires_name: false\n\n# Disable security\nplugins.security.disabled: true\n"
  },
  {
    "path": "docker/test-images/zipkin-opensearch2/start-opensearch",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# ENTRYPOINT script that starts OpenSearch\n#\n# This intentionally locates config using the current working directory, in order to consolidate\n# Dockerfile instructions to WORKDIR\nset -eu\n\n# Configure the Docker HEALTHCHECK\nexport HEALTHCHECK_PORT=9200\nexport HEALTHCHECK_PATH=/_cluster/health\n\n# Use the 'java.io.tmp' dir and precreate the 'opensearch' folder there\nexport OPENSEARCH_TMPDIR=`java -cp 'classes:lib/*' org.opensearch.tools.launchers.TempDirectory`\n\n# The JVM options parser produces the final JVM options to start OpenSearch.\n# It does this by incorporating JVM options in the following way:\n#   - first, system JVM options are applied (these are hardcoded options in the\n#     parser)\n#   - second, JVM options are read from jvm.options and jvm.options.d/*.options\n#   - third, JVM options from OPENSEARCH_JAVA_OPTS are applied\n#   - fourth, ergonomic JVM options are applied\nOPENSEARCH_JAVA_OPTS=`java -cp 'classes:lib/*' org.opensearch.tools.launchers.JvmOptionsParser \"$PWD/config\"`\n\n# -cp 'classes:lib/*' allows layers to patch the image without packaging or overwriting jars\n# We allow security manager (via flag to prevent JRE 21 crash) as OpenSearch.main needs it.\nexec java -cp 'classes:lib/*' ${OPENSEARCH_JAVA_OPTS} \\\n  -Djava.security.manager=allow \\\n  -Djava.io.tmpdir=/tmp \\\n  -Dlog4j2.disable.jmx=true \\\n  -Dopensearch.path.home=$PWD \\\n  -Dopensearch.path.conf=$PWD/config \\\n  -Dopensearch.distribution.type=docker \\\n  -Dopensearch.bundled_jdk=false \\\n  org.opensearch.bootstrap.OpenSearch \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-pulsar/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Use latest from https://hub.docker.com/r/apachepulsar/pulsar/tags\nARG pulsar_version=4.0.2\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nWORKDIR /docker-bin\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\n\nARG pulsar_version\n\nFROM apachepulsar/pulsar:${pulsar_version} as zipkin-pulsar\nLABEL pulsar-version=$pulsar_version\nLABEL org.opencontainers.image.description=\"Apache Pulsar on Alpine Linux\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\n\n# Usually, we read env set from pid 1 to get docker-healthcheck parameters.\n# However, pulsar-server has to start as root even if permissions are dropped\n# later. So, we expose it in the Dockerfile instead.\nENV HEALTHCHECK_PORT=8080\nENV HEALTHCHECK_KIND=http\nENV HEALTHCHECK_PATH=/admin/v2/clusters/standalone\nENV PULSAR_LOG_ROOT_LEVEL=WARN\nEXPOSE 8080 6650\nCMD [\"bin/pulsar\", \"standalone\"]"
  },
  {
    "path": "docker/test-images/zipkin-pulsar/README.md",
    "content": "## zipkin-pulsar Docker image\n\nThe `zipkin-pulsar` testing image runs Pulsar for Pulsar collector integration.\n\nTo build `openzipkin/zipkin-pulsar:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-pulsar/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-pulsar:test\n```\n\nYou can use the env variable `PULSAR_LOG_ROOT_LEVEL` to change the log level for Pulsar. Defaults to \"WARN\"."
  },
  {
    "path": "docker/test-images/zipkin-rabbitmq/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Use latest from https://hub.docker.com/_/rabbitmq/tags?page=1&name=alpine\nARG rabbitmq_version=3.13.7\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nWORKDIR /docker-bin\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\n# Bootstrap config including a queue setup out-of-band, exported like so:\n# rabbitmqctl export_definitions defs.json\nCOPY docker/test-images/zipkin-rabbitmq/config/* /config/\n\nARG rabbitmq_version\n\nFROM rabbitmq:${rabbitmq_version}-alpine as zipkin-rabbitmq\nLABEL rabbitmq-version=$rabbitmq_version\nLABEL org.opencontainers.image.description=\"RabbitMQ on Alpine Linux\"\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\n\nCOPY --from=scratch /config/* /etc/rabbitmq/\n\nRUN apk add --update --no-cache rabbitmq-c-utils\n\n# Usually, we read env set from pid 1 to get docker-healthcheck parameters.\n# However, rabbitmq-server has to start as root even if permissions are dropped\n# later. So, we expose it in the Dockerfile instead.\nENV HEALTHCHECK_PORT=5672\nENV HEALTHCHECK_KIND=tcp\nEXPOSE 5672\n"
  },
  {
    "path": "docker/test-images/zipkin-rabbitmq/README.md",
    "content": "## zipkin-rabbitmq Docker image\n\nThe `zipkin-rabbitmq` testing image runs `rabbitmq-server` for the\n[RabbitMQ collector](../../../zipkin-collector/rabbitmq) integration.\n\nFor convenience, this includes the \"guest\" user and a default queue named\n\"zipkin\". To add more queues, exec `amqp-declare-queue` in a running container.\n\nTo build `openzipkin/zipkin-rabbitmq:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-rabbitmq/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-rabbitmq:test\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-rabbitmq/config/defs.json",
    "content": "{\n  \"permissions\": [\n    {\n      \"configure\": \".*\",\n      \"read\": \".*\",\n      \"user\": \"guest\",\n      \"vhost\": \"/\",\n      \"write\": \".*\"\n    }\n  ],\n  \"bindings\": [],\n  \"queues\": [\n    {\n      \"arguments\": {},\n      \"auto_delete\": false,\n      \"durable\": false,\n      \"name\": \"zipkin\",\n      \"type\": \"classic\",\n      \"vhost\": \"/\"\n    }\n  ],\n  \"parameters\": [],\n  \"policies\": [],\n  \"vhosts\": [\n    {\n      \"limits\": [],\n      \"metadata\": {\n        \"description\": \"Default virtual host\",\n        \"tags\": []\n      },\n      \"name\": \"/\"\n    }\n  ],\n  \"exchanges\": [],\n  \"global_parameters\": [],\n  \"topic_permissions\": [],\n  \"users\": [\n    {\n      \"hashing_algorithm\": \"rabbit_password_hashing_sha256\",\n      \"limits\": {},\n      \"name\": \"guest\",\n      \"password_hash\": \"s4XuI8xVMHg3To1FSsFSnsQ2vfgMTP0/XJtmh3plzM/u86Es\",\n      \"tags\": [\n        \"administrator\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "docker/test-images/zipkin-rabbitmq/config/rabbitmq.conf",
    "content": "# Does not require management plugin to be enabled.\nloopback_users.guest = false\ndefinitions.import_backend = local_filesystem\ndefinitions.local.path = /etc/rabbitmq/defs.json\n"
  },
  {
    "path": "docker/test-images/zipkin-ui/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/alpine\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG alpine_version=3.21.3\n\n# java_version is used during the installation process to build or download the zipkin-lens jar.\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/ /build-bin/\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-ui/start-nginx /docker-bin/\nCOPY docker/test-images/zipkin-ui/nginx.conf /conf.d/zipkin.conf.template\nCOPY . /code/\n\n# This version is only used during the install process. Try to be consistent as it reduces layers,\n# which reduces downloads.\nFROM ghcr.io/openzipkin/java:${java_version} as install\n\nCOPY --from=scratch /build-bin/ /build-bin/\n\nWORKDIR /code\n# Conditions aren't supported in Dockerfile instructions, so we copy source even if it isn't used.\nCOPY --from=scratch /code/ .\n\nWORKDIR /install\n\n# When true, build-bin/maven/unjar searches /code for the artifact instead of resolving remotely.\n# /code contains what is allowed in .dockerignore. On problem, ensure .dockerignore is correct.\nARG release_from_maven_build=false\nENV RELEASE_FROM_MAVEN_BUILD=$release_from_maven_build\n# Version of the artifact to unjar. Ex. \"2.4.5\" or \"2.4.5-SNAPSHOT\" \"master\" to use the pom version.\nARG version=master\nENV VERSION=$version\nENV MAVEN_PROJECT_BASEDIR=/code\nRUN if [ \"${RELEASE_FROM_MAVEN_BUILD}\" == \"false\" ]; then /build-bin/maybe_install_npm; fi; \\\n    /build-bin/maven/maven_build_or_unjar io.zipkin zipkin-lens ${VERSION}\n\nFROM ghcr.io/openzipkin/alpine:$alpine_version as zipkin-ui\nLABEL org.opencontainers.image.description=\"NGINX on Alpine Linux hosting the Zipkin UI with Zipkin API proxy_pass\"\n# Use latest from https://pkgs.alpinelinux.org/packages?name=nginx\nARG nginx_version=1.28.2-r2\nLABEL nginx-version=$nginx_version\n\nENV ZIPKIN_BASE_URL=http://zipkin:9411\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-nginx\"]\n\n# Add content and setup NGINX\nCOPY --from=install /install/zipkin-lens/* /var/www/html/zipkin/\nCOPY --from=scratch /conf.d/ /etc/nginx/conf.d/\nRUN apk add --update --no-cache nginx=~${nginx_version} && \\\n    mkdir -p /var/tmp/nginx && chown -R nginx:nginx /var/tmp/nginx\n\n# Usually, we read env set from pid 1 to get docker-healthcheck parameters. However, NGINX wipes the\n# env, so we need to expose it in the Dockerfile instead.\nENV HEALTHCHECK_PORT=80\nEXPOSE 80\n"
  },
  {
    "path": "docker/test-images/zipkin-ui/README.md",
    "content": "## zipkin-ui Docker image\n\nThe `zipkin-ui` testing image contains the static parts of the Zipkin UI served directly with NGINX.\n\nBesides norms defined in [docker-alpine](https://github.com/openzipkin/docker-alpine), this accepts the\nfollowing environment variables:\n\n* `ZIPKIN_BASE_URL`: The proxied zipkin base URL. Defaults to http://zipkin:9411\n\nTo build `openzipkin/zipkin-ui:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-ui/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-ui:test\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-ui/nginx.conf",
    "content": "user  nginx nginx;\nworker_processes  2;\n\nerror_log  /dev/stdout warn;\npid        /var/run/nginx.pid;\n\ndaemon off;\n\nevents {\n    worker_connections  1024;\n}\n\n\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  /dev/stdout  main;\n\n    sendfile        on;\n    #tcp_nopush     on;\n\n    keepalive_timeout  65;\n\n    gzip  on;\n    gzip_types    application/javascript application/json text/css;\n\n    server_tokens off;\n\n    types {\n        application/font-woff2  woff2;\n    }\n\n    server {\n        listen  80;\n\n        root /var/www/html;\n\n        index index.html;\n\n        # Make site accessible from http://set-ip-address.xip.io\n        server_name localhost;\n\n        charset utf-8;\n\n        # redirect root as UI is hosted under /zipkin\n        location / {\n           return 302 /zipkin/;\n        }\n\n        # the entrypoint of the app will expire every day.\n        # this includes links to js assets with random names.\n        location /zipkin/index.html {\n            expires 1d;\n        }\n\n        location /zipkin {\n            try_files $uri /zipkin/index.html = 404;\n        }\n\n        # Pass through health check to the proxy for our docker HEALTHCHECK\n        location /health {\n            expires 1s;\n            access_log off;\n            error_log off;\n            # start-nginx overrides the base url to $ZIPKIN_UI_BASEPATH\n            proxy_pass http://localhost:9411;\n        }\n\n        # accept UI config from the server\n        location /zipkin/config.json {\n            expires 10m;\n            # start-nginx overrides the base url to $ZIPKIN_BASE_URL\n            proxy_pass http://localhost:9411;\n        }\n\n        # the UI looks for the api under the same relative path\n        location /zipkin/api {\n            expires off;\n            # start-nginx overrides the base url to $ZIPKIN_BASE_URL\n            proxy_pass http://localhost:9411;\n        }\n\n        # due to minification, the js assets will change names.\n        # this makes them safe to cache longer\n        location ~* \\.(?:ico|css|js|gif|jpe?g|png)$ {\n            expires 1y;\n            add_header Cache-Control \"public\";\n        }\n\n        location = /favicon.ico { log_not_found off; access_log off; }\n        location = /robots.txt  { access_log off; log_not_found off; }\n\n        # Deny .htaccess file access\n        location ~ /\\.ht {\n            deny all;\n        }\n\n    }\n}\n"
  },
  {
    "path": "docker/test-images/zipkin-ui/start-nginx",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nsed \"s~proxy_pass http://localhost:9411~proxy_pass ${ZIPKIN_BASE_URL}~g\" \\\n /etc/nginx/conf.d/zipkin.conf.template > /etc/nginx/nginx.conf\n\necho Starting NGINX\nexec nginx \"$@\"\n"
  },
  {
    "path": "docker/test-images/zipkin-uiproxy/Dockerfile",
    "content": "#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/alpine\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG alpine_version=3.21.3\n\n# java_version is used during the installation process to build or download the zipkin-lens jar.\n#\n# Use latest version here: https://github.com/orgs/openzipkin/packages/container/package/java\n# This is defined in many places because Docker has no \"env\" script functionality unless you use\n# docker-compose: When updating, update everywhere.\nARG java_version=21.0.7_p6\n\n# We copy files from the context into a scratch container first to avoid a problem where docker and\n# docker compose don't share layer hashes https://github.com/docker/compose/issues/883 normally.\n# COPY --from= works around the issue.\nFROM scratch as scratch\n\nCOPY build-bin/ /build-bin/\nCOPY build-bin/docker/docker-healthcheck /docker-bin/\nCOPY docker/test-images/zipkin-uiproxy/start-nginx /docker-bin/\nCOPY docker/test-images/zipkin-uiproxy/nginx.conf /conf.d/zipkin.conf.template\n\nFROM ghcr.io/openzipkin/alpine:$alpine_version as zipkin-uiproxy\nLABEL org.opencontainers.image.description=\"NGINX on Alpine Linux proxying the Zipkin UI with proxy_pass\"\n# Use latest from https://pkgs.alpinelinux.org/packages?name=nginx\nARG nginx_version=1.28.2-r2\nLABEL nginx-version=$nginx_version\n\nENV ZIPKIN_UI_BASEPATH=/zipkin\nENV ZIPKIN_BASE_URL=http://zipkin:9411\n\n# Add HEALTHCHECK and ENTRYPOINT scripts into the default search path\nCOPY --from=scratch /docker-bin/* /usr/local/bin/\n# We use start period of 30s to avoid marking the container unhealthy on slow or contended CI hosts\nHEALTHCHECK --interval=1s --start-period=30s --timeout=5s CMD [\"docker-healthcheck\"]\nENTRYPOINT [\"start-nginx\"]\n\n# Setup NGINX\nCOPY --from=scratch /conf.d/ /etc/nginx/conf.d/\nRUN apk add --update --no-cache nginx=~${nginx_version} && \\\n    mkdir -p /var/tmp/nginx && chown -R nginx:nginx /var/tmp/nginx\n\n# Usually, we read env set from pid 1 to get docker-healthcheck parameters. However, NGINX wipes the\n# env, so we need to expose it in the Dockerfile instead.\nENV HEALTHCHECK_PORT=80\nEXPOSE 80\n"
  },
  {
    "path": "docker/test-images/zipkin-uiproxy/README.md",
    "content": "## zipkin-uiproxy Docker image\n\nThe `zipkin-uiproxy` testing image proxies the Zipkin UI with NGINX.\n\nBesides norms defined in [docker-alpine](https://github.com/openzipkin/docker-alpine), this accepts the\nfollowing environment variables:\n\n* `ZIPKIN_UI_BASEPATH`: The path this proxy serves the UI under. Defaults to /zipkin\n* `ZIPKIN_BASE_URL`: The proxied zipkin base URL. Defaults to http://zipkin:9411\n\nTo build `openzipkin/zipkin-ui:test`, from the top-level of the repository, run:\n```bash\n$ DOCKER_FILE=docker/test-images/zipkin-uiproxy/Dockerfile build-bin/docker/docker_build openzipkin/zipkin-uiproxy:test\n```\n"
  },
  {
    "path": "docker/test-images/zipkin-uiproxy/nginx.conf",
    "content": "user  nginx nginx;\nworker_processes  2;\n\nerror_log  /dev/stdout warn;\npid        /var/run/nginx.pid;\n\ndaemon off;\n\nevents {\n    worker_connections  1024;\n}\n\n\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  /dev/stdout  main;\n\n    sendfile        on;\n\n    keepalive_timeout  65;\n\n    gzip  on;\n    gzip_types    application/javascript application/json text/css;\n\n    server_tokens off;\n\n    server {\n        listen  80;\n\n        root /var/www/html;\n\n        index index.html;\n\n        # Make site accessible from http://set-ip-address.xip.io\n        server_name localhost;\n\n        charset utf-8;\n\n        # start-nginx overrides /zipkin to $ZIPKIN_UI_BASEPATH\n        location /zipkin {\n            # start-nginx overrides the base url to $ZIPKIN_BASE_URL\n            proxy_pass http://localhost:9411/zipkin;\n            proxy_redirect default;\n        }\n\n        # Pass through health check to the proxy for our docker HEALTHCHECK\n        location /health {\n            expires 1s;\n            access_log off;\n            error_log off;\n            # start-nginx overrides the base url to $ZIPKIN_BASE_URL\n            proxy_pass http://localhost:9411;\n        }\n\n\n        location = /favicon.ico { log_not_found off; access_log off; }\n        location = /robots.txt  { access_log off; log_not_found off; }\n\n        # Deny .htaccess file access\n        location ~ /\\.ht {\n            deny all;\n        }\n\n    }\n}\n"
  },
  {
    "path": "docker/test-images/zipkin-uiproxy/start-nginx",
    "content": "#!/bin/sh\n#\n# Copyright The OpenZipkin Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n\nsed -e \"s~proxy_pass http://localhost:9411~proxy_pass ${ZIPKIN_BASE_URL}~g\" \\\n -e \"s~location /zipkin~location ${ZIPKIN_UI_BASEPATH}~g\" \\\n /etc/nginx/conf.d/zipkin.conf.template > /etc/nginx/nginx.conf\n\necho Starting NGINX\nexec nginx \"$@\"\n"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.3.2\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ]; then\n\n  if [ -f /usr/local/etc/mavenrc ]; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ]; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ]; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false\ndarwin=false\nmingw=false\ncase \"$(uname)\" in\nCYGWIN*) cygwin=true ;;\nMINGW*) mingw=true ;;\nDarwin*)\n  darwin=true\n  # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n  # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n  if [ -z \"$JAVA_HOME\" ]; then\n    if [ -x \"/usr/libexec/java_home\" ]; then\n      JAVA_HOME=\"$(/usr/libexec/java_home)\"\n      export JAVA_HOME\n    else\n      JAVA_HOME=\"/Library/Java/Home\"\n      export JAVA_HOME\n    fi\n  fi\n  ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ]; then\n  if [ -r /etc/gentoo-release ]; then\n    JAVA_HOME=$(java-config --jre-home)\n  fi\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin; then\n  [ -n \"$JAVA_HOME\" ] \\\n    && JAVA_HOME=$(cygpath --unix \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] \\\n    && CLASSPATH=$(cygpath --path --unix \"$CLASSPATH\")\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw; then\n  [ -n \"$JAVA_HOME\" ] && [ -d \"$JAVA_HOME\" ] \\\n    && JAVA_HOME=\"$(\n      cd \"$JAVA_HOME\" || (\n        echo \"cannot cd into $JAVA_HOME.\" >&2\n        exit 1\n      )\n      pwd\n    )\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"$(which javac)\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"$(expr \"$javaExecutable\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=$(which readlink)\n    if [ ! \"$(expr \"$readLink\" : '\\([^ ]*\\)')\" = \"no\" ]; then\n      if $darwin; then\n        javaHome=\"$(dirname \"$javaExecutable\")\"\n        javaExecutable=\"$(cd \"$javaHome\" && pwd -P)/javac\"\n      else\n        javaExecutable=\"$(readlink -f \"$javaExecutable\")\"\n      fi\n      javaHome=\"$(dirname \"$javaExecutable\")\"\n      javaHome=$(expr \"$javaHome\" : '\\(.*\\)/bin')\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ]; then\n  if [ -n \"$JAVA_HOME\" ]; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"$(\n      \\unset -f command 2>/dev/null\n      \\command -v java\n    )\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ]; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\" >&2\nfi\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n  if [ -z \"$1\" ]; then\n    echo \"Path not specified to find_maven_basedir\" >&2\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ]; do\n    if [ -d \"$wdir\"/.mvn ]; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=$(\n        cd \"$wdir/..\" || exit 1\n        pwd\n      )\n    fi\n    # end of workaround\n  done\n  printf '%s' \"$(\n    cd \"$basedir\" || exit 1\n    pwd\n  )\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    # Remove \\r in case we run on Windows within Git Bash\n    # and check out the repository with auto CRLF management\n    # enabled. Otherwise, we may read lines that are delimited with\n    # \\r\\n and produce $'-Xarg\\r' rather than -Xarg due to word\n    # splitting rules.\n    tr -s '\\r\\n' ' ' <\"$1\"\n  fi\n}\n\nlog() {\n  if [ \"$MVNW_VERBOSE\" = true ]; then\n    printf '%s\\n' \"$1\"\n  fi\n}\n\nBASE_DIR=$(find_maven_basedir \"$(dirname \"$0\")\")\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1\nfi\n\nMAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nexport MAVEN_PROJECTBASEDIR\nlog \"$MAVEN_PROJECTBASEDIR\"\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nwrapperJarPath=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\"\nif [ -r \"$wrapperJarPath\" ]; then\n  log \"Found $wrapperJarPath\"\nelse\n  log \"Couldn't find $wrapperJarPath, downloading it ...\"\n\n  if [ -n \"$MVNW_REPOURL\" ]; then\n    wrapperUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar\"\n  else\n    wrapperUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar\"\n  fi\n  while IFS=\"=\" read -r key value; do\n    # Remove '\\r' from value to allow usage on windows as IFS does not consider '\\r' as a separator ( considers space, tab, new line ('\\n'), and custom '=' )\n    safeValue=$(echo \"$value\" | tr -d '\\r')\n    case \"$key\" in wrapperUrl)\n      wrapperUrl=\"$safeValue\"\n      break\n      ;;\n    esac\n  done <\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\n  log \"Downloading from: $wrapperUrl\"\n\n  if $cygwin; then\n    wrapperJarPath=$(cygpath --path --windows \"$wrapperJarPath\")\n  fi\n\n  if command -v wget >/dev/null; then\n    log \"Found wget ... using wget\"\n    [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--quiet\"\n    if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n      wget $QUIET \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n    else\n      wget $QUIET --http-user=\"$MVNW_USERNAME\" --http-password=\"$MVNW_PASSWORD\" \"$wrapperUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n    fi\n  elif command -v curl >/dev/null; then\n    log \"Found curl ... using curl\"\n    [ \"$MVNW_VERBOSE\" = true ] && QUIET=\"\" || QUIET=\"--silent\"\n    if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n      curl $QUIET -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n    else\n      curl $QUIET --user \"$MVNW_USERNAME:$MVNW_PASSWORD\" -o \"$wrapperJarPath\" \"$wrapperUrl\" -f -L || rm -f \"$wrapperJarPath\"\n    fi\n  else\n    log \"Falling back to using Java to download\"\n    javaSource=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n    javaClass=\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class\"\n    # For Cygwin, switch paths to Windows format before running javac\n    if $cygwin; then\n      javaSource=$(cygpath --path --windows \"$javaSource\")\n      javaClass=$(cygpath --path --windows \"$javaClass\")\n    fi\n    if [ -e \"$javaSource\" ]; then\n      if [ ! -e \"$javaClass\" ]; then\n        log \" - Compiling MavenWrapperDownloader.java ...\"\n        (\"$JAVA_HOME/bin/javac\" \"$javaSource\")\n      fi\n      if [ -e \"$javaClass\" ]; then\n        log \" - Running MavenWrapperDownloader.java ...\"\n        (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$wrapperUrl\" \"$wrapperJarPath\") || rm -f \"$wrapperJarPath\"\n      fi\n    fi\n  fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\n# If specified, validate the SHA-256 sum of the Maven wrapper jar file\nwrapperSha256Sum=\"\"\nwhile IFS=\"=\" read -r key value; do\n  case \"$key\" in wrapperSha256Sum)\n    wrapperSha256Sum=$value\n    break\n    ;;\n  esac\ndone <\"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties\"\nif [ -n \"$wrapperSha256Sum\" ]; then\n  wrapperSha256Result=false\n  if command -v sha256sum >/dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | sha256sum -c >/dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  elif command -v shasum >/dev/null; then\n    if echo \"$wrapperSha256Sum  $wrapperJarPath\" | shasum -a 256 -c >/dev/null 2>&1; then\n      wrapperSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\" >&2\n    echo \"Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  fi\n  if [ $wrapperSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.\" >&2\n    echo \"Investigate or delete $wrapperJarPath to attempt a clean download.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified wrapperSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$JAVA_HOME\" ] \\\n    && JAVA_HOME=$(cygpath --path --windows \"$JAVA_HOME\")\n  [ -n \"$CLASSPATH\" ] \\\n    && CLASSPATH=$(cygpath --path --windows \"$CLASSPATH\")\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] \\\n    && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\")\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $*\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\n# shellcheck disable=SC2086 # safe args\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\r\n@REM Licensed to the Apache Software Foundation (ASF) under one\r\n@REM or more contributor license agreements.  See the NOTICE file\r\n@REM distributed with this work for additional information\r\n@REM regarding copyright ownership.  The ASF licenses this file\r\n@REM to you under the Apache License, Version 2.0 (the\r\n@REM \"License\"); you may not use this file except in compliance\r\n@REM with the License.  You may obtain a copy of the License at\r\n@REM\r\n@REM    http://www.apache.org/licenses/LICENSE-2.0\r\n@REM\r\n@REM Unless required by applicable law or agreed to in writing,\r\n@REM software distributed under the License is distributed on an\r\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n@REM KIND, either express or implied.  See the License for the\r\n@REM specific language governing permissions and limitations\r\n@REM under the License.\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@REM ----------------------------------------------------------------------------\r\n@REM Apache Maven Wrapper startup batch script, version 3.3.2\r\n@REM\r\n@REM Required ENV vars:\r\n@REM JAVA_HOME - location of a JDK home dir\r\n@REM\r\n@REM Optional ENV vars\r\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\r\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\r\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\r\n@REM     e.g. to debug Maven itself, use\r\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\r\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\r\n@echo off\r\n@REM set title of command window\r\ntitle %0\r\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\r\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\r\n\r\n@REM set %HOME% to equivalent of $HOME\r\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\r\n\r\n@REM Execute a user defined script before this one\r\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\r\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\r\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\r\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\r\n:skipRcPre\r\n\r\n@setlocal\r\n\r\nset ERROR_CODE=0\r\n\r\n@REM To isolate internal variables from possible post scripts, we use another setlocal\r\n@setlocal\r\n\r\n@REM ==== START VALIDATION ====\r\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\r\n\r\necho. >&2\r\necho Error: JAVA_HOME not found in your environment. >&2\r\necho Please set the JAVA_HOME variable in your environment to match the >&2\r\necho location of your Java installation. >&2\r\necho. >&2\r\ngoto error\r\n\r\n:OkJHome\r\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\r\n\r\necho. >&2\r\necho Error: JAVA_HOME is set to an invalid directory. >&2\r\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\r\necho Please set the JAVA_HOME variable in your environment to match the >&2\r\necho location of your Java installation. >&2\r\necho. >&2\r\ngoto error\r\n\r\n@REM ==== END VALIDATION ====\r\n\r\n:init\r\n\r\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\r\n@REM Fallback to current working directory if not found.\r\n\r\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\r\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\r\n\r\nset EXEC_DIR=%CD%\r\nset WDIR=%EXEC_DIR%\r\n:findBaseDir\r\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\r\ncd ..\r\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\r\nset WDIR=%CD%\r\ngoto findBaseDir\r\n\r\n:baseDirFound\r\nset MAVEN_PROJECTBASEDIR=%WDIR%\r\ncd \"%EXEC_DIR%\"\r\ngoto endDetectBaseDir\r\n\r\n:baseDirNotFound\r\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\r\ncd \"%EXEC_DIR%\"\r\n\r\n:endDetectBaseDir\r\n\r\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\r\n\r\n@setlocal EnableExtensions EnableDelayedExpansion\r\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\r\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\r\n\r\n:endReadAdditionalConfig\r\n\r\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\r\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\r\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\r\n\r\nset WRAPPER_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar\"\r\n\r\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\r\n    IF \"%%A\"==\"wrapperUrl\" SET WRAPPER_URL=%%B\r\n)\r\n\r\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\r\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\r\nif exist %WRAPPER_JAR% (\r\n    if \"%MVNW_VERBOSE%\" == \"true\" (\r\n        echo Found %WRAPPER_JAR%\r\n    )\r\n) else (\r\n    if not \"%MVNW_REPOURL%\" == \"\" (\r\n        SET WRAPPER_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar\"\r\n    )\r\n    if \"%MVNW_VERBOSE%\" == \"true\" (\r\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\r\n        echo Downloading from: %WRAPPER_URL%\r\n    )\r\n\r\n    powershell -Command \"&{\"^\r\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\r\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\r\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\r\n\t\t\"}\"^\r\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')\"^\r\n\t\t\"}\"\r\n    if \"%MVNW_VERBOSE%\" == \"true\" (\r\n        echo Finished downloading %WRAPPER_JAR%\r\n    )\r\n)\r\n@REM End of extension\r\n\r\n@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file\r\nSET WRAPPER_SHA_256_SUM=\"\"\r\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\r\n    IF \"%%A\"==\"wrapperSha256Sum\" SET WRAPPER_SHA_256_SUM=%%B\r\n)\r\nIF NOT %WRAPPER_SHA_256_SUM%==\"\" (\r\n    powershell -Command \"&{\"^\r\n       \"Import-Module $PSHOME\\Modules\\Microsoft.PowerShell.Utility -Function Get-FileHash;\"^\r\n       \"$hash = (Get-FileHash \\\"%WRAPPER_JAR%\\\" -Algorithm SHA256).Hash.ToLower();\"^\r\n       \"If('%WRAPPER_SHA_256_SUM%' -ne $hash){\"^\r\n       \"  Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';\"^\r\n       \"  Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';\"^\r\n       \"  Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';\"^\r\n       \"  exit 1;\"^\r\n       \"}\"^\r\n       \"}\"\r\n    if ERRORLEVEL 1 goto error\r\n)\r\n\r\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\r\n@REM work with both Windows and non-Windows executions.\r\nset MAVEN_CMD_LINE_ARGS=%*\r\n\r\n%MAVEN_JAVA_EXE% ^\r\n  %JVM_CONFIG_MAVEN_PROPS% ^\r\n  %MAVEN_OPTS% ^\r\n  %MAVEN_DEBUG_OPTS% ^\r\n  -classpath %WRAPPER_JAR% ^\r\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\r\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\r\nif ERRORLEVEL 1 goto error\r\ngoto end\r\n\r\n:error\r\nset ERROR_CODE=1\r\n\r\n:end\r\n@endlocal & set ERROR_CODE=%ERROR_CODE%\r\n\r\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\r\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\r\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\r\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\r\n:skipRcPost\r\n\r\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\r\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\r\n\r\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\r\n\r\ncmd /C exit /B %ERROR_CODE%\r\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>io.zipkin</groupId>\n  <artifactId>zipkin-parent</artifactId>\n  <version>3.6.0-SNAPSHOT</version>\n  <packaging>pom</packaging>\n\n  <modules>\n    <module>zipkin</module>\n    <module>zipkin-tests</module>\n    <module>zipkin-junit5</module>\n    <module>zipkin-storage</module>\n    <module>zipkin-collector</module>\n    <module>zipkin-server</module>\n  </modules>\n\n  <properties>\n    <main.basedir>${project.basedir}</main.basedir>\n\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>\n    <project.build.outputEncoding>UTF-8</project.build.outputEncoding>\n    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\n    <!-- Except for zipkin core Jar (Java 1.8), everything is 17, as that's\n         the minimum of Spring Boot 3, required for recent JOOQ and works with\n         zipkin-dependencies (Spark 3.4+). -->\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <maven.compiler.release>17</maven.compiler.release>\n    <maven.compiler.testSource>17</maven.compiler.testSource>\n    <maven.compiler.testTarget>17</maven.compiler.testTarget>\n    <maven.compiler.testRelease>17</maven.compiler.testRelease>\n\n    <!-- override to set exclusions per-project -->\n    <errorprone.args />\n    <errorprone.version>2.42.0</errorprone.version>\n\n    <zipkin-proto3.version>1.0.0</zipkin-proto3.version>\n\n    <armeria.groupId>com.linecorp.armeria</armeria.groupId>\n    <armeria.version>1.37.0</armeria.version>\n    <!-- Match Armeria version to avoid conflicts including running tests in the IDE -->\n    <netty.version>4.2.10.Final</netty.version>\n\n    <!-- It's easy for Jackson dependencies to get misaligned, so we manage it ourselves. -->\n    <jackson.version>2.21.1</jackson.version>\n\n    <java-driver.version>4.19.0</java-driver.version>\n    <micrometer.version>1.16.4</micrometer.version>\n\n    <!-- Used for Generated annotations -->\n    <javax-annotation-api.version>1.3.2</javax-annotation-api.version>\n\n    <!-- update together -->\n    <spring-boot.version>3.5.11</spring-boot.version>\n    <spring.version>6.2.17</spring.version>\n\n    <!-- MySQL connector is GPL, even if it has an OSS exception.\n         https://www.mysql.com/about/legal/licensing/foss-exception/\n\n         MariaDB has a friendlier license, LGPL, which is less scary in audits.\n    -->\n    <mariadb-java-client.version>3.5.3</mariadb-java-client.version>\n    <HikariCP.version>6.2.1</HikariCP.version>\n    <slf4j.version>2.0.17</slf4j.version>\n    <auto-value.version>1.11.0</auto-value.version>\n    <git-commit-id.version>4.9.10</git-commit-id.version>\n\n    <!-- Test only dependencies -->\n    <junit-jupiter.version>5.13.4</junit-jupiter.version>\n    <junit-platform-laucher.version>1.13.4</junit-platform-laucher.version>\n    <mockito.version>5.21.0</mockito.version>\n    <assertj.version>3.27.7</assertj.version>\n    <awaitility.version>4.3.0</awaitility.version>\n    <testcontainers.version>1.21.4</testcontainers.version>\n    <okhttp.version>4.12.0</okhttp.version>\n    <kryo.version>5.6.2</kryo.version>\n    <!-- Only used for proto interop testing; wire-maven-plugin is usually behind latest. -->\n    <wire.version>5.1.0</wire.version>\n    <gson.version>2.13.1</gson.version>\n    <unpack-proto.directory>${project.build.directory}/test/proto</unpack-proto.directory>\n\n    <license.skip>${skipTests}</license.skip>\n\n    <build-helper-maven-plugin.version>3.6.0</build-helper-maven-plugin.version>\n    <go-offline-maven-plugin.version>1.2.8</go-offline-maven-plugin.version>\n    <!-- TODO: cleanup any redundant ignores now also in the 4.0 release (once final) -->\n    <license-maven-plugin.version>5.0.0</license-maven-plugin.version>\n    <maven-assembly-plugin.version>3.7.1</maven-assembly-plugin.version>\n    <maven-bundle-plugin.version>6.0.0</maven-bundle-plugin.version>\n    <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>\n    <!-- Use same version as https://github.com/openzipkin/docker-java -->\n    <maven-dependency-plugin.version>3.8.1</maven-dependency-plugin.version>\n    <maven-deploy-plugin.version>3.1.2</maven-deploy-plugin.version>\n    <maven-enforcer-plugin.version>3.5.0</maven-enforcer-plugin.version>\n    <!-- Use same version as https://github.com/openzipkin/docker-java -->\n    <maven-help-plugin.version>3.5.1</maven-help-plugin.version>\n    <maven-install-plugin.version>3.1.3</maven-install-plugin.version>\n    <maven-javadoc-plugin.version>3.11.2</maven-javadoc-plugin.version>\n    <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>\n    <maven-release-plugin.version>3.1.1</maven-release-plugin.version>\n    <maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>\n    <maven-source-plugin.version>3.3.1</maven-source-plugin.version>\n    <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>\n    <nexus-staging-maven-plugin.version>1.7.0</nexus-staging-maven-plugin.version>\n    <wire-maven-plugin.version>1.3</wire-maven-plugin.version>\n  </properties>\n\n  <name>Zipkin (Parent)</name>\n  <description>Zipkin (Parent)</description>\n  <url>https://github.com/openzipkin/zipkin</url>\n  <inceptionYear>2015</inceptionYear>\n\n  <organization>\n    <name>OpenZipkin</name>\n    <url>https://zipkin.io/</url>\n  </organization>\n\n  <licenses>\n    <license>\n      <name>The Apache Software License, Version 2.0</name>\n      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>\n      <distribution>repo</distribution>\n    </license>\n  </licenses>\n\n  <scm>\n    <url>https://github.com/openzipkin/zipkin</url>\n    <connection>scm:git:https://github.com/openzipkin/zipkin.git</connection>\n    <developerConnection>scm:git:https://github.com/openzipkin/zipkin.git</developerConnection>\n    <tag>HEAD</tag>\n  </scm>\n\n  <!-- Developer section is needed for Maven Central, but doesn't need to include each person -->\n  <developers>\n    <developer>\n      <id>openzipkin</id>\n      <name>OpenZipkin Gitter</name>\n      <url>https://gitter.im/openzipkin/zipkin</url>\n    </developer>\n  </developers>\n\n  <distributionManagement>\n    <snapshotRepository>\n      <id>ossrh</id>\n      <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n    </snapshotRepository>\n    <repository>\n      <id>ossrh</id>\n      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n    </repository>\n  </distributionManagement>\n\n  <issueManagement>\n    <system>Github</system>\n    <url>https://github.com/openzipkin/zipkin/issues</url>\n  </issueManagement>\n\n  <dependencyManagement>\n    <!-- Be careful here, especially to not import BOMs as io.zipkin.zipkin2:zipkin has this parent.\n\n         For example, if you imported Netty's BOM here, using Brave would also download that BOM as\n         it depends indirectly on io.zipkin.zipkin2:zipkin. As Brave itself is indirectly used, this\n         can be extremely confusing when people are troubleshooting library version assignments. -->\n  </dependencyManagement>\n\n  <dependencies>\n    <!-- Do not add compile dependencies here. This can cause problems for libraries that depend on\n         io.zipkin.zipkin2:zipkin difficult to unravel. -->\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- needed for surefire.\n         https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html -->\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.platform</groupId>\n      <artifactId>junit-platform-launcher</artifactId>\n      <version>${junit-platform-laucher.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.assertj</groupId>\n      <artifactId>assertj-core</artifactId>\n      <version>${assertj.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.mockito</groupId>\n      <artifactId>mockito-junit-jupiter</artifactId>\n      <version>${mockito.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <pluginManagement>\n      <plugins>\n        <!-- mvn de.qaware.maven:go-offline-maven-plugin:resolve-dependencies -->\n        <plugin>\n          <groupId>de.qaware.maven</groupId>\n          <artifactId>go-offline-maven-plugin</artifactId>\n          <version>${go-offline-maven-plugin.version}</version>\n          <configuration>\n            <!-- Add dependencies indirectly referenced by build plugins -->\n            <dynamicDependencies>\n              <DynamicDependency>\n                <groupId>com.mycila</groupId>\n                <artifactId>license-maven-plugin-git</artifactId>\n                <version>${license-maven-plugin.version}</version>\n                <repositoryType>MAIN</repositoryType>\n              </DynamicDependency>\n              <DynamicDependency>\n                <groupId>com.google.errorprone</groupId>\n                <artifactId>error_prone_core</artifactId>\n                <version>${errorprone.version}</version>\n                <repositoryType>MAIN</repositoryType>\n              </DynamicDependency>\n              <DynamicDependency>\n                <groupId>org.apache.maven.surefire</groupId>\n                <artifactId>surefire-junit-platform</artifactId>\n                <version>${maven-surefire-plugin.version}</version>\n                <repositoryType>PLUGIN</repositoryType>\n              </DynamicDependency>\n            </dynamicDependencies>\n          </configuration>\n        </plugin>\n\n        <plugin>\n          <artifactId>maven-compiler-plugin</artifactId>\n          <version>${maven-compiler-plugin.version}</version>\n          <inherited>true</inherited>\n          <configuration>\n            <fork>true</fork>\n            <showWarnings>true</showWarnings>\n          </configuration>\n        </plugin>\n\n        <!-- Uploads occur as a last step (which also adds checksums) -->\n        <plugin>\n          <artifactId>maven-deploy-plugin</artifactId>\n          <version>${maven-deploy-plugin.version}</version>\n        </plugin>\n\n        <plugin>\n          <artifactId>maven-install-plugin</artifactId>\n          <version>${maven-install-plugin.version}</version>\n        </plugin>\n\n        <plugin>\n          <artifactId>maven-jar-plugin</artifactId>\n          <version>${maven-jar-plugin.version}</version>\n          <configuration>\n            <archive>\n              <!-- prevents huge pom file from also being added to the jar under META-INF/maven -->\n              <addMavenDescriptor>false</addMavenDescriptor>\n            </archive>\n          </configuration>\n        </plugin>\n\n        <plugin>\n          <artifactId>maven-release-plugin</artifactId>\n          <version>${maven-release-plugin.version}</version>\n          <configuration>\n            <useReleaseProfile>false</useReleaseProfile>\n            <releaseProfiles>release</releaseProfiles>\n            <autoVersionSubmodules>true</autoVersionSubmodules>\n            <tagNameFormat>@{project.version}</tagNameFormat>\n          </configuration>\n        </plugin>\n\n        <plugin>\n          <groupId>org.sonatype.plugins</groupId>\n          <artifactId>nexus-staging-maven-plugin</artifactId>\n          <version>${nexus-staging-maven-plugin.version}</version>\n        </plugin>\n\n        <plugin>\n          <groupId>org.eclipse.m2e</groupId>\n          <artifactId>lifecycle-mapping</artifactId>\n          <version>1.0.0</version>\n          <configuration>\n            <lifecycleMappingMetadata>\n              <pluginExecutions>\n                <pluginExecution>\n                  <pluginExecutionFilter>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <versionRange>[3.7,)</versionRange>\n                    <goals>\n                      <goal>compile</goal>\n                      <goal>testCompile</goal>\n                    </goals>\n                  </pluginExecutionFilter>\n                  <action>\n                    <configurator>\n                      <id>org.eclipse.m2e.jdt.javaConfigurator</id>\n                    </configurator>\n                  </action>\n                </pluginExecution>\n              </pluginExecutions>\n            </lifecycleMappingMetadata>\n          </configuration>\n        </plugin>\n        <plugin>\n          <artifactId>maven-eclipse-plugin</artifactId>\n          <version>2.10</version>\n          <configuration>\n            <downloadSources>true</downloadSources>\n            <downloadJavadocs>true</downloadJavadocs>\n          </configuration>\n        </plugin>\n\n        <plugin>\n          <artifactId>maven-shade-plugin</artifactId>\n          <version>${maven-shade-plugin.version}</version>\n        </plugin>\n\n        <!-- The below plugins compile protobuf stubs in the indicated source tree -->\n        <plugin>\n          <artifactId>maven-dependency-plugin</artifactId>\n          <version>${maven-dependency-plugin.version}</version>\n          <executions>\n            <!-- wire-maven-plugin cannot get proto definitions from dependencies: this will -->\n            <execution>\n              <id>unpack-proto</id>\n              <phase>generate-sources</phase>\n              <goals>\n                <goal>unpack-dependencies</goal>\n              </goals>\n              <configuration>\n                <includeArtifactIds>zipkin-proto3</includeArtifactIds>\n                <includes>**/*.proto</includes>\n                <outputDirectory>${unpack-proto.directory}</outputDirectory>\n              </configuration>\n            </execution>\n          </executions>\n        </plugin>\n        <plugin>\n          <!-- com.squareup.wire version was abandoned -->\n          <groupId>de.m3y.maven</groupId>\n          <artifactId>wire-maven-plugin</artifactId>\n          <version>${wire-maven-plugin.version}</version>\n          <executions>\n            <execution>\n              <phase>generate-sources</phase>\n              <goals>\n                <goal>generate-sources</goal>\n              </goals>\n              <configuration>\n                <protoSourceDirectory>${unpack-proto.directory}</protoSourceDirectory>\n                <includes>\n                  <include>zipkin.proto3.*</include>\n                </includes>\n              </configuration>\n            </execution>\n          </executions>\n        </plugin>\n      </plugins>\n    </pluginManagement>\n\n    <plugins>\n      <!-- Ensure common utility commands use coherent versions (avoid lazy downloads) -->\n      <plugin>\n        <artifactId>maven-dependency-plugin</artifactId>\n        <version>${maven-dependency-plugin.version}</version>\n      </plugin>\n      <plugin>\n        <artifactId>maven-help-plugin</artifactId>\n        <version>${maven-help-plugin.version}</version>\n      </plugin>\n\n      <plugin>\n        <artifactId>maven-surefire-plugin</artifactId>\n        <version>${maven-surefire-plugin.version}</version>\n        <configuration>\n          <!-- Ensures root cause ends up in the console -->\n          <trimStackTrace>false</trimStackTrace>\n        </configuration>\n        <dependencies>\n          <!-- needed for surefire.\n            https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html -->\n          <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>${junit-jupiter.version}</version>\n          </dependency>\n        </dependencies>\n      </plugin>\n\n      <plugin>\n        <artifactId>maven-failsafe-plugin</artifactId>\n        <version>${maven-surefire-plugin.version}</version>\n        <executions>\n          <execution>\n            <id>integration-test</id>\n            <goals>\n              <goal>integration-test</goal>\n            </goals>\n          </execution>\n          <execution>\n            <id>verify</id>\n            <goals>\n              <goal>verify</goal>\n            </goals>\n          </execution>\n        </executions>\n        <configuration>\n          <systemProperties>\n            <!-- Gives better context when there's an exception such as AbortedStreamException.\n                 Set globally as we have failures sometimes in storage-elasticsearch and sometimes\n                 in zipkin-server tests (same code used two places).\n            -->\n            <com.linecorp.armeria.verboseExceptions>always</com.linecorp.armeria.verboseExceptions>\n          </systemProperties>\n          <!-- workaround to SUREFIRE-1831 -->\n          <useModulePath>false</useModulePath>\n          <!-- Ensures root cause ends up in the console -->\n          <trimStackTrace>false</trimStackTrace>\n        </configuration>\n      </plugin>\n\n      <plugin>\n        <artifactId>maven-enforcer-plugin</artifactId>\n        <version>${maven-enforcer-plugin.version}</version>\n        <executions>\n          <execution>\n            <id>enforce-java</id>\n            <goals>\n              <goal>enforce</goal>\n            </goals>\n            <configuration>\n              <rules>\n                <requireJavaVersion>\n                  <!-- Change this to control LTS JDK versions allowed to build\n                       the project. Keep in sync with .github/workflows -->\n                  <version>[17,18),[21,22)</version>\n                </requireJavaVersion>\n              </rules>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n\n      <plugin>\n        <groupId>com.mycila</groupId>\n        <artifactId>license-maven-plugin</artifactId>\n        <version>${license-maven-plugin.version}</version>\n        <configuration>\n          <skip>${license.skip}</skip>\n          <!-- session.executionRootDirectory resolves properly even with nested modules -->\n          <header>${main.basedir}/src/etc/header.txt</header>\n          <mapping>\n            <!-- Don't use javadoc style as this makes code formatters break it by adding tags! -->\n            <java>SLASHSTAR_STYLE</java>\n            <kt>SLASHSTAR_STYLE</kt>\n            <jsx>SLASHSTAR_STYLE</jsx>\n            <ts>SLASHSTAR_STYLE</ts>\n            <tsx>SLASHSTAR_STYLE</tsx>\n            <bnd>SCRIPT_STYLE</bnd>\n            <ejs>XML_STYLE</ejs>\n            <css>SLASHSTAR_STYLE</css>\n            <!-- build-bin non-trivial scripts -->\n            <javadoc_to_gh_pages>SCRIPT_STYLE</javadoc_to_gh_pages>\n            <maybe_install_npm>SCRIPT_STYLE</maybe_install_npm>\n            <!-- build-bin/docker -->\n            <docker_block_on_health>SCRIPT_STYLE</docker_block_on_health>\n            <configure_docker>SCRIPT_STYLE</configure_docker>\n            <configure_docker_push>SCRIPT_STYLE</configure_docker_push>\n            <docker_arch>SCRIPT_STYLE</docker_arch>\n            <docker_args>SCRIPT_STYLE</docker_args>\n            <docker_build>SCRIPT_STYLE</docker_build>\n            <docker_push>SCRIPT_STYLE</docker_push>\n            <docker_test_image>SCRIPT_STYLE</docker_test_image>\n            <!-- build-bin/git -->\n            <login_git>SCRIPT_STYLE</login_git>\n            <version_from_trigger_tag>SCRIPT_STYLE</version_from_trigger_tag>\n            <!-- build-bin/gpg -->\n            <configure_gpg>SCRIPT_STYLE</configure_gpg>\n            <!-- build-bin/maven -->\n            <maven_build>SCRIPT_STYLE</maven_build>\n            <maven_build_or_unjar>SCRIPT_STYLE</maven_build_or_unjar>\n            <maven_deploy>SCRIPT_STYLE</maven_deploy>\n            <maven_go_offline>SCRIPT_STYLE</maven_go_offline>\n            <maven_opts>SCRIPT_STYLE</maven_opts>\n            <maven_release>SCRIPT_STYLE</maven_release>\n            <maven_unjar>SCRIPT_STYLE</maven_unjar>\n            <!-- docker/**/start-* -->\n            <start-activemq>SCRIPT_STYLE</start-activemq>\n            <start-eureka>SCRIPT_STYLE</start-eureka>\n            <start-cassandra>SCRIPT_STYLE</start-cassandra>\n            <start-elasticsearch>SCRIPT_STYLE</start-elasticsearch>\n            <start-kafka-zookeeper>SCRIPT_STYLE</start-kafka-zookeeper>\n            <start-mysql>SCRIPT_STYLE</start-mysql>\n            <start-nginx>SCRIPT_STYLE</start-nginx>\n            <start-zipkin>SCRIPT_STYLE</start-zipkin>\n            <!-- docker/**/docker-healthcheck -->\n            <docker-healthcheck>SCRIPT_STYLE</docker-healthcheck>\n          </mapping>\n          <excludes>\n            <exclude>**/simplelogger.properties</exclude>\n            <exclude>**/continuous-build.yml</exclude>\n            <exclude>**/*.dockerignore</exclude>\n            <exclude>.editorconfig</exclude>\n            <exclude>.gitattributes</exclude>\n            <exclude>.gitignore</exclude>\n            <exclude>.github/**</exclude>\n            <exclude>.mvn/**</exclude>\n            <exclude>mvnw*</exclude>\n            <exclude>etc/header.txt</exclude>\n            <exclude>**/nginx.conf</exclude>\n            <exclude>**/.idea/**</exclude>\n            <exclude>**/node_modules/**</exclude>\n            <exclude>**/build/**</exclude>\n            <exclude>**/dist/**</exclude>\n            <exclude>**/coverage/**</exclude>\n            <exclude>**/.babelrc</exclude>\n            <exclude>**/.bowerrc</exclude>\n            <exclude>**/.editorconfig</exclude>\n            <exclude>**/.env.development</exclude>\n            <exclude>**/.eslintignore</exclude>\n            <exclude>**/.eslintrc</exclude>\n            <exclude>**/.eslintrc</exclude>\n            <exclude>**/.eslintrc.js</exclude>\n            <exclude>**/.linguirc</exclude>\n            <exclude>**/testdata/**/*.json</exclude>\n            <exclude>**/test/data/**/*.json</exclude>\n            <exclude>**/src/translations/**</exclude>\n            <exclude>LICENSE</exclude>\n            <exclude>**/*.md</exclude>\n            <exclude>**/*.bnd</exclude>\n            <exclude>**/src/main/resources/zipkin.txt</exclude>\n            <exclude>**/src/main/resources/*.yml</exclude>\n            <exclude>**/spring.factories</exclude>\n            <!-- Cassandra integration tests break when license headers are present -->\n            <exclude>**/src/main/resources/*.cql</exclude>\n            <exclude>kafka_*/**</exclude>\n            <exclude>**/nohup.out</exclude>\n            <exclude>src/test/resources/**</exclude>\n            <exclude>**/generated/**</exclude>\n            <exclude>.dockerignore</exclude>\n            <!-- trivial build-bin scripts -->\n            <exclude>build-bin/configure_deploy</exclude>\n            <exclude>build-bin/configure_test</exclude>\n            <exclude>build-bin/deploy</exclude>\n            <exclude>build-bin/test</exclude>\n          </excludes>\n          <strictCheck>true</strictCheck>\n        </configuration>\n        <dependencies>\n          <dependency>\n            <groupId>com.mycila</groupId>\n            <artifactId>license-maven-plugin-git</artifactId>\n            <version>${license-maven-plugin.version}</version>\n          </dependency>\n        </dependencies>\n        <executions>\n          <execution>\n            <goals>\n              <goal>check</goal>\n            </goals>\n            <phase>compile</phase>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n\n  <profiles>\n    <profile>\n      <id>include-lens</id>\n      <activation>\n        <property>\n          <name>!skipLens</name>\n        </property>\n      </activation>\n      <modules>\n        <module>zipkin-lens</module>\n      </modules>\n    </profile>\n\n    <!-- -DskipTests ensures benchmarks don't end up in javadocs or in Maven Central -->\n    <profile>\n      <id>include-benchmarks</id>\n      <activation>\n        <property>\n          <name>!skipTests</name>\n        </property>\n      </activation>\n      <modules>\n        <module>benchmarks</module>\n      </modules>\n    </profile>\n\n    <profile>\n      <id>error-prone-17+</id>\n      <activation>\n        <!-- Only LTS versions that work with errorprone -->\n        <jdk>[17,18),[21,22)</jdk>\n      </activation>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <version>${maven-compiler-plugin.version}</version>\n            <inherited>true</inherited>\n            <configuration>\n              <fork>true</fork>\n              <showWarnings>true</showWarnings>\n            </configuration>\n            <executions>\n              <execution>\n                <!-- only use errorprone on main source tree -->\n                <id>default-compile</id>\n                <phase>compile</phase>\n                <goals>\n                  <goal>compile</goal>\n                </goals>\n                <configuration>\n                  <forceJavacCompilerUse>true</forceJavacCompilerUse>\n                  <compilerArgs>\n                    <arg>-XDcompilePolicy=simple</arg>\n                    <arg>--should-stop=ifError=FLOW</arg>\n                    <arg>-Xplugin:ErrorProne ${errorprone.args}</arg>\n                    <!-- below needed per https://errorprone.info/docs/installation -->\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>\n                    <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>\n                    <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>\n                    <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>\n                  </compilerArgs>\n                  <annotationProcessorPaths>\n                    <processorPath>\n                      <groupId>com.google.errorprone</groupId>\n                      <artifactId>error_prone_core</artifactId>\n                      <version>${errorprone.version}</version>\n                    </processorPath>\n                    <!-- auto-value is placed here eventhough not needed for all projects as\n                         configuring along with errorprone is tricky in subprojects -->\n                    <processorPath>\n                      <groupId>com.google.auto.value</groupId>\n                      <artifactId>auto-value</artifactId>\n                      <version>${auto-value.version}</version>\n                    </processorPath>\n                  </annotationProcessorPaths>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n\n    <profile>\n      <id>release</id>\n      <build>\n        <plugins>\n          <plugin>\n            <groupId>org.sonatype.plugins</groupId>\n            <artifactId>nexus-staging-maven-plugin</artifactId>\n            <extensions>true</extensions>\n            <configuration>\n              <serverId>ossrh</serverId>\n              <nexusUrl>https://oss.sonatype.org/</nexusUrl>\n              <!-- Zipkin release is about ~100M mostly from the two server distributions. Default\n                   will timeout after 5 minutes, which can trigger fairly easily with this size. -->\n              <stagingProgressPauseDurationSeconds>20</stagingProgressPauseDurationSeconds>\n              <stagingProgressTimeoutMinutes>30</stagingProgressTimeoutMinutes>\n              <autoReleaseAfterClose>true</autoReleaseAfterClose>\n            </configuration>\n          </plugin>\n\n          <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-gpg-plugin</artifactId>\n            <version>3.2.3</version>\n            <executions>\n              <execution>\n                <id>sign-artifacts</id>\n                <phase>verify</phase>\n                <goals>\n                  <goal>sign</goal>\n                </goals>\n                <configuration>\n                  <gpgArguments>\n                    <arg>--pinentry-mode</arg>\n                    <arg>loopback</arg>\n                  </gpgArguments>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n\n          <!-- Creates source jar -->\n          <plugin>\n            <artifactId>maven-source-plugin</artifactId>\n            <version>${maven-source-plugin.version}</version>\n            <executions>\n              <execution>\n                <id>attach-sources</id>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n              </execution>\n            </executions>\n          </plugin>\n\n          <!-- Creates javadoc jar, skipping internal classes -->\n          <plugin>\n            <artifactId>maven-javadoc-plugin</artifactId>\n            <version>${maven-javadoc-plugin.version}</version>\n            <configuration>\n              <sourceFileExcludes>\n                <exclude>**/internal/*.java</exclude>\n                <exclude>**/Internal*.java</exclude>\n              </sourceFileExcludes>\n              <excludePackageNames>*.internal.*</excludePackageNames>\n              <failOnError>false</failOnError>\n              <!-- hush pedantic warnings: we don't put param and return on everything! -->\n              <doclint>none</doclint>\n              <!-- While we publish modules, our source is pre-Java9 so tell javadoc that. -->\n              <source>${maven.compiler.release}</source>\n            </configuration>\n            <executions>\n              <execution>\n                <id>attach-javadocs</id>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n                <phase>package</phase>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n\n    <profile>\n      <id>netbeans</id>\n      <activation>\n        <activeByDefault>true</activeByDefault>\n      </activation>\n      <properties>\n        <!-- NetBeans -->\n        <org-netbeans-modules-editor-indent.CodeStyle.usedProfile>project</org-netbeans-modules-editor-indent.CodeStyle.usedProfile>\n        <org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>2</org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>\n        <org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>2</org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>\n        <org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>2</org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>\n        <org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>110</org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>\n        <org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>true</org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>\n      </properties>\n    </profile>\n\n    <profile>\n      <id>module-info</id>\n      <!-- Build profiles can only consider static properties, such as files or ENV variables.\n           To conditionally add module information, we use existence of bnd.bnd. This allows\n           irrelevant packages such as tests and benchmarks to quietly opt-out.\n           http://maven.apache.org/guides/introduction/introduction-to-profiles.html -->\n      <activation>\n        <file>\n          <exists>bnd.bnd</exists>\n        </file>\n      </activation>\n      <build>\n        <plugins>\n          <!-- OSGi and Java Modules configuration -->\n          <plugin>\n            <groupId>org.apache.felix</groupId>\n            <artifactId>maven-bundle-plugin</artifactId>\n            <version>${maven-bundle-plugin.version}</version>\n            <configuration>\n              <obrRepository>NONE</obrRepository>\n              <instructions>\n                <_include>-bnd.bnd</_include>\n              </instructions>\n            </configuration>\n            <executions>\n              <execution>\n                <phase>process-classes</phase>\n                <goals>\n                  <goal>manifest</goal>\n                </goals>\n              </execution>\n            </executions>\n          </plugin>\n          <plugin>\n            <artifactId>maven-jar-plugin</artifactId>\n            <executions>\n              <execution>\n                <id>default-jar</id>\n                <configuration>\n                  <archive>\n                    <!-- Include the MANIFEST.MF maven-bundle-plugin generates from bnd.bnd -->\n                    <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>\n                    <manifestEntries>\n                      <Automatic-Module-Name>${module.name}</Automatic-Module-Name>\n                    </manifestEntries>\n                  </archive>\n                </configuration>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n  </profiles>\n</project>\n"
  },
  {
    "path": "src/etc/header.txt",
    "content": "Copyright The OpenZipkin Authors\nSPDX-License-Identifier: Apache-2.0\n"
  },
  {
    "path": "zipkin/RATIONALE.md",
    "content": "Zipkin Core Library Rationale\n==============\n\nMuch of this rationale is the same as [Brave](https://github.com/openzipkin/brave/blob/master/brave/RATIONALE.md), for consistency reasons. Some\naspects, such as Java language level, directly support older versions of Brave.\nHowever, looking only at Brave will not give a full impact of choices here.\nThis library is used for streaming pipelines and other instrumentation\nlibraries. It is important to consider carefully before revisiting topics here.\n\nWhile many ideas are our own, there are notable aspects borrowed or adapted\nfrom others. It is our goal to cite when we learned something through a prior\nlibrary, as that allows people to research any rationale that predates our\nusage.\n\nBelow is always incomplete, and always improvable. We don't document every\nthought as it would betray productivity and make this document unreadable.\nRationale here should be limited to impactful designs, and aspects non-obvious,\nnon-conventional or subtle.\n\n## Java conventions\nWe only expose types public internally or after significant demand. This keeps\nthe api small and easier to manage when charting migration paths. Otherwise,\ntypes are always package private.\n\nMethods should only be marked public when they are intentional apis or\ninheritance requires it. This practice prevents accidental dependence on\nutilities.\n\n### Why no private symbols? (methods and fields)\nZipkin is a library with embedded use cases, such as inside Java agents or\nAndroid code.\n\n<!-- markdown-link-check-disable-next-line -->\nFor example, Android has a [hard limit on total methods in an application](https://developer.android.com/build/multidex#avoid).\nFields marked private imply accessors in order to share state in the same\npackage. We routinely share state, such as codec internals within a package.\nIf we marked fields private, we'd count against that limit without adding\nvalue.\n\nModifiers on fields and methods are distracting to read and increase the size\nof the bytecode generated during compilation. By recovering the size otherwise\nspent on private modifiers, we not only avoid hitting limits, but we are also\nable to add more code with the same jar size.\n\nFor example, Zipkin 2.21 remains less than 250KiB, with no dependencies,\nincluding an in-memory storage implementation and embedded JSON, ProtoBuf and\nThrift codecs.\n\nThis means we do not support sharing our packages with third parties, but we\ndo support an \"always share inside a package\" in our repository. In other\nwords, we trust our developers to proceed with caution. In the first seven\nyears of project history, we have had no issues raised with this policy.\n\n### Java 8\n\nUp until Zipkin 3, we supported instrumentation of very old applications via\nsource level 1.6. Since then, Brave embedded its own JSON writer and no longer\nuses this library. Also, other instrumentation libraries, like OpenTelemetry,\nhave a floor version of Java 8. Keeping Java 6 support here limits the LTS\nwe can use to release to max JDK 11. We moved to Java 8 to compromise on these\npoints, knowing Brave 6 can still serve old applications.\n\n### Zero dependency policy\nSome dependents of this library are instrumentation in nature, and we cannot\npredict what 3rd party dependencies they will have. Attempting to do that would\nlimit the applicability of this library, which is an anti-goal. Instead, we\nchoose to use nothing except floor Java version features, currently Java 6.\n\nHere's an example of when things that seem right aren't. We once dropped our\ninternal `@Nullable` annotation (which is source retention), in favor of JSR\n305 (which is runtime retention). In doing so, we got `null` analysis from\nIntellij. However, we entered a swamp of dependency conflicts, which started\nwith OSGi (making us switch to a service mix bundle for the annotations), and\nlater Java 9 (conflict with apps using jax-ws). In summary, it is easiest for\nus to have no dependencies also, as we don't inherit tug-of-war between modular\nframeworks who each have a different \"right\" answer for even annotations!\n\nIncidentally, IntelliJ can be configured to use `zipkin2.internal.Nullable`, now.\nSearch for `Nullable` under inspections to configure this.\n\n### Why `new NullPointerException(\"xxx == null\")`\nFor public entry points, we eagerly check null and throw `NullPointerException`\nwith a message like \"xxx == null\". This is not a normal pre-condition, such as\nargument validation, which you'd throw `IllegalArgumentException` for. What's\nhappening here is we are making debugging (literally NPEs are bugs) easier, by\nnot deferring to Java to raise the NPE. If we deferred, it could be confusing\nwhich local was null, especially as deferring results in an exception with no\nmessage.\n\n"
  },
  {
    "path": "zipkin/bnd.bnd",
    "content": "# we block import of shaded packages as maven bundle plugin analyzes the unshaded jar\nImport-Package: \\\n\t!com.google.gson.stream,\\\n\t*\nExport-Package: \\\n\tzipkin2,\\\n\tzipkin2.codec,\\\n\tzipkin2.storage,\\\n\tzipkin2.v1,\\\n\tzipkin2.internal;zipkin2internal=true;mandatory:=zipkin2internal\n"
  },
  {
    "path": "zipkin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <groupId>io.zipkin.zipkin2</groupId>\n  <artifactId>zipkin</artifactId>\n  <name>Zipkin Core Library</name>\n\n  <properties>\n    <!-- Matches Export-Package in bnd.bnd -->\n    <module.name>zipkin2</module.name>\n\n    <!--\n      MutablePublicArray: We share an array of hex chars in internal code\n      JdkObsolete: SortedMap was replaced by NavigableMap, but the focus is Sorted not Navigable\n    -->\n    <errorprone.args>-Xep:MutablePublicArray:OFF -Xep:JdkObsolete:OFF</errorprone.args>\n\n    <main.basedir>${project.basedir}/..</main.basedir>\n\n    <maven.compiler.source>8</maven.compiler.source>\n    <maven.compiler.target>8</maven.compiler.target>\n    <maven.compiler.release>8</maven.compiler.release>\n  </properties>\n\n  <dependencies>\n    <!-- Internal classes used in SpanBytesDecoder.JSON_V[12] -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <!-- this dependency is shaded out -->\n      <optional>true</optional>\n    </dependency>\n\n    <dependency>\n      <groupId>com.esotericsoftware</groupId>\n      <artifactId>kryo</artifactId>\n      <version>${kryo.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <plugins>\n      <!-- Use of gson is internal only -->\n      <plugin>\n        <artifactId>maven-shade-plugin</artifactId>\n        <executions>\n          <execution>\n            <phase>package</phase>\n            <goals>\n              <goal>shade</goal>\n            </goals>\n            <configuration>\n              <createDependencyReducedPom>false</createDependencyReducedPom>\n              <minimizeJar>true</minimizeJar>\n              <filters>\n                <filter>\n                  <artifact>com.google.code.gson:gson</artifact>\n                  <includes>\n                    <include>com/google/gson/Strictness.class</include>\n                    <include>com/google/gson/stream/JsonReader*.class</include>\n                    <include>com/google/gson/stream/JsonToken.class</include>\n                    <include>com/google/gson/stream/MalformedJsonException.class</include>\n                    <include>com/google/gson/internal/JsonReaderInternalAccess.class</include>\n                    <include>com/google/gson/internal/TroubleshootingGuide.class</include>\n                  </includes>\n                </filter>\n              </filters>\n              <relocations>\n                <relocation>\n                  <pattern>com.google.gson</pattern>\n                  <shadedPattern>zipkin2.internal.gson</shadedPattern>\n                </relocation>\n              </relocations>\n              <transformers>\n                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                  <manifestEntries>\n                    <Automatic-Module-Name>${module.name}</Automatic-Module-Name>\n                  </manifestEntries>\n                </transformer>\n              </transformers>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n\n    <resources>\n      <!-- This adds the LICENSE and NOTICE file to the jar and -sources jar of each module -->\n      <resource>\n        <filtering>false</filtering>\n        <directory>${main.basedir}</directory>\n        <targetPath>META-INF/</targetPath>\n        <includes>\n          <include>LICENSE</include>\n          <include>NOTICE</include>\n        </includes>\n      </resource>\n    </resources>\n  </build>\n</project>\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Annotation.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.ObjectStreamException;\nimport java.io.Serializable;\nimport java.io.StreamCorruptedException;\n\n/**\n * Associates an event that explains latency with a timestamp.\n *\n * <p>Unlike log statements, annotations are often codes: Ex. {@code cache.miss}.\n */\n//@Immutable\npublic final class Annotation implements Comparable<Annotation>, Serializable { // for Spark jobs\n  private static final long serialVersionUID = 0L;\n\n  public static Annotation create(long timestamp, String value) {\n    if (value == null) throw new NullPointerException(\"value == null\");\n    return new Annotation(timestamp, value);\n  }\n\n  /**\n   * Microseconds from epoch.\n   *\n   * <p>This value should be set directly by instrumentation, using the most precise value possible.\n   * For example, {@code gettimeofday} or multiplying {@link System#currentTimeMillis} by 1000.\n   */\n  public long timestamp() {\n    return timestamp;\n  }\n\n  /**\n   * Usually a short tag indicating an event, like {@code cache.miss} or {@code error}\n   */\n  public String value() {\n    return value;\n  }\n\n\n  /** Compares by {@link #timestamp}, then {@link #value}. */\n  @Override public int compareTo(Annotation that) {\n    if (this == that) return 0;\n    int byTimestamp = timestamp() < that.timestamp() ? -1 : timestamp() == that.timestamp() ? 0 : 1;\n    if (byTimestamp != 0) return byTimestamp;\n    return value().compareTo(that.value());\n  }\n\n  // clutter below mainly due to difficulty working with Kryo which cannot handle AutoValue subclass\n  // See https://github.com/openzipkin/zipkin/issues/1879\n  final long timestamp;\n  final String value;\n\n  Annotation(long timestamp, String value) {\n    this.timestamp = timestamp;\n    this.value = value;\n  }\n\n  @Override public String toString() {\n    return \"Annotation{\"\n      + \"timestamp=\" + timestamp + \", \"\n      + \"value=\" + value\n      + \"}\";\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof Annotation)) return false;\n    Annotation that = (Annotation) o;\n    return timestamp == that.timestamp() && value.equals(that.value());\n  }\n\n  @Override public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (int) ((timestamp >>> 32) ^ timestamp);\n    h *= 1000003;\n    h ^= value.hashCode();\n    return h;\n  }\n\n  // As this is an immutable object (no default constructor), defer to a serialization proxy.\n  Object writeReplace() throws ObjectStreamException {\n    return new SerializedForm(this);\n  }\n\n  private static final class SerializedForm implements Serializable {\n    static final long serialVersionUID = 0L;\n\n    final long timestamp;\n    final String value;\n\n    SerializedForm(Annotation annotation) {\n      timestamp = annotation.timestamp;\n      value = annotation.value;\n    }\n\n    Object readResolve() throws ObjectStreamException {\n      try {\n        return Annotation.create(timestamp, value);\n      } catch (IllegalArgumentException e) {\n        throw new StreamCorruptedException(e.getMessage());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Call.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/**\n * This captures a (usually remote) request and can be used once, either {@link #execute()\n * synchronously} or {@link #enqueue(Callback) asynchronously}. At any time, from any thread, you\n * can call {@linkplain #cancel()}, which might stop an in-flight request or prevent one from\n * occurring.\n *\n * <p>Implementations should prepare a call such that there's little or no likelihood of late\n * runtime exceptions. For example, if the call is to get a trace, the call to {@code listSpans}\n * should propagate input errors vs delay them until a call to {@linkplain #execute()} or\n * {@linkplain #enqueue(Callback)}.\n *\n * <p>Ex.\n * <pre>{@code\n * // Any translation of an input request to remote parameters should happen here, and any related\n * // errors should propagate here.\n * Call<List<List<Span>>> listTraces = spanStore.listTraces(request);\n * // When this executes, it should simply run the remote request.\n * List<Span> trace = getTraceCall.execute();\n * }</pre>\n *\n * <p>An instance of call cannot be invoked more than once, but you can {@linkplain #clone()} an\n * instance if you need to replay the call. There is no relationship between a call and a number of\n * remote requests. For example, an implementation that stores spans may make hundreds of remote\n * requests, possibly retrying on your behalf.\n *\n * <p>This type owes its design to {@code retrofit2.Call}, which is nearly the same, except limited\n * to HTTP transports.\n *\n * @param <V> the success type, typically not null except when {@code V} is {@linkplain Void}.\n */\npublic abstract class Call<V> implements Cloneable {\n  /**\n   * Returns a completed call which has the supplied value. This is useful when input parameters\n   * imply there's no call needed. For example, an empty input might always result in an empty\n   * output.\n   */\n  public static <V> Call<V> create(V v) {\n    return new Constant<>(v);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  public static <T> Call<List<T>> emptyList() {\n    return Call.create(Collections.emptyList());\n  }\n\n  public interface Mapper<V1, V2> {\n    V2 map(V1 input);\n  }\n\n  /**\n   * Maps the result of this call into a different shape, as defined by the {@code mapper} function.\n   * This is used to convert values from one type to another. For example, you could use this to\n   * convert between zipkin v1 and v2 span format.\n   *\n   * <pre>{@code\n   * getTracesV1Call = getTracesV2Call.map(traces -> v2TracesConverter);\n   * }</pre>\n   *\n   * <p>This method intends to be used for chaining. That means \"this\" instance should be discarded\n   * in favor of the result of this method.\n   */\n  public final <R> Call<R> map(Mapper<V, R> mapper) {\n    return new Mapping<>(mapper, this);\n  }\n\n  public interface FlatMapper<V1, V2> {\n    Call<V2> map(V1 input);\n  }\n\n  /**\n   * Maps the result of this call into another, as defined by the {@code flatMapper} function. This\n   * is used to chain two remote calls together. For example, you could use this to chain a list IDs\n   * call to a get by IDs call.\n   *\n   * <pre>{@code\n   * getTracesCall = getIdsCall.flatMap(ids -> getTraces(ids));\n   *\n   * // this would now invoke the chain\n   * traces = getTracesCall.enqueue(tracesCallback);\n   * }</pre>\n   *\n   * Cancelation propagates to the mapped call.\n   *\n   * <p>This method intends to be used for chaining. That means \"this\" instance should be discarded\n   * in favor of the result of this method.\n   */\n  public final <R> Call<R> flatMap(FlatMapper<V, R> flatMapper) {\n    return new FlatMapping<>(flatMapper, this);\n  }\n\n  public interface ErrorHandler<V> {\n    /** Attempts to resolve an error. The user must call the callback. */\n    void onErrorReturn(Throwable error, Callback<V> callback);\n  }\n\n  /**\n   * Returns a call which can attempt to resolve an exception. This is useful when a remote call\n   * returns an error when a resource is not found.\n   *\n   * <p>Here's an example of coercing 404 to empty:\n   * <pre>{@code\n   * call.handleError((error, callback) -> {\n   *   if (error instanceof HttpException && ((HttpException) error).code == 404) {\n   *     callback.onSuccess(Collections.emptyList());\n   *   } else {\n   *     callback.onError(error);\n   *   }\n   * });\n   * }</pre>\n   */\n  public final Call<V> handleError(ErrorHandler<V> errorHandler) {\n    return new ErrorHandling<>(errorHandler, this);\n  }\n\n  // Taken from RxJava throwIfFatal, which was taken from scala\n  public static void propagateIfFatal(Throwable t) {\n    if (t instanceof VirtualMachineError) {\n      throw (VirtualMachineError) t;\n    } else if (t instanceof ThreadDeath) {\n      throw (ThreadDeath) t;\n    } else if (t instanceof LinkageError) {\n      throw (LinkageError) t;\n    }\n  }\n\n  /**\n   * Invokes a request, returning a success value or propagating an error to the caller. Invoking\n   * this more than once will result in an error. To repeat a call, make a copy with {@linkplain\n   * #clone()}.\n   *\n   * <p>Eventhough this is a blocking call, implementations may honor calls to {@linkplain\n   * #cancel()} from a different thread.\n   *\n   * @return a success value. Null is unexpected, except when {@code V} is {@linkplain Void}.\n   */\n  public abstract V execute() throws IOException;\n\n  /**\n   * Invokes a request asynchronously, signaling the {@code callback} when complete. Invoking this\n   * more than once will result in an error. To repeat a call, make a copy with {@linkplain\n   * #clone()}.\n   */\n  public abstract void enqueue(Callback<V> callback);\n\n  /**\n   * Requests to cancel this call, even if some implementations may not support it. For example, a\n   * blocking call is sometimes not cancelable.\n   */\n  // Boolean isn't returned because some implementations may cancel asynchronously.\n  // Implementing might throw an IOException on execute or callback.onError(IOException)\n  public abstract void cancel();\n\n  /**\n   * Returns true if {@linkplain #cancel()} was called.\n   *\n   * <p>Calls can fail before being canceled, so true does always mean cancelation caused a call to\n   * fail. That said, successful cancellation does result in a failure.\n   */\n  // isCanceled exists while isExecuted does not because you do not need the latter to implement\n  // asynchronous bindings, such as rxjava2\n  public abstract boolean isCanceled();\n\n  /** Returns a copy of this object, so you can make an identical follow-up request. */\n  @Override public abstract Call<V> clone();\n\n  static class Constant<V> extends Base<V> { // not final for mock testing\n    final V v;\n\n    Constant(V v) {\n      this.v = v;\n    }\n\n    @Override protected V doExecute() {\n      return v;\n    }\n\n    @Override protected void doEnqueue(Callback<V> callback) {\n      callback.onSuccess(v);\n    }\n\n    @Override public Call<V> clone() {\n      return new Constant<>(v);\n    }\n\n    @Override public String toString() {\n      return \"ConstantCall{value=\" + v + \"}\";\n    }\n\n    @Override public boolean equals(Object o) {\n      if (o == this) return true;\n      if (o instanceof Constant) {\n        Constant that = (Constant) o;\n        return Objects.equals(this.v, that.v);\n      }\n      return false;\n    }\n\n    @Override\n    public int hashCode() {\n      int h = 1;\n      h *= 1000003;\n      h ^= (v == null) ? 0 : v.hashCode();\n      return h;\n    }\n  }\n\n  static final class Mapping<R, V> extends Base<R> {\n    final Mapper<V, R> mapper;\n    final Call<V> delegate;\n\n    Mapping(Mapper<V, R> mapper, Call<V> delegate) {\n      this.mapper = mapper;\n      this.delegate = delegate;\n    }\n\n    @Override protected R doExecute() throws IOException {\n      return mapper.map(delegate.execute());\n    }\n\n    @Override protected void doEnqueue(final Callback<R> callback) {\n      delegate.enqueue(new Callback<V>() {\n        @Override public void onSuccess(V value) {\n          try {\n            callback.onSuccess(mapper.map(value));\n          } catch (Throwable t) {\n            callback.onError(t);\n          }\n        }\n\n        @Override public void onError(Throwable t) {\n          callback.onError(t);\n        }\n      });\n    }\n\n    @Override public String toString() {\n      return \"Mapping{call=\" + delegate + \", mapper=\" + mapper + \"}\";\n    }\n\n    @Override public Call<R> clone() {\n      return new Mapping<>(mapper, delegate.clone());\n    }\n  }\n\n  static final class FlatMapping<R, V> extends Base<R> {\n    final FlatMapper<V, R> flatMapper;\n    final Call<V> delegate;\n    volatile Call<R> mapped;\n\n    FlatMapping(FlatMapper<V, R> flatMapper, Call<V> delegate) {\n      this.flatMapper = flatMapper;\n      this.delegate = delegate;\n    }\n\n    @Override protected R doExecute() throws IOException {\n      return (mapped = flatMapper.map(delegate.execute())).execute();\n    }\n\n    @Override protected void doEnqueue(final Callback<R> callback) {\n      delegate.enqueue(new Callback<V>() {\n        @Override public void onSuccess(V value) {\n          try {\n            (mapped = flatMapper.map(value)).enqueue(callback);\n          } catch (Throwable t) {\n            propagateIfFatal(t);\n            callback.onError(t);\n          }\n        }\n\n        @Override public void onError(Throwable t) {\n          callback.onError(t);\n        }\n      });\n    }\n\n    @Override protected void doCancel() {\n      delegate.cancel();\n      if (mapped != null) mapped.cancel();\n    }\n\n    @Override public String toString() {\n      return \"FlatMapping{call=\" + delegate + \", flatMapper=\" + flatMapper + \"}\";\n    }\n\n    @Override public Call<R> clone() {\n      return new FlatMapping<>(flatMapper, delegate.clone());\n    }\n  }\n\n  static final class ErrorHandling<V> extends Base<V> {\n    static final Object SENTINEL = new Object(); // to differentiate from null\n    final ErrorHandler<V> errorHandler;\n    final Call<V> delegate;\n\n    ErrorHandling(ErrorHandler<V> errorHandler, Call<V> delegate) {\n      this.errorHandler = errorHandler;\n      this.delegate = delegate;\n    }\n\n    @Override protected V doExecute() throws IOException {\n      try {\n        return delegate.execute();\n      } catch (IOException e) {\n        return handleError(e);\n      } catch (RuntimeException e) {\n        return handleError(e);\n      } catch (Error e) {\n        Call.propagateIfFatal(e);\n        return handleError(e);\n      }\n    }\n\n    <T extends Throwable> V handleError(T e) throws T {\n      final AtomicReference ref = new AtomicReference(SENTINEL);\n      errorHandler.onErrorReturn(e, new Callback<V>() {\n        @Override\n        public void onSuccess(V value) {\n          ref.set(value);\n        }\n\n        @Override\n        public void onError(Throwable t) {\n        }\n      });\n      Object result = ref.get();\n      if (SENTINEL == result) throw e;\n      return (V) result;\n    }\n\n    @Override protected void doEnqueue(final Callback<V> callback) {\n      delegate.enqueue(new Callback<V>() {\n        @Override public void onSuccess(V value) {\n          callback.onSuccess(value);\n        }\n\n        @Override public void onError(Throwable t) {\n          errorHandler.onErrorReturn(t, callback);\n        }\n      });\n    }\n\n    @Override protected void doCancel() {\n      delegate.cancel();\n    }\n\n    @Override public String toString() {\n      return \"ErrorHandling{call=\" + delegate + \", errorHandler=\" + errorHandler + \"}\";\n    }\n\n    @Override public Call<V> clone() {\n      return new ErrorHandling<>(errorHandler, delegate.clone());\n    }\n  }\n\n  public static abstract class Base<V> extends Call<V> {\n    volatile boolean canceled;\n    boolean executed;\n\n    protected Base() {\n    }\n\n    @Override public final V execute() throws IOException {\n      synchronized (this) {\n        if (this.executed) throw new IllegalStateException(\"Already Executed\");\n        this.executed = true;\n      }\n\n      if (isCanceled()) {\n        throw new IOException(\"Canceled\");\n      } else {\n        return this.doExecute();\n      }\n    }\n\n    protected abstract V doExecute() throws IOException;\n\n    @Override public final void enqueue(Callback<V> callback) {\n      synchronized (this) {\n        if (this.executed) throw new IllegalStateException(\"Already Executed\");\n        this.executed = true;\n      }\n\n      if (isCanceled()) {\n        callback.onError(new IOException(\"Canceled\"));\n      } else {\n        this.doEnqueue(callback);\n      }\n    }\n\n    protected abstract void doEnqueue(Callback<V> callback);\n\n    @Override public final void cancel() {\n      this.canceled = true;\n      doCancel();\n    }\n\n    protected void doCancel() {\n    }\n\n    @Override public final boolean isCanceled() {\n      return this.canceled || doIsCanceled();\n    }\n\n    protected boolean doIsCanceled() {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Callback.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport zipkin2.internal.Nullable;\n\n/**\n * A callback of a single result or error.\n *\n * <p>This is a bridge to async libraries such as CompletableFuture complete, completeExceptionally.\n *\n * <p>Implementations will call either {@link #onSuccess} or {@link #onError}, but not both.\n */\npublic interface Callback<V> {\n\n  /**\n   * Invoked when computation produces its potentially null value successfully.\n   *\n   * <p>When this is called, {@link #onError} won't be.\n   */\n  void onSuccess(@Nullable V value);\n\n  /**\n   * Invoked when computation produces a possibly null value successfully.\n   *\n   * <p>When this is called, {@link #onSuccess} won't be.\n   */\n  void onError(Throwable t);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/CheckResult.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport zipkin2.internal.Nullable;\n\n/**\n * Answers the question: Are operations on this component likely to succeed?\n *\n * <p>Implementations should initialize the component if necessary. It should test a remote\n * connection, or consult a trusted source to derive the result. They should use least resources\n * possible to establish a meaningful result, and be safe to call many times, even concurrently.\n *\n * @see CheckResult#OK\n */\n// @Immutable\npublic final class CheckResult {\n  public static final CheckResult OK = new CheckResult(true, null);\n\n  public static CheckResult failed(Throwable error) {\n    return new CheckResult(false, error);\n  }\n\n  public boolean ok() {\n    return ok;\n  }\n\n  /** Present when not ok */\n  @Nullable\n  public Throwable error() {\n    return error;\n  }\n\n  final boolean ok;\n  final Throwable error;\n\n  CheckResult(boolean ok, @Nullable Throwable error) {\n    this.ok = ok;\n    this.error = error;\n  }\n\n  @Override\n  public String toString() {\n    return \"CheckResult{ok=\" + ok + \", \" + \"error=\" + error + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Component.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.Closeable;\nimport java.io.IOException;\n\n/**\n * Components are object graphs used to compose a zipkin service or client. For example, a storage\n * component might return a query api.\n *\n * <p>Components are lazy with regards to I/O. They can be injected directly to other components so\n * as to avoid crashing the application graph if a network service is unavailable.\n */\npublic abstract class Component implements Closeable {\n\n  /**\n   * Answers the question: Are operations on this component likely to succeed?\n   *\n   * <p>Implementations should initialize the component if necessary. It should test a remote\n   * connection, or consult a trusted source to derive the result. They should use least resources\n   * possible to establish a meaningful result, and be safe to call many times, even concurrently.\n   *\n   * @see CheckResult#OK\n   */\n  public CheckResult check() {\n    return CheckResult.OK;\n  }\n\n  /**\n   * Closes any network resources created implicitly by the component.\n   *\n   * <p>For example, if this created a connection, it would close it. If it was provided one, this\n   * would close any sessions, but leave the connection open.\n   */\n  @Override public void close() throws IOException {\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/DependencyLink.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.ObjectStreamException;\nimport java.io.Serializable;\nimport java.io.StreamCorruptedException;\nimport java.util.Locale;\nimport zipkin2.codec.DependencyLinkBytesDecoder;\nimport zipkin2.codec.DependencyLinkBytesEncoder;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/** A dependency link is an edge between two services. */\n//@Immutable\npublic final class DependencyLink implements Serializable { // for Spark and Flink jobs\n  private static final long serialVersionUID = 0L;\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  /** The parent service name (caller), {@link Span#localServiceName()} if instrumented. */\n  public String parent() {\n    return parent;\n  }\n\n  /** The chold service name (callee), {@link Span#localServiceName()} if instrumented. */\n  public String child() {\n    return child;\n  }\n\n  /** Total traced calls made from {@link #parent} to {@link #child} */\n  public long callCount() {\n    return callCount;\n  }\n\n  /**\n   * {@linkplain #callCount Count of calls} known to be errors (having one {@linkplain Span#tags()\n   * tag} named \"error\").\n   */\n  public long errorCount() {\n    return errorCount;\n  }\n\n  public Builder toBuilder() {\n    return new Builder(this);\n  }\n\n  public static final class Builder {\n    String parent, child;\n    long callCount, errorCount;\n\n    Builder() {\n    }\n\n    Builder(DependencyLink source) {\n      this.parent = source.parent;\n      this.child = source.child;\n      this.callCount = source.callCount;\n      this.errorCount = source.errorCount;\n    }\n\n    /** @see #parent() */\n    public Builder parent(String parent) {\n      if (parent == null) throw new NullPointerException(\"parent == null\");\n      this.parent = parent.toLowerCase(Locale.ROOT);\n      return this;\n    }\n\n    /** @see #child() */\n    public Builder child(String child) {\n      if (child == null) throw new NullPointerException(\"child == null\");\n      this.child = child.toLowerCase(Locale.ROOT);\n      return this;\n    }\n\n    /** @see #callCount() */\n    public Builder callCount(long callCount) {\n      this.callCount = callCount;\n      return this;\n    }\n\n    /** @see #errorCount() */\n    public Builder errorCount(long errorCount) {\n      this.errorCount = errorCount;\n      return this;\n    }\n\n    public DependencyLink build() {\n      String missing = \"\";\n      if (parent == null) missing += \" parent\";\n      if (child == null) missing += \" child\";\n      if (!missing.isEmpty()) throw new IllegalStateException(\"Missing :\" + missing);\n      return new DependencyLink(this);\n    }\n  }\n\n  @Override public String toString() {\n    return new String(DependencyLinkBytesEncoder.JSON_V1.encode(this), UTF_8);\n  }\n\n  // clutter below mainly due to difficulty working with Kryo which cannot handle AutoValue subclass\n  // See https://github.com/openzipkin/zipkin/issues/1879\n  final String parent, child;\n  final long callCount, errorCount;\n\n  DependencyLink(Builder builder) {\n    parent = builder.parent;\n    child = builder.child;\n    callCount = builder.callCount;\n    errorCount = builder.errorCount;\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof DependencyLink)) return false;\n    DependencyLink that = (DependencyLink) o;\n    return parent.equals(that.parent)\n      && child.equals(that.child)\n      && callCount == that.callCount\n      && errorCount == that.errorCount;\n  }\n\n  @Override public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= parent.hashCode();\n    h *= 1000003;\n    h ^= child.hashCode();\n    h *= 1000003;\n    h ^= (int) ((callCount >>> 32) ^ callCount);\n    h *= 1000003;\n    h ^= (int) ((errorCount >>> 32) ^ errorCount);\n    return h;\n  }\n\n  // This is an immutable object, and our encoder is faster than java's: use a serialization proxy.\n  Object writeReplace() throws ObjectStreamException {\n    return new SerializedForm(DependencyLinkBytesEncoder.JSON_V1.encode(this));\n  }\n\n  private static final class SerializedForm implements Serializable {\n    private static final long serialVersionUID = 0L;\n\n    final byte[] bytes;\n\n    SerializedForm(byte[] bytes) {\n      this.bytes = bytes;\n    }\n\n    Object readResolve() throws ObjectStreamException {\n      try {\n        return DependencyLinkBytesDecoder.JSON_V1.decodeOne(bytes);\n      } catch (IllegalArgumentException e) {\n        throw new StreamCorruptedException(e.getMessage());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Endpoint.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.ObjectStreamException;\nimport java.io.Serializable;\nimport java.io.StreamCorruptedException;\nimport java.net.Inet4Address;\nimport java.net.Inet6Address;\nimport java.net.InetAddress;\nimport java.nio.ByteBuffer;\nimport java.util.Locale;\nimport java.util.Objects;\nimport zipkin2.internal.Nullable;\nimport zipkin2.internal.RecyclableBuffers;\n\nimport static zipkin2.internal.HexCodec.HEX_DIGITS;\n\n/** The network context of a node in the service graph. */\n//@Immutable\npublic final class Endpoint implements Serializable { // for Spark and Flink jobs\n  private static final long serialVersionUID = 0L;\n\n  /**\n   * Lower-case label of this node in the service graph, such as \"favstar\". Leave absent if\n   * unknown.\n   *\n   * <p>This is a primary label for trace lookup and aggregation, so it should be intuitive and\n   * consistent. Many use a name from service discovery.\n   */\n  @Nullable public String serviceName() {\n    return serviceName;\n  }\n\n  /**\n   * The text representation of the primary IPv4 address associated with this a connection. Ex.\n   * 192.168.99.100 Absent if unknown.\n   */\n  @Nullable public String ipv4() {\n    return ipv4;\n  }\n\n  /**\n   * IPv4 endpoint address packed into 4 bytes or null if unknown.\n   *\n   * @see #ipv6()\n   * @see java.net.Inet4Address#getAddress()\n   */\n  @Nullable public byte[] ipv4Bytes() {\n    return ipv4Bytes;\n  }\n\n  /**\n   * The text representation of the primary IPv6 address associated with this a connection. Ex.\n   * 2001:db8::c001 Absent if unknown.\n   *\n   * @see #ipv4() for mapped addresses\n   * @see #ipv6Bytes()\n   */\n  @Nullable public String ipv6() {\n    return ipv6;\n  }\n\n  /**\n   * IPv6 endpoint address packed into 16 bytes or null if unknown.\n   *\n   * @see #ipv6()\n   * @see java.net.Inet6Address#getAddress()\n   */\n  @Nullable public byte[] ipv6Bytes() {\n    return ipv6Bytes;\n  }\n\n  /**\n   * Port of the IP's socket or null, if not known.\n   *\n   * @see java.net.InetSocketAddress#getPort()\n   */\n  @Nullable public Integer port() {\n    return port != 0 ? port : null;\n  }\n\n  /**\n   * Like {@link #port()} except returns a primitive where zero implies absent.\n   *\n   * <p>Using this method will avoid allocation, so is encouraged when copying data.\n   */\n  public int portAsInt() {\n    return port;\n  }\n\n  public Builder toBuilder() {\n    return new Builder(this);\n  }\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder {\n    String serviceName, ipv4, ipv6;\n    byte[] ipv4Bytes, ipv6Bytes;\n    int port; // zero means null\n\n    Builder(Endpoint source) {\n      serviceName = source.serviceName;\n      ipv4 = source.ipv4;\n      ipv6 = source.ipv6;\n      ipv4Bytes = source.ipv4Bytes;\n      ipv6Bytes = source.ipv6Bytes;\n      port = source.port;\n    }\n\n    Builder merge(Endpoint source) {\n      if (serviceName == null) serviceName = source.serviceName;\n      if (ipv4 == null) ipv4 = source.ipv4;\n      if (ipv6 == null) ipv6 = source.ipv6;\n      if (ipv4Bytes == null) ipv4Bytes = source.ipv4Bytes;\n      if (ipv6Bytes == null) ipv6Bytes = source.ipv6Bytes;\n      if (port == 0) port = source.port;\n      return this;\n    }\n\n    /** Sets {@link Endpoint#serviceName} */\n    public Builder serviceName(@Nullable String serviceName) {\n      this.serviceName = serviceName == null || serviceName.isEmpty()\n        ? null : serviceName.toLowerCase(Locale.ROOT);\n      return this;\n    }\n\n    /** Chaining variant of {@link #parseIp(InetAddress)} */\n    public Builder ip(@Nullable InetAddress addr) {\n      parseIp(addr);\n      return this;\n    }\n\n    /**\n     * Returns true if {@link Endpoint#ipv4()} or {@link Endpoint#ipv6()} could be parsed from the\n     * input.\n     *\n     * <p>Returns boolean not this for conditional parsing. For example:\n     * <pre>{@code\n     * if (!builder.parseIp(input.getHeader(\"X-Forwarded-For\"))) {\n     *   builder.parseIp(input.getRemoteAddr());\n     * }\n     * }</pre>\n     *\n     * @see #parseIp(String)\n     */\n    public boolean parseIp(@Nullable InetAddress addr) {\n      if (addr == null) return false;\n      if (addr instanceof Inet4Address) {\n        ipv4 = addr.getHostAddress();\n        ipv4Bytes = addr.getAddress();\n      } else if (addr instanceof Inet6Address) {\n        byte[] addressBytes = addr.getAddress();\n        if (!parseEmbeddedIPv4(addressBytes)) {\n          ipv6 = writeIpV6(addressBytes);\n          ipv6Bytes = addressBytes;\n        }\n      } else {\n        return false;\n      }\n      return true;\n    }\n\n    /**\n     * Like {@link #parseIp(String)} except this accepts a byte array.\n     *\n     * @param ipBytes byte array whose ownership is exclusively transferred to this endpoint.\n     */\n    public boolean parseIp(byte[] ipBytes) {\n      if (ipBytes == null) return false;\n      if (ipBytes.length == 4) {\n        ipv4Bytes = ipBytes;\n        ipv4 = writeIpV4(ipBytes);\n      } else if (ipBytes.length == 16) {\n        if (!parseEmbeddedIPv4(ipBytes)) {\n          ipv6 = writeIpV6(ipBytes);\n          ipv6Bytes = ipBytes;\n        }\n      } else {\n        return false;\n      }\n      return true;\n    }\n\n    static String writeIpV4(byte[] ipBytes) {\n      char[] buf = RecyclableBuffers.shortStringBuffer();\n      int pos = 0;\n      pos = writeBackwards(ipBytes[0] & 0xff, pos, buf);\n      buf[pos++] = '.';\n      pos = writeBackwards(ipBytes[1] & 0xff, pos, buf);\n      buf[pos++] = '.';\n      pos = writeBackwards(ipBytes[2] & 0xff, pos, buf);\n      buf[pos++] = '.';\n      pos = writeBackwards(ipBytes[3] & 0xff, pos, buf);\n      return new String(buf, 0, pos);\n    }\n\n    static int writeBackwards(int b, int pos, char[] buf) {\n      if (b < 10) {\n        buf[pos] = HEX_DIGITS[b];\n        return pos + 1;\n      }\n      int i = pos += b < 100 ? 2 : 3; // We write backwards from right to left.\n      while (b != 0) {\n        int digit = b % 10;\n        buf[--i] = HEX_DIGITS[digit];\n        b /= 10;\n      }\n      return pos;\n    }\n\n    /** Chaining variant of {@link #parseIp(String)} */\n    public Builder ip(@Nullable String ipString) {\n      parseIp(ipString);\n      return this;\n    }\n\n    /**\n     * Returns true if {@link Endpoint#ipv4()} or {@link Endpoint#ipv6()} could be parsed from the\n     * input.\n     *\n     * <p>Returns boolean not this for conditional parsing. For example:\n     * <pre>{@code\n     * if (!builder.parseIp(input.getHeader(\"X-Forwarded-For\"))) {\n     *   builder.parseIp(input.getRemoteAddr());\n     * }\n     * }</pre>\n     *\n     * @see #parseIp(InetAddress)\n     */\n    public boolean parseIp(@Nullable String ipString) {\n      if (ipString == null || ipString.isEmpty()) return false;\n      IpFamily format = detectFamily(ipString);\n      if (format == IpFamily.IPv4) {\n        ipv4 = ipString;\n        ipv4Bytes = getIpv4Bytes(ipv4);\n      } else if (format == IpFamily.IPv4Embedded) {\n        ipv4 = ipString.substring(ipString.lastIndexOf(':') + 1);\n        ipv4Bytes = getIpv4Bytes(ipv4);\n      } else if (format == IpFamily.IPv6) {\n        byte[] addressBytes = textToNumericFormatV6(ipString);\n        if (addressBytes == null) return false;\n        ipv6 = writeIpV6(addressBytes); // ensures consistent format\n        ipv6Bytes = addressBytes;\n      } else {\n        return false;\n      }\n      return true;\n    }\n\n    /**\n     * Use this to set the port to an externally defined value.\n     *\n     * @param port port associated with the endpoint. zero coerces to null (unknown)\n     * @see Endpoint#port()\n     */\n    public Builder port(@Nullable Integer port) {\n      if (port != null) {\n        if (port > 0xffff) throw new IllegalArgumentException(\"invalid port \" + port);\n        if (port <= 0) port = 0;\n      }\n      this.port = port != null ? port : 0;\n      return this;\n    }\n\n    /** Sets {@link Endpoint#portAsInt()} */\n    public Builder port(int port) {\n      if (port > 0xffff) throw new IllegalArgumentException(\"invalid port \" + port);\n      if (port < 0) port = 0;\n      this.port = port;\n      return this;\n    }\n\n    public Endpoint build() {\n      return new Endpoint(this);\n    }\n\n    Builder() {\n    }\n\n    boolean parseEmbeddedIPv4(byte[] ipv6) {\n      for (int i = 0; i < 10; i++) { // Embedded IPv4 addresses start with unset 80 bits\n        if (ipv6[i] != 0) return false;\n      }\n\n      int flag = (ipv6[10] & 0xff) << 8 | (ipv6[11] & 0xff);\n      if (flag != 0) return false; // IPv4-Compatible or IPv4-Mapped\n\n      byte o1 = ipv6[12], o2 = ipv6[13], o3 = ipv6[14], o4 = ipv6[15];\n      if (o1 == 0 && o2 == 0 && o3 == 0 && o4 == 1) {\n        return false; // ::1 is localhost, not an embedded compat address\n      }\n\n      ipv4 = String.valueOf(o1 & 0xff) + '.' + (o2 & 0xff) + '.' + (o3 & 0xff) + '.' + (o4 & 0xff);\n      ipv4Bytes = new byte[] {o1, o2, o3, o4};\n      return true;\n    }\n  }\n\n  enum IpFamily {\n    Unknown,\n    IPv4,\n    IPv4Embedded,\n    IPv6\n  }\n\n  /**\n   * Adapted from code in {@code com.google.common.net.InetAddresses.ipStringToBytes}. This version\n   * separates detection from parsing and checks more carefully about embedded addresses.\n   */\n  static IpFamily detectFamily(String ipString) {\n    boolean hasColon = false;\n    boolean hasDot = false;\n    for (int i = 0, length = ipString.length(); i < length; i++) {\n      char c = ipString.charAt(i);\n      if (c == '.') {\n        hasDot = true;\n      } else if (c == ':') {\n        if (hasDot) return IpFamily.Unknown; // Colons must not appear after dots.\n        hasColon = true;\n      } else if (notHex(c)) {\n        return IpFamily.Unknown; // Everything else must be a decimal or hex digit.\n      }\n    }\n\n    // Now decide which address family to parse.\n    if (hasColon) {\n      if (hasDot) {\n        int lastColonIndex = ipString.lastIndexOf(':');\n        if (!isValidIpV4Address(ipString, lastColonIndex + 1, ipString.length())) {\n          return IpFamily.Unknown;\n        }\n        if (lastColonIndex == 1 && ipString.charAt(0) == ':') {// compressed like ::1.2.3.4\n          return IpFamily.IPv4Embedded;\n        }\n        if (lastColonIndex != 6 || ipString.charAt(0) != ':' || ipString.charAt(1) != ':') {\n          return IpFamily.Unknown;\n        }\n        for (int i = 2; i < 6; i++) {\n          char c = ipString.charAt(i);\n          if (c != 'f' && c != 'F' && c != '0') return IpFamily.Unknown;\n        }\n        return IpFamily.IPv4Embedded;\n      }\n      return IpFamily.IPv6;\n    } else if (hasDot && isValidIpV4Address(ipString, 0, ipString.length())) {\n      return IpFamily.IPv4;\n    }\n    return IpFamily.Unknown;\n  }\n\n  static boolean notHex(char c) {\n    return (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F');\n  }\n\n  static String writeIpV6(byte[] ipv6) {\n    int pos = 0;\n    char[] buf = RecyclableBuffers.shortStringBuffer();\n\n    // Compress the longest string of zeros\n    int zeroCompressionIndex = -1;\n    int zeroCompressionLength = -1;\n    int zeroIndex = -1;\n    boolean allZeros = true;\n    for (int i = 0; i < ipv6.length; i += 2) {\n      if (ipv6[i] == 0 && ipv6[i + 1] == 0) {\n        if (zeroIndex < 0) zeroIndex = i;\n        continue;\n      }\n      allZeros = false;\n      if (zeroIndex >= 0) {\n        int zeroLength = i - zeroIndex;\n        if (zeroLength > zeroCompressionLength) {\n          zeroCompressionIndex = zeroIndex;\n          zeroCompressionLength = zeroLength;\n        }\n        zeroIndex = -1;\n      }\n    }\n\n    // handle all zeros: 0:0:0:0:0:0:0:0 -> ::\n    if (allZeros) return \"::\";\n\n    // handle trailing zeros: 2001:0:0:4:0:0:0:0 -> 2001:0:0:4::\n    if (zeroCompressionIndex == -1 && zeroIndex != -1) {\n      zeroCompressionIndex = zeroIndex;\n      zeroCompressionLength = 16 - zeroIndex;\n    }\n\n    int i = 0;\n    while (i < ipv6.length) {\n      if (i == zeroCompressionIndex) {\n        buf[pos++] = ':';\n        i += zeroCompressionLength;\n        if (i == ipv6.length) buf[pos++] = ':';\n        continue;\n      }\n      if (i != 0) buf[pos++] = ':';\n\n      byte high = ipv6[i++];\n      byte low = ipv6[i++];\n\n      // handle leading zeros: 2001:0:0:4:0000:0:0:8 -> 2001:0:0:4::8\n      boolean leadingZero;\n      char val = HEX_DIGITS[(high >> 4) & 0xf];\n      if (!(leadingZero = val == '0')) buf[pos++] = val;\n      val = HEX_DIGITS[high & 0xf];\n      if (!(leadingZero = (leadingZero && val == '0'))) buf[pos++] = val;\n      val = HEX_DIGITS[(low >> 4) & 0xf];\n      if (!(leadingZero && val == '0')) buf[pos++] = val;\n      buf[pos++] = HEX_DIGITS[low & 0xf];\n    }\n    return new String(buf, 0, pos);\n  }\n\n  // Begin code from com.google.common.net.InetAddresses 23\n  static final int IPV6_PART_COUNT = 8;\n\n  @Nullable\n  static byte[] textToNumericFormatV6(String ipString) {\n    // An address can have [2..8] colons, and N colons make N+1 parts.\n    String[] parts = ipString.split(\":\", IPV6_PART_COUNT + 2);\n    if (parts.length < 3 || parts.length > IPV6_PART_COUNT + 1) {\n      return null;\n    }\n\n    // Disregarding the endpoints, find \"::\" with nothing in between.\n    // This indicates that a run of zeroes has been skipped.\n    int skipIndex = -1;\n    for (int i = 1; i < parts.length - 1; i++) {\n      if (parts[i].isEmpty()) {\n        if (skipIndex >= 0) {\n          return null; // Can't have more than one ::\n        }\n        skipIndex = i;\n      }\n    }\n\n    int partsHi; // Number of parts to copy from above/before the \"::\"\n    int partsLo; // Number of parts to copy from below/after the \"::\"\n    if (skipIndex >= 0) {\n      // If we found a \"::\", then check if it also covers the endpoints.\n      partsHi = skipIndex;\n      partsLo = parts.length - skipIndex - 1;\n      if (parts[0].isEmpty() && --partsHi != 0) {\n        return null; // ^: requires ^::\n      }\n      if (parts[parts.length - 1].isEmpty() && --partsLo != 0) {\n        return null; // :$ requires ::$\n      }\n    } else {\n      // Otherwise, allocate the entire address to partsHi. The endpoints\n      // could still be empty, but parseHextet() will check for that.\n      partsHi = parts.length;\n      partsLo = 0;\n    }\n\n    // If we found a ::, then we must have skipped at least one part.\n    // Otherwise, we must have exactly the right number of parts.\n    int partsSkipped = IPV6_PART_COUNT - (partsHi + partsLo);\n    if (!(skipIndex >= 0 ? partsSkipped >= 1 : partsSkipped == 0)) {\n      return null;\n    }\n\n    // Now parse the hextets into a byte array.\n    ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT);\n    try {\n      for (int i = 0; i < partsHi; i++) {\n        rawBytes.putShort(parseHextet(parts[i]));\n      }\n      for (int i = 0; i < partsSkipped; i++) {\n        rawBytes.putShort((short) 0);\n      }\n      for (int i = partsLo; i > 0; i--) {\n        rawBytes.putShort(parseHextet(parts[parts.length - i]));\n      }\n    } catch (NumberFormatException ex) {\n      return null;\n    }\n    return rawBytes.array();\n  }\n\n  static short parseHextet(String ipPart) {\n    // Note: we already verified that this string contains only hex digits.\n    int hextet = Integer.parseInt(ipPart, 16);\n    if (hextet > 0xffff) {\n      throw new NumberFormatException();\n    }\n    return (short) hextet;\n  }\n  // End code from com.google.common.net.InetAddresses 23\n\n  // Begin code from io.netty.util.NetUtil 4.1\n  static boolean isValidIpV4Address(String ip, int from, int toExcluded) {\n    int len = toExcluded - from;\n    int i;\n    return len <= 15 && len >= 7 &&\n      (i = ip.indexOf('.', from + 1)) > 0 && isValidIpV4Word(ip, from, i) &&\n      (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&\n      (i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&\n      isValidIpV4Word(ip, i + 1, toExcluded);\n  }\n\n  static boolean isValidIpV4Word(CharSequence word, int from, int toExclusive) {\n    int len = toExclusive - from;\n    char c0, c1, c2;\n    if (len < 1 || len > 3 || (c0 = word.charAt(from)) < '0') {\n      return false;\n    }\n    if (len == 3) {\n      return (c1 = word.charAt(from + 1)) >= '0' &&\n        (c2 = word.charAt(from + 2)) >= '0' &&\n        ((c0 <= '1' && c1 <= '9' && c2 <= '9') ||\n          (c0 == '2' && c1 <= '5' && (c2 <= '5' || (c1 < '5' && c2 <= '9'))));\n    }\n    return c0 <= '9' && (len == 1 || isValidNumericChar(word.charAt(from + 1)));\n  }\n\n  static boolean isValidNumericChar(char c) {\n    return c >= '0' && c <= '9';\n  }\n  // End code from io.netty.util.NetUtil 4.1\n\n  // clutter below mainly due to difficulty working with Kryo which cannot handle AutoValue subclass\n  // See https://github.com/openzipkin/zipkin/issues/1879\n  final String serviceName, ipv4, ipv6;\n  final byte[] ipv4Bytes, ipv6Bytes;\n  final int port;\n\n  Endpoint(Builder builder) {\n    serviceName = builder.serviceName;\n    ipv4 = builder.ipv4;\n    ipv4Bytes = builder.ipv4Bytes;\n    ipv6 = builder.ipv6;\n    ipv6Bytes = builder.ipv6Bytes;\n    port = builder.port;\n  }\n\n  Endpoint(SerializedForm serializedForm) {\n    serviceName = serializedForm.serviceName;\n    ipv4 = serializedForm.ipv4;\n    ipv4Bytes = serializedForm.ipv4Bytes;\n    ipv6 = serializedForm.ipv6;\n    ipv6Bytes = serializedForm.ipv6Bytes;\n    port = serializedForm.port;\n  }\n\n  @Override public String toString() {\n    return \"Endpoint{\"\n      + \"serviceName=\" + serviceName + \", \"\n      + \"ipv4=\" + ipv4 + \", \"\n      + \"ipv6=\" + ipv6 + \", \"\n      + \"port=\" + port\n      + \"}\";\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof Endpoint)) return false;\n    Endpoint that = (Endpoint) o;\n    return Objects.equals(serviceName, that.serviceName)\n      && Objects.equals(ipv4, that.ipv4)\n      && Objects.equals(ipv6, that.ipv6)\n      && port == that.port;\n  }\n\n  @Override public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (serviceName == null) ? 0 : serviceName.hashCode();\n    h *= 1000003;\n    h ^= (ipv4 == null) ? 0 : ipv4.hashCode();\n    h *= 1000003;\n    h ^= (ipv6 == null) ? 0 : ipv6.hashCode();\n    h *= 1000003;\n    h ^= port;\n    return h;\n  }\n\n  // As this is an immutable object (no default constructor), defer to a serialization proxy.\n  Object writeReplace() throws ObjectStreamException {\n    return new SerializedForm(this);\n  }\n\n  // TODO: replace this with native proto3 encoding\n  static final class SerializedForm implements Serializable {\n    static final long serialVersionUID = 0L;\n\n    final String serviceName, ipv4, ipv6;\n    final byte[] ipv4Bytes, ipv6Bytes;\n    final int port;\n\n    SerializedForm(Endpoint endpoint) {\n      serviceName = endpoint.serviceName;\n      ipv4 = endpoint.ipv4;\n      ipv4Bytes = endpoint.ipv4Bytes;\n      ipv6 = endpoint.ipv6;\n      ipv6Bytes = endpoint.ipv6Bytes;\n      port = endpoint.port;\n    }\n\n    Object readResolve() throws ObjectStreamException {\n      try {\n        return new Endpoint(this);\n      } catch (IllegalArgumentException e) {\n        throw new StreamCorruptedException(e.getMessage());\n      }\n    }\n  }\n\n  static byte[] getIpv4Bytes(String ipv4) {\n    byte[] result = new byte[4];\n    int pos = 0;\n    for (int i = 0, len = ipv4.length(); i < len; ) {\n      char ch = ipv4.charAt(i++);\n      int octet = ch - '0';\n      if (i == len || (ch = ipv4.charAt(i++)) == '.') {\n        // then we have a single digit octet\n        result[pos++] = (byte) octet;\n        continue;\n      }\n      // push the decimal\n      octet = (octet * 10) + (ch - '0');\n      if (i == len || (ch = ipv4.charAt(i++)) == '.') {\n        // then we have a two digit octet\n        result[pos++] = (byte) octet;\n        continue;\n      }\n      // otherwise, we have a three digit octet\n      octet = (octet * 10) + (ch - '0');\n      result[pos++] = (byte) octet;\n      i++; // skip the dot\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/Span.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.ObjectStreamException;\nimport java.io.Serializable;\nimport java.io.StreamCorruptedException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.TreeMap;\nimport java.util.logging.Logger;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.internal.Nullable;\nimport zipkin2.internal.RecyclableBuffers;\n\nimport static java.lang.String.format;\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.logging.Level.FINEST;\nimport static zipkin2.internal.HexCodec.HEX_DIGITS;\n\n/**\n * A span is a single-host view of an operation. A trace is a series of spans (often RPC calls)\n * which nest to form a latency tree. Spans are in the same trace when they share the same trace ID.\n * The {@link #parentId} field establishes the position of one span in the tree.\n *\n * <p>The root span is where {@link #parentId} is null and usually has the longest {@link\n * #duration} in the trace. However, nested asynchronous work can materialize as child spans whose\n * duration exceed the root span.\n *\n * <p>Spans usually represent remote activity such as RPC calls, or messaging producers and\n * consumers. However, they can also represent in-process activity in any position of the trace. For\n * example, a root span could represent a server receiving an initial client request. A root span\n * could also represent a scheduled job that has no remote context.\n *\n * <p>While span identifiers are packed into longs, they should be treated opaquely. ID encoding is\n * 16 or 32 character lower-hex, to avoid signed interpretation.\n *\n * <h3>Relationship to {@code zipkin.Span}</h3>\n *\n * <p>This type is intended to replace use of {@code zipkin.Span}. Particularly, tracers represent\n * a single-host view of an operation. By making one endpoint implicit for all data, this type does\n * not need to repeat endpoints on each data like {@code zipkin.Span} does. This results in simpler\n * and smaller data.\n */\n//@Immutable\npublic final class Span implements Serializable { // for Spark and Flink jobs\n  static final Endpoint EMPTY_ENDPOINT = Endpoint.newBuilder().build();\n\n  static final int FLAG_DEBUG = 1 << 1;\n  static final int FLAG_DEBUG_SET = 1 << 2;\n  static final int FLAG_SHARED = 1 << 3;\n  static final int FLAG_SHARED_SET = 1 << 4;\n\n  private static final long serialVersionUID = 0L;\n\n  /**\n   * Trace identifier, set on all spans within it.\n   *\n   * <p>Encoded as 16 or 32 lowercase hex characters corresponding to 64 or 128 bits. For example,\n   * a 128bit trace ID looks like {@code 4e441824ec2b6a44ffdc9bb9a6453df3}.\n   *\n   * <p>Some systems downgrade trace identifiers to 64bit by dropping the left-most 16 characters.\n   * For example, {@code 4e441824ec2b6a44ffdc9bb9a6453df3} becomes {@code ffdc9bb9a6453df3}.\n   */\n  public String traceId() {\n    return traceId;\n  }\n\n  /**\n   * The parent's {@link #id} or null if this the root span in a trace.\n   *\n   * <p>This is the same encoding as {@link #id}. For example {@code ffdc9bb9a6453df3}\n   */\n  @Nullable public String parentId() {\n    return parentId;\n  }\n\n  /**\n   * Unique 64bit identifier for this operation within the trace.\n   *\n   * <p>Encoded as 16 lowercase hex characters. For example {@code ffdc9bb9a6453df3}\n   *\n   * <p>A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #id()}).\n   */\n  public String id() {\n    return id;\n  }\n\n  /** Indicates the primary span type. */\n  public enum Kind {\n    CLIENT,\n    SERVER,\n    /**\n     * When present, {@link #timestamp()} is the moment a producer sent a message to a destination.\n     * {@link #duration()} represents delay sending the message, such as batching, while {@link\n     * #remoteEndpoint()} indicates the destination, such as a broker.\n     *\n     * <p>Unlike {@link #CLIENT}, messaging spans never share a span ID. For example, the {@link\n     * #CONSUMER} of the same message has {@link #parentId()} set to this span's {@link #id()}.\n     */\n    PRODUCER,\n    /**\n     * When present, {@link #timestamp()} is the moment a consumer received a message from an\n     * origin. {@link #duration()} represents delay consuming the message, such as from backlog,\n     * while {@link #remoteEndpoint()} indicates the origin, such as a broker.\n     *\n     * <p>Unlike {@link #SERVER}, messaging spans never share a span ID. For example, the {@link\n     * #PRODUCER} of this message is the {@link #parentId()} of this span.\n     */\n    CONSUMER\n  }\n\n  /** When present, used to interpret {@link #remoteEndpoint} */\n  @Nullable public Kind kind() {\n    return kind;\n  }\n\n  /**\n   * Span name in lowercase, rpc method for example.\n   *\n   * <p>Conventionally, when the span name isn't known, name = \"unknown\".\n   */\n  @Nullable public String name() {\n    return name;\n  }\n\n  /**\n   * Epoch microseconds of the start of this span, possibly absent if this an incomplete span.\n   *\n   * <p>This value should be set directly by instrumentation, using the most precise value\n   * possible. For example, {@code gettimeofday} or multiplying {@link System#currentTimeMillis} by\n   * 1000.\n   *\n   * <p>There are three known edge-cases where this could be reported absent:\n   *\n   * <pre><ul>\n   * <li>A span was allocated but never started (ex not yet received a timestamp)</li>\n   * <li>The span's start event was lost</li>\n   * <li>Data about a completed span (ex tags) were sent after the fact</li>\n   * </pre><ul>\n   *\n   * <p>Note: timestamps at or before epoch (0L == 1970) are invalid\n   *\n   * @see #duration()\n   * @see #timestampAsLong()\n   */\n  @Nullable public Long timestamp() {\n    return timestamp > 0 ? timestamp : null;\n  }\n\n  /**\n   * Like {@link #timestamp()} except returns a primitive where zero implies absent.\n   *\n   * <p>Using this method will avoid allocation, so is encouraged when copying data.\n   */\n  public long timestampAsLong() {\n    return timestamp;\n  }\n\n  /**\n   * Measurement in microseconds of the critical path, if known. Durations of less than one\n   * microsecond must be rounded up to 1 microsecond.\n   *\n   * <p>This value should be set directly, as opposed to implicitly via annotation timestamps.\n   * Doing so encourages precision decoupled from problems of clocks, such as skew or NTP updates\n   * causing time to move backwards.\n   *\n   * <p>If this field is persisted as unset, zipkin will continue to work, except duration query\n   * support will be implementation-specific. Similarly, setting this field non-atomically is\n   * implementation-specific.\n   *\n   * <p>This field is i64 vs i32 to support spans longer than 35 minutes.\n   *\n   * @see #durationAsLong()\n   */\n  @Nullable public Long duration() {\n    return duration > 0 ? duration : null;\n  }\n\n  /**\n   * Like {@link #duration()} except returns a primitive where zero implies absent.\n   *\n   * <p>Using this method will avoid allocation, so is encouraged when copying data.\n   */\n  public long durationAsLong() {\n    return duration;\n  }\n\n  /**\n   * The host that recorded this span, primarily for query by service name.\n   *\n   * <p>Instrumentation should always record this and be consistent as possible with the service\n   * name as it is used in search. This is nullable for legacy reasons.\n   */\n  // Nullable for data conversion especially late arriving data which might not have an annotation\n  @Nullable public Endpoint localEndpoint() {\n    return localEndpoint;\n  }\n\n  /**\n   * When an RPC (or messaging) span, indicates the other side of the connection.\n   *\n   * <p>By recording the remote endpoint, your trace will contain network context even if the peer\n   * is not tracing. For example, you can record the IP from the {@code X-Forwarded-For} header or\n   * the service name and socket of a remote peer.\n   */\n  @Nullable public Endpoint remoteEndpoint() {\n    return remoteEndpoint;\n  }\n\n  /**\n   * Events that explain latency with a timestamp. Unlike log statements, annotations are often\n   * short or contain codes: for example \"brave.flush\". Annotations are sorted ascending by\n   * timestamp.\n   */\n  public List<Annotation> annotations() {\n    return annotations;\n  }\n\n  /**\n   * Tags a span with context, usually to support query or aggregation.\n   *\n   * <p>For example, a tag key could be {@code \"http.path\"}.\n   */\n  public Map<String, String> tags() {\n    return tags;\n  }\n\n  /** True is a request to store this span even if it overrides sampling policy. */\n  @Nullable public Boolean debug() {\n    return (flags & FLAG_DEBUG_SET) == FLAG_DEBUG_SET\n      ? (flags & FLAG_DEBUG) == FLAG_DEBUG\n      : null;\n  }\n\n  /**\n   * True if we are contributing to a span started by another tracer (ex on a different host).\n   * Defaults to null. When set, it is expected for {@link #kind()} to be {@link Kind#SERVER}.\n   *\n   * <p>When an RPC trace is client-originated, it will be sampled and the same span ID is used for\n   * the server side. However, the server shouldn't set span.timestamp or duration since it didn't\n   * start the span.\n   */\n  @Nullable public Boolean shared() {\n    return (flags & FLAG_SHARED_SET) == FLAG_SHARED_SET\n      ? (flags & FLAG_SHARED) == FLAG_SHARED\n      : null;\n  }\n\n  @Nullable public String localServiceName() {\n    Endpoint localEndpoint = localEndpoint();\n    return localEndpoint != null ? localEndpoint.serviceName() : null;\n  }\n\n  @Nullable public String remoteServiceName() {\n    Endpoint remoteEndpoint = remoteEndpoint();\n    return remoteEndpoint != null ? remoteEndpoint.serviceName() : null;\n  }\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public Builder toBuilder() {\n    return new Builder(this);\n  }\n\n  public static final class Builder {\n    String traceId, parentId, id;\n    Kind kind;\n    String name;\n    long timestamp, duration; // zero means null\n    Endpoint localEndpoint, remoteEndpoint;\n    ArrayList<Annotation> annotations;\n    TreeMap<String, String> tags;\n    int flags = 0; // bit field for timestamp and duration\n\n    public Builder clear() {\n      traceId = null;\n      parentId = null;\n      id = null;\n      kind = null;\n      name = null;\n      timestamp = 0L;\n      duration = 0L;\n      localEndpoint = null;\n      remoteEndpoint = null;\n      if (annotations != null) annotations.clear();\n      if (tags != null) tags.clear();\n      flags = 0;\n      return this;\n    }\n\n    @Override public Builder clone() {\n      Builder result = new Builder();\n      result.traceId = traceId;\n      result.parentId = parentId;\n      result.id = id;\n      result.kind = kind;\n      result.name = name;\n      result.timestamp = timestamp;\n      result.duration = duration;\n      result.localEndpoint = localEndpoint;\n      result.remoteEndpoint = remoteEndpoint;\n      if (annotations != null) {\n        result.annotations = (ArrayList) annotations.clone();\n      }\n      if (tags != null) {\n        result.tags = (TreeMap) tags.clone();\n      }\n      result.flags = flags;\n      return result;\n    }\n\n    Builder(Span source) {\n      traceId = source.traceId;\n      parentId = source.parentId;\n      id = source.id;\n      kind = source.kind;\n      name = source.name;\n      timestamp = source.timestamp;\n      duration = source.duration;\n      localEndpoint = source.localEndpoint;\n      remoteEndpoint = source.remoteEndpoint;\n      if (!source.annotations.isEmpty()) {\n        annotations = new ArrayList<>(source.annotations.size());\n        annotations.addAll(source.annotations);\n      }\n      if (!source.tags.isEmpty()) {\n        tags = new TreeMap<>();\n        tags.putAll(source.tags);\n      }\n      flags = source.flags;\n    }\n\n    /**\n     * Used to merge multiple incomplete spans representing the same operation on the same host. Do\n     * not use this to merge spans that occur on different hosts.\n     */\n    public Builder merge(Span source) {\n      if (traceId == null) traceId = source.traceId;\n      if (id == null) id = source.id;\n      if (parentId == null) parentId = source.parentId;\n      if (kind == null) kind = source.kind;\n      if (name == null) name = source.name;\n      if (timestamp == 0L) timestamp = source.timestamp;\n      if (duration == 0L) duration = source.duration;\n      if (localEndpoint == null) {\n        localEndpoint = source.localEndpoint;\n      } else if (source.localEndpoint != null) {\n        localEndpoint = localEndpoint.toBuilder().merge(source.localEndpoint).build();\n      }\n      if (remoteEndpoint == null) {\n        remoteEndpoint = source.remoteEndpoint;\n      } else if (source.remoteEndpoint != null) {\n        remoteEndpoint = remoteEndpoint.toBuilder().merge(source.remoteEndpoint).build();\n      }\n      if (!source.annotations.isEmpty()) {\n        if (annotations == null) {\n          annotations = new ArrayList<>(source.annotations.size());\n        }\n        annotations.addAll(source.annotations);\n      }\n      if (!source.tags.isEmpty()) {\n        if (tags == null) tags = new TreeMap<>();\n        tags.putAll(source.tags);\n      }\n      flags = flags | source.flags;\n      return this;\n    }\n\n    @Nullable public Kind kind() {\n      return kind;\n    }\n\n    @Nullable public Endpoint localEndpoint() {\n      return localEndpoint;\n    }\n\n    /**\n     * Sets {@link Span#id()} or throws {@link IllegalArgumentException} if not lower-hex format.\n     */\n    public Builder traceId(String traceId) {\n      this.traceId = normalizeTraceId(traceId);\n      return this;\n    }\n\n    /**\n     * Encodes 64 or 128 bits from the input into a hex trace ID.\n     *\n     * @param high Upper 64bits of the trace ID. Zero means the trace ID is 64-bit.\n     * @param low Lower 64bits of the trace ID.\n     * @throws IllegalArgumentException if both values are zero\n     */\n    public Builder traceId(long high, long low) {\n      if (high == 0L && low == 0L) throw new IllegalArgumentException(\"empty trace ID\");\n      char[] data = RecyclableBuffers.shortStringBuffer();\n      int pos = 0;\n      if (high != 0L) {\n        writeHexLong(data, pos, high);\n        pos += 16;\n      }\n      writeHexLong(data, pos, low);\n      this.traceId = new String(data, 0, high != 0L ? 32 : 16);\n      return this;\n    }\n\n    /** Hex encodes the input as the {@link Span#parentId()} or unsets if the input is zero. */\n    public Builder parentId(long parentId) {\n      this.parentId = parentId != 0L ? toLowerHex(parentId) : null;\n      return this;\n    }\n\n    /**\n     * Sets {@link Span#parentId()} or throws {@link IllegalArgumentException} if not lower-hex\n     * format.\n     */\n    public Builder parentId(@Nullable String parentId) {\n      if (parentId == null) {\n        this.parentId = null;\n        return this;\n      }\n      int length = parentId.length();\n      if (length == 0) throw new IllegalArgumentException(\"parentId is empty\");\n      if (length > 16) throw new IllegalArgumentException(\"parentId.length > 16\");\n      if (validateHexAndReturnZeroPrefix(parentId) == length) {\n        this.parentId = null;\n      } else {\n        this.parentId = length < 16 ? padLeft(parentId, 16) : parentId;\n      }\n      return this;\n    }\n\n    /**\n     * Hex encodes the input as the {@link Span#id()} or throws IllegalArgumentException if the\n     * input is zero.\n     */\n    public Builder id(long id) {\n      if (id == 0L) throw new IllegalArgumentException(\"empty id\");\n      this.id = toLowerHex(id);\n      return this;\n    }\n\n    /** Sets {@link Span#id()} or throws {@link IllegalArgumentException} if not lower-hex format. */\n    public Builder id(String id) {\n      if (id == null) throw new NullPointerException(\"id == null\");\n      int length = id.length();\n      if (length == 0) throw new IllegalArgumentException(\"id is empty\");\n      if (length > 16) throw new IllegalArgumentException(\"id.length > 16\");\n      if (validateHexAndReturnZeroPrefix(id) == 16) {\n        throw new IllegalArgumentException(\"id is all zeros\");\n      }\n      this.id = length < 16 ? padLeft(id, 16) : id;\n      return this;\n    }\n\n    /** Sets {@link Span#kind} */\n    public Builder kind(@Nullable Kind kind) {\n      this.kind = kind;\n      return this;\n    }\n\n    /** Sets {@link Span#name} */\n    public Builder name(@Nullable String name) {\n      this.name = name == null || name.isEmpty() ? null : name.toLowerCase(Locale.ROOT);\n      return this;\n    }\n\n    /** Sets {@link Span#timestampAsLong()} */\n    public Builder timestamp(long timestamp) {\n      if (timestamp < 0L) timestamp = 0L;\n      this.timestamp = timestamp;\n      return this;\n    }\n\n    /** Sets {@link Span#timestamp()} */\n    public Builder timestamp(@Nullable Long timestamp) {\n      if (timestamp == null || timestamp < 0L) timestamp = 0L;\n      this.timestamp = timestamp;\n      return this;\n    }\n\n    /** Sets {@link Span#durationAsLong()} */\n    public Builder duration(long duration) {\n      if (duration < 0L) duration = 0L;\n      this.duration = duration;\n      return this;\n    }\n\n    /** Sets {@link Span#duration()} */\n    public Builder duration(@Nullable Long duration) {\n      if (duration == null || duration < 0L) duration = 0L;\n      this.duration = duration;\n      return this;\n    }\n\n    /** Sets {@link Span#localEndpoint} */\n    public Builder localEndpoint(@Nullable Endpoint localEndpoint) {\n      if (EMPTY_ENDPOINT.equals(localEndpoint)) localEndpoint = null;\n      this.localEndpoint = localEndpoint;\n      return this;\n    }\n\n    /** Sets {@link Span#remoteEndpoint} */\n    public Builder remoteEndpoint(@Nullable Endpoint remoteEndpoint) {\n      if (EMPTY_ENDPOINT.equals(remoteEndpoint)) remoteEndpoint = null;\n      this.remoteEndpoint = remoteEndpoint;\n      return this;\n    }\n\n    /** Sets {@link Span#annotations} */\n    public Builder addAnnotation(long timestamp, String value) {\n      if (annotations == null) annotations = new ArrayList<>(2);\n      annotations.add(Annotation.create(timestamp, value));\n      return this;\n    }\n\n    /** Sets {@link Span#annotations} */\n    public Builder clearAnnotations() {\n      if (annotations == null) return this;\n      annotations.clear();\n      return this;\n    }\n\n    /** Sets {@link Span#tags} */\n    public Builder putTag(String key, String value) {\n      if (tags == null) tags = new TreeMap<>();\n      if (key == null) throw new NullPointerException(\"key == null\");\n      if (value == null) throw new NullPointerException(\"value of \" + key + \" == null\");\n      this.tags.put(key, value);\n      return this;\n    }\n\n    /** Sets {@link Span#tags} */\n    public Builder clearTags() {\n      if (tags == null) return this;\n      tags.clear();\n      return this;\n    }\n\n    /** Sets {@link Span#debug} */\n    public Builder debug(boolean debug) {\n      flags |= FLAG_DEBUG_SET;\n      if (debug) {\n        flags |= FLAG_DEBUG;\n      } else {\n        flags &= ~FLAG_DEBUG;\n      }\n      return this;\n    }\n\n    /** Sets {@link Span#debug} */\n    public Builder debug(@Nullable Boolean debug) {\n      if (debug != null) return debug((boolean) debug);\n      flags &= ~(FLAG_DEBUG_SET | FLAG_DEBUG);\n      return this;\n    }\n\n    /** Sets {@link Span#shared} */\n    public Builder shared(boolean shared) {\n      flags |= FLAG_SHARED_SET;\n      if (shared) {\n        flags |= FLAG_SHARED;\n      } else {\n        flags &= ~FLAG_SHARED;\n      }\n      return this;\n    }\n\n    /** Sets {@link Span#shared} */\n    public Builder shared(@Nullable Boolean shared) {\n      if (shared != null) return shared((boolean) shared);\n      flags &= ~(FLAG_SHARED_SET | FLAG_SHARED);\n      return this;\n    }\n\n    public Span build() {\n      String missing = \"\";\n      if (traceId == null) missing += \" traceId\";\n      if (id == null) missing += \" id\";\n      if (!missing.isEmpty()) throw new IllegalStateException(\"Missing :\" + missing);\n      if (id.equals(parentId)) { // edge case, so don't require a logger field\n        Logger logger = Logger.getLogger(Span.class.getName());\n        if (logger.isLoggable(FINEST)) {\n          logger.fine(format(\"undoing circular dependency: traceId=%s, spanId=%s\", traceId, id));\n        }\n        parentId = null;\n      }\n      // shared is for the server side, unset it if accidentally set on the client side\n      if ((flags & FLAG_SHARED) == FLAG_SHARED && kind == Kind.CLIENT) {\n        Logger logger = Logger.getLogger(Span.class.getName());\n        if (logger.isLoggable(FINEST)) {\n          logger.fine(format(\"removing shared flag on client: traceId=%s, spanId=%s\", traceId, id));\n        }\n        shared(null);\n      }\n      return new Span(this);\n    }\n\n    Builder() {\n    }\n  }\n\n  @Override public String toString() {\n    return new String(SpanBytesEncoder.JSON_V2.encode(this), UTF_8);\n  }\n\n  /**\n   * Returns a valid lower-hex trace ID, padded left as needed to 16 or 32 characters.\n   *\n   * @throws IllegalArgumentException if oversized or not lower-hex\n   */\n  public static String normalizeTraceId(String traceId) {\n    if (traceId == null) throw new NullPointerException(\"traceId == null\");\n    int length = traceId.length();\n    if (length == 0) throw new IllegalArgumentException(\"traceId is empty\");\n    if (length > 32) throw new IllegalArgumentException(\"traceId.length > 32\");\n    int zeros = validateHexAndReturnZeroPrefix(traceId);\n    if (zeros == length) throw new IllegalArgumentException(\"traceId is all zeros\");\n    if (length == 32 || length == 16) {\n      if (length == 32 && zeros >= 16) return traceId.substring(16);\n      return traceId;\n    } else if (length < 16) {\n      return padLeft(traceId, 16);\n    } else {\n      return padLeft(traceId, 32);\n    }\n  }\n\n  static final String THIRTY_TWO_ZEROS;\n  static {\n    char[] zeros = new char[32];\n    Arrays.fill(zeros, '0');\n    THIRTY_TWO_ZEROS = new String(zeros);\n  }\n\n  static String padLeft(String id, int desiredLength) {\n    int length = id.length();\n    int numZeros = desiredLength - length;\n\n    char[] data = RecyclableBuffers.shortStringBuffer();\n    THIRTY_TWO_ZEROS.getChars(0, numZeros, data, 0);\n    id.getChars(0, length, data, numZeros);\n\n    return new String(data, 0, desiredLength);\n  }\n\n  static String toLowerHex(long v) {\n    char[] data = RecyclableBuffers.shortStringBuffer();\n    writeHexLong(data, 0, v);\n    return new String(data, 0, 16);\n  }\n\n  /** Inspired by {@code okio.Buffer.writeLong} */\n  static void writeHexLong(char[] data, int pos, long v) {\n    writeHexByte(data, pos + 0, (byte) ((v >>> 56L) & 0xff));\n    writeHexByte(data, pos + 2, (byte) ((v >>> 48L) & 0xff));\n    writeHexByte(data, pos + 4, (byte) ((v >>> 40L) & 0xff));\n    writeHexByte(data, pos + 6, (byte) ((v >>> 32L) & 0xff));\n    writeHexByte(data, pos + 8, (byte) ((v >>> 24L) & 0xff));\n    writeHexByte(data, pos + 10, (byte) ((v >>> 16L) & 0xff));\n    writeHexByte(data, pos + 12, (byte) ((v >>> 8L) & 0xff));\n    writeHexByte(data, pos + 14, (byte) (v & 0xff));\n  }\n\n  static void writeHexByte(char[] data, int pos, byte b) {\n    data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf];\n    data[pos + 1] = HEX_DIGITS[b & 0xf];\n  }\n\n  static int validateHexAndReturnZeroPrefix(String id) {\n    int zeros = 0;\n    boolean inZeroPrefix = id.charAt(0) == '0';\n    for (int i = 0, length = id.length(); i < length; i++) {\n      char c = id.charAt(i);\n      if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {\n        throw new IllegalArgumentException(id + \" should be lower-hex encoded with no prefix\");\n      }\n      if (c != '0') {\n        inZeroPrefix = false;\n      } else if (inZeroPrefix) {\n        zeros++;\n      }\n    }\n    return zeros;\n  }\n\n  static <T extends Comparable<? super T>> List<T> sortedList(@Nullable List<T> in) {\n    if (in == null || in.isEmpty()) return Collections.emptyList();\n    if (in.size() == 1) return Collections.singletonList(in.get(0));\n    Object[] array = in.toArray();\n    Arrays.sort(array);\n\n    // dedupe\n    int j = 0, i = 1;\n    while (i < array.length) {\n      if (!array[i].equals(array[j])) {\n        array[++j] = array[i];\n      }\n      i++;\n    }\n\n    List result = Arrays.asList(i == j + 1 ? array : Arrays.copyOf(array, j + 1));\n    return Collections.unmodifiableList(result);\n  }\n\n  // Custom impl to reduce GC churn and Kryo which cannot handle AutoValue subclass\n  // See https://github.com/openzipkin/zipkin/issues/1879\n  final String traceId, parentId, id;\n  final Kind kind;\n  final String name;\n  final long timestamp, duration; // zero means null, saving 2 object references\n  final Endpoint localEndpoint, remoteEndpoint;\n  final List<Annotation> annotations;\n  final Map<String, String> tags;\n  final int flags; // bit field for timestamp and duration, saving 2 object references\n\n  Span(Builder builder) {\n    traceId = builder.traceId;\n    // prevent self-referencing spans\n    parentId = builder.id.equals(builder.parentId) ? null : builder.parentId;\n    id = builder.id;\n    kind = builder.kind;\n    name = builder.name;\n    timestamp = builder.timestamp;\n    duration = builder.duration;\n    localEndpoint = builder.localEndpoint;\n    remoteEndpoint = builder.remoteEndpoint;\n    annotations = sortedList(builder.annotations);\n    tags = builder.tags == null\n      ? Collections.emptyMap()\n      : new LinkedHashMap<>(builder.tags);\n    flags = builder.flags;\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof Span)) return false;\n    Span that = (Span) o;\n    return traceId.equals(that.traceId)\n      && Objects.equals(parentId, that.parentId)\n      && id.equals(that.id)\n      && Objects.equals(kind, that.kind)\n      && Objects.equals(name, that.name)\n      && timestamp == that.timestamp\n      && duration == that.duration\n      && Objects.equals(localEndpoint, that.localEndpoint)\n      && Objects.equals(remoteEndpoint, that.remoteEndpoint)\n      && annotations.equals(that.annotations)\n      && tags.equals(that.tags)\n      && flags == that.flags;\n  }\n\n  @Override public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= traceId.hashCode();\n    h *= 1000003;\n    h ^= (parentId == null) ? 0 : parentId.hashCode();\n    h *= 1000003;\n    h ^= id.hashCode();\n    h *= 1000003;\n    h ^= (kind == null) ? 0 : kind.hashCode();\n    h *= 1000003;\n    h ^= (name == null) ? 0 : name.hashCode();\n    h *= 1000003;\n    h ^= (int) (h ^ ((timestamp >>> 32) ^ timestamp));\n    h *= 1000003;\n    h ^= (int) (h ^ ((duration >>> 32) ^ duration));\n    h *= 1000003;\n    h ^= (localEndpoint == null) ? 0 : localEndpoint.hashCode();\n    h *= 1000003;\n    h ^= (remoteEndpoint == null) ? 0 : remoteEndpoint.hashCode();\n    h *= 1000003;\n    h ^= annotations.hashCode();\n    h *= 1000003;\n    h ^= tags.hashCode();\n    h *= 1000003;\n    h ^= flags;\n    return h;\n  }\n\n  // This is an immutable object, and our encoder is faster than java's: use a serialization proxy.\n  Object writeReplace() throws ObjectStreamException {\n    return new SerializedForm(SpanBytesEncoder.PROTO3.encode(this));\n  }\n\n  private static final class SerializedForm implements Serializable {\n    private static final long serialVersionUID = 0L;\n\n    final byte[] bytes;\n\n    SerializedForm(byte[] bytes) {\n      this.bytes = bytes;\n    }\n\n    Object readResolve() throws ObjectStreamException {\n      try {\n        return SpanBytesDecoder.PROTO3.decodeOne(bytes);\n      } catch (IllegalArgumentException e) {\n        throw new StreamCorruptedException(e.getMessage());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/SpanBytesDecoderDetector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.nio.ByteBuffer;\nimport zipkin2.codec.BytesDecoder;\nimport zipkin2.codec.SpanBytesDecoder;\n\n/**\n * Detecting decoder used in transports which don't include means to identify the type of the data.\n *\n * <p>For example, we can identify the encoding and also the format in http via the request path\n * and content-type. However, in Kafka it could be that folks send mixed Zipkin data without\n * identifying its format. For example, Kafka historically has no content-type and users don't\n * always segregate different queues by instrumentation format.\n */\n// In TBinaryProtocol encoding, the first byte is the TType, in a range 0-16\n// .. If the first byte isn't in that range, it isn't a thrift.\n//\n// When byte(0) == '[' (91), assume it is a list of json-encoded spans\n//\n// When byte(0) == 10, assume it is a proto3-encoded span or trace ID field\n//\n// When byte(0) <= 16, assume it is a TBinaryProtocol-encoded thrift\n// .. When serializing a Span (Struct), the first byte will be the type of a field\n// .. When serializing a List[ThriftSpan], the first byte is the member type, TType.STRUCT(12)\n// .. As ThriftSpan has no STRUCT fields: so, if the first byte is TType.STRUCT(12), it is a list.\npublic final class SpanBytesDecoderDetector {\n  /**\n   * Zipkin v2 json will have \"localEndpoint\" or \"remoteEndpoint\" fields, and others won't.\n   *\n   * <p>Note: Technically, it is also possible that one can thwart this by creating an binary\n   * annotation of type string with a name or value literally ending in Endpoint. This would be\n   * strange, especially as the convention to identify a local endpoint is the key \"lc\". To prevent\n   * a secondary check, this scenario is also ignored.\n   */\n  static final byte[] ENDPOINT_FIELD_SUFFIX = {'E', 'n', 'd', 'p', 'o', 'i', 'n', 't', '\"'};\n\n  /**\n   * Technically, it is possible to have a v2 span with no endpoints. This should catch the case\n   * where someone reported a tag without reporting the \"localEndpoint\".\n   *\n   * <p>Note: we don't check for annotations as that exists in both v1 and v2 formats.\n   */\n  static final byte[] TAGS_FIELD = {'\"', 't', 'a', 'g', 's', '\"'};\n\n  /**\n   * Throws {@link IllegalArgumentException} if the input isn't a v1 json or thrift single-span\n   * message\n   */\n  public static BytesDecoder<Span> decoderForMessage(byte[] span) {\n    BytesDecoder<Span> decoder = detectDecoder(ByteBuffer.wrap(span));\n    if (span[0] == 12 /* List[ThriftSpan] */ || span[0] == '[') {\n      throw new IllegalArgumentException(\"Expected json or thrift object, not list encoding\");\n    }\n    if (decoder == SpanBytesDecoder.JSON_V2 || decoder == SpanBytesDecoder.PROTO3) {\n      throw new UnsupportedOperationException(\"v2 formats should only be used with list messages\");\n    }\n    return decoder;\n  }\n\n  /**\n   * Throws {@link IllegalArgumentException} if the input isn't a json, proto3 or thrift list\n   * message.\n   */\n  public static BytesDecoder<Span> decoderForListMessage(byte[] spans) {\n    return decoderForListMessage(ByteBuffer.wrap(spans));\n  }\n\n  public static BytesDecoder<Span> decoderForListMessage(ByteBuffer spans) {\n    BytesDecoder<Span> decoder = detectDecoder(spans);\n    byte first = spans.get(spans.position());\n    if (first != 12 /* List[ThriftSpan] */\n        && first != 11 /* openzipkin/zipkin-reporter-java#133 */\n        && !protobuf3(spans) && first != '[') {\n      throw new IllegalArgumentException(\"Expected json, proto3 or thrift list encoding\");\n    }\n    return decoder;\n  }\n\n  /** @throws IllegalArgumentException if the input isn't a json or thrift list or object. */\n  static BytesDecoder<Span> detectDecoder(ByteBuffer bytes) {\n    byte first = bytes.get(bytes.position());\n    if (first <= 16) { // binary format\n      if (protobuf3(bytes)) return SpanBytesDecoder.PROTO3;\n      return SpanBytesDecoder.THRIFT; /* the first byte is the TType, in a range 0-16 */\n    } else if (first != '[' && first != '{') {\n      throw new IllegalArgumentException(\"Could not detect the span format\");\n    }\n    if (contains(bytes, ENDPOINT_FIELD_SUFFIX)) return SpanBytesDecoder.JSON_V2;\n    if (contains(bytes, TAGS_FIELD)) return SpanBytesDecoder.JSON_V2;\n    return SpanBytesDecoder.JSON_V1;\n  }\n\n  static boolean contains(ByteBuffer bytes, byte[] subsequence) {\n    bytes:\n    for (int i = 0; i < bytes.remaining() - subsequence.length + 1; i++) {\n      for (int j = 0; j < subsequence.length; j++) {\n        if (bytes.get(bytes.position() + i + j) != subsequence[j]) {\n          continue bytes;\n        }\n      }\n      return true;\n    }\n    return false;\n  }\n\n  /* span key or trace ID key */\n  static boolean protobuf3(ByteBuffer bytes) {\n    // varint follows and won't be zero\n    return bytes.get(bytes.position()) == 10 && bytes.get(bytes.position() + 1) != 0;\n  }\n\n  SpanBytesDecoderDetector() {\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/BytesDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.internal.Nullable;\n\n/**\n * This type accepts a collection that receives decoded elements.\n *\n * <pre>{@code\n * ArrayList<Span> out = new ArrayList<>();\n * SpanBytesDecoder.JSON_V2.decodeList(spans, out)\n * }</pre>\n *\n * @param <T> type of the object to deserialize\n */\n// This receives a collection, not a function because it profiles 10% faster and all use cases are\n// collections. This receives collection as opposed to list as zipkin-dependencies decodes into a\n// set to dedupe redundant messages.\npublic interface BytesDecoder<T> {\n  Encoding encoding();\n\n  /**\n   * This is used seldom as the canonical message form is a {@link #decodeList(byte[], Collection)\n   * list}.\n   *\n   * <p>Note: multiple elements can be consumed from a single serialized object. For example, if the\n   * input is Zipkin v1, the list might receive two elements if the serialized object was a shared\n   * span.\n   *\n   * @param serialized a single message, for example a json object\n   * @return true if an element was decoded\n   * @throws {@linkplain IllegalArgumentException} if the type couldn't be decoded\n   */\n  // used by zipkin-dependencies which reads elements one-at-a-time from ES documents\n  boolean decode(byte[] serialized, Collection<T> out);\n\n  /** Visible for testing. This returns the first element parsed from the serialized object or null */\n  @Nullable T decodeOne(byte[] serialized);\n\n  /** Returns {@code true} if an element was decoded or throws {@link IllegalArgumentException}. */\n  boolean decodeList(byte[] serialized, Collection<T> out);\n\n  /** Convenience method for {@link #decodeList(byte[], Collection)} */\n  List<T> decodeList(byte[] serialized);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/BytesEncoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.List;\n\n/**\n * Utility for encoding one or more elements of a type into a byte array.\n *\n * @param <T> type of the object to encode\n */\npublic interface BytesEncoder<T> {\n  Encoding encoding();\n\n  int sizeInBytes(T input);\n\n  /** Serializes an object into its binary form. */\n  byte[] encode(T input);\n\n  /** Serializes a list of objects into their binary form. */\n  byte[] encodeList(List<T> input);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/DependencyLinkBytesDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.DependencyLink;\nimport zipkin2.internal.JsonCodec;\nimport zipkin2.internal.JsonCodec.JsonReader;\nimport zipkin2.internal.JsonCodec.JsonReaderAdapter;\nimport zipkin2.internal.Nullable;\nimport zipkin2.internal.ReadBuffer;\n\npublic enum DependencyLinkBytesDecoder implements BytesDecoder<DependencyLink> {\n  JSON_V1 {\n    @Override public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override public boolean decode(byte[] link, Collection<DependencyLink> out) {\n      return JsonCodec.read(READER, ReadBuffer.wrap(link), out);\n    }\n\n    @Override @Nullable public DependencyLink decodeOne(byte[] link) {\n      return JsonCodec.readOne(READER, ReadBuffer.wrap(link));\n    }\n\n    @Override public boolean decodeList(byte[] links, Collection<DependencyLink> out) {\n      return JsonCodec.readList(READER, ReadBuffer.wrap(links), out);\n    }\n\n    @Override public List<DependencyLink> decodeList(byte[] links) {\n      List<DependencyLink> out = new ArrayList<>();\n      decodeList(links, out);\n      return out;\n    }\n  };\n\n  static final JsonReaderAdapter<DependencyLink> READER = new JsonReaderAdapter<DependencyLink>() {\n    @Override public DependencyLink fromJson(JsonReader reader) throws IOException {\n      DependencyLink.Builder result = DependencyLink.newBuilder();\n      reader.beginObject();\n      while (reader.hasNext()) {\n        String nextName = reader.nextName();\n        if (nextName.equals(\"parent\")) {\n          result.parent(reader.nextString());\n        } else if (nextName.equals(\"child\")) {\n          result.child(reader.nextString());\n        } else if (nextName.equals(\"callCount\")) {\n          result.callCount(reader.nextLong());\n        } else if (nextName.equals(\"errorCount\")) {\n          result.errorCount(reader.nextLong());\n        } else {\n          reader.skipValue();\n        }\n      }\n      reader.endObject();\n      return result.build();\n    }\n\n    @Override public String toString() {\n      return \"DependencyLink\";\n    }\n  };\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/DependencyLinkBytesEncoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.List;\nimport zipkin2.DependencyLink;\nimport zipkin2.internal.JsonCodec;\nimport zipkin2.internal.WriteBuffer;\nimport zipkin2.internal.WriteBuffer.Writer;\n\nimport static zipkin2.internal.JsonEscaper.jsonEscape;\nimport static zipkin2.internal.JsonEscaper.jsonEscapedSizeInBytes;\nimport static zipkin2.internal.WriteBuffer.asciiSizeInBytes;\n\npublic enum DependencyLinkBytesEncoder implements BytesEncoder<DependencyLink> {\n  JSON_V1 {\n    @Override public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override public int sizeInBytes(DependencyLink input) {\n      return WRITER.sizeInBytes(input);\n    }\n\n    @Override public byte[] encode(DependencyLink link) {\n      return JsonCodec.write(WRITER, link);\n    }\n\n    @Override public byte[] encodeList(List<DependencyLink> links) {\n      return JsonCodec.writeList(WRITER, links);\n    }\n  };\n\n  static final Writer<DependencyLink> WRITER = new Writer<DependencyLink>() {\n    @Override public int sizeInBytes(DependencyLink value) {\n      int sizeInBytes = 37; // {\"parent\":\"\",\"child\":\"\",\"callCount\":}\n      sizeInBytes += jsonEscapedSizeInBytes(value.parent());\n      sizeInBytes += jsonEscapedSizeInBytes(value.child());\n      sizeInBytes += asciiSizeInBytes(value.callCount());\n      if (value.errorCount() > 0) {\n        sizeInBytes += 14; // ,\"errorCount\":\n        sizeInBytes += asciiSizeInBytes(value.errorCount());\n      }\n      return sizeInBytes;\n    }\n\n    @Override public void write(DependencyLink value, WriteBuffer b) {\n      b.writeAscii(\"{\\\"parent\\\":\\\"\");\n      b.writeUtf8(jsonEscape(value.parent()));\n      b.writeAscii(\"\\\",\\\"child\\\":\\\"\");\n      b.writeUtf8(jsonEscape(value.child()));\n      b.writeAscii(\"\\\",\\\"callCount\\\":\");\n      b.writeAscii(value.callCount());\n      if (value.errorCount() > 0) {\n        b.writeAscii(\",\\\"errorCount\\\":\");\n        b.writeAscii(value.errorCount());\n      }\n      b.writeByte('}');\n    }\n\n    @Override public String toString() {\n      return \"DependencyLink\";\n    }\n  };\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/Encoding.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.List;\n\n// ZIPKIN3 make this not an enum as it prevents non-standard encoding, for example reporting to\n// DataDog which has a message pack encoding.\npublic enum Encoding {\n  JSON {\n    /** Encoding overhead of a single element is brackets */\n    @Override public int listSizeInBytes(int encodedSizeInBytes) {\n      return 2 + encodedSizeInBytes;\n    }\n\n    /** Encoding overhead is brackets and a comma for each span over 1 */\n    @Override public int listSizeInBytes(List<byte[]> values) {\n      int sizeInBytes = 2; // brackets\n      for (int i = 0, length = values.size(); i < length; ) {\n        sizeInBytes += values.get(i++).length;\n        if (i < length) sizeInBytes++;\n      }\n      return sizeInBytes;\n    }\n  },\n  /**\n   * The first format of Zipkin was TBinaryProtocol, big-endian thrift. It is no longer used, but\n   * defined here to allow collectors to support reading old data.\n   *\n   * <p>The message's binary data includes a list header followed by N spans serialized in\n   * TBinaryProtocol\n   *\n   * @deprecated this format is deprecated in favor of json or proto3\n   */\n  @Deprecated\n  THRIFT {\n    /** Encoding overhead is thrift type plus 32-bit length prefix */\n    @Override public int listSizeInBytes(int encodedSizeInBytes) {\n      return 5 + encodedSizeInBytes;\n    }\n\n    /** Encoding overhead is thrift type plus 32-bit length prefix */\n    @Override public int listSizeInBytes(List<byte[]> values) {\n      int sizeInBytes = 5;\n      for (byte[] value : values) {\n        sizeInBytes += value.length;\n      }\n      return sizeInBytes;\n    }\n  },\n  /**\n   * Repeated (type 2) fields are length-prefixed. A list is a concatenation of fields with no\n   * additional overhead.\n   *\n   * <p>See https://developers.google.com/protocol-buffers/docs/encoding#optional\n   */\n  PROTO3 {\n    /** Returns the input as it is assumed to be length-prefixed field from a protobuf message */\n    @Override public int listSizeInBytes(int encodedSizeInBytes) {\n      return encodedSizeInBytes;\n    }\n\n    /** Returns a concatenation of sizes */\n    @Override public int listSizeInBytes(List<byte[]> values) {\n      int sizeInBytes = 0;\n      for (int i = 0, length = values.size(); i < length; ) {\n        sizeInBytes += values.get(i++).length;\n      }\n      return sizeInBytes;\n    }\n  };\n\n  /** Like {@link #listSizeInBytes(List)}, except for a single element. */\n  public abstract int listSizeInBytes(int encodedSizeInBytes);\n\n  public abstract int listSizeInBytes(List<byte[]> values);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/SpanBytesDecoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.Span;\nimport zipkin2.internal.JsonCodec;\nimport zipkin2.internal.Nullable;\nimport zipkin2.internal.Proto3Codec;\nimport zipkin2.internal.ReadBuffer;\nimport zipkin2.internal.ThriftCodec;\nimport zipkin2.internal.V1JsonSpanReader;\nimport zipkin2.internal.V2SpanReader;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\n/** This is separate from {@link SpanBytesEncoder}, as it isn't needed for instrumentation */\n@SuppressWarnings(\"ImmutableEnumChecker\") // because span is immutable\npublic enum SpanBytesDecoder implements BytesDecoder<Span> {\n  /** Corresponds to the Zipkin v1 json format */\n  JSON_V1 {\n    @Override public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override public boolean decode(byte[] span, Collection<Span> out) { // ex DependencyLinker\n      Span result = decodeOne(ReadBuffer.wrap(span));\n      if (result == null) return false;\n      out.add(result);\n      return true;\n    }\n\n    @Override public boolean decodeList(byte[] spans, Collection<Span> out) { // ex getTrace\n      return new V1JsonSpanReader().readList(ReadBuffer.wrap(spans), out);\n    }\n\n    @Override public boolean decodeList(ByteBuffer spans, Collection<Span> out) {\n      return new V1JsonSpanReader().readList(ReadBuffer.wrapUnsafe(spans), out);\n    }\n\n    @Override @Nullable public Span decodeOne(byte[] span) {\n      return decodeOne(ReadBuffer.wrap(span));\n    }\n\n    @Override @Nullable public Span decodeOne(ByteBuffer span) {\n      return decodeOne(ReadBuffer.wrapUnsafe(span));\n    }\n\n    Span decodeOne(ReadBuffer buffer) {\n      V1Span v1 = JsonCodec.readOne(new V1JsonSpanReader(), buffer);\n      List<Span> out = new ArrayList<>(1);\n      V1SpanConverter.create().convert(v1, out);\n      return out.get(0);\n    }\n\n    @Override public List<Span> decodeList(byte[] spans) {\n      return doDecodeList(this, spans);\n    }\n\n    @Override public List<Span> decodeList(ByteBuffer spans) {\n      return doDecodeList(this, spans);\n    }\n  },\n  /** Corresponds to the Zipkin v1 thrift format */\n  THRIFT {\n    @Override public Encoding encoding() {\n      return Encoding.THRIFT;\n    }\n\n    @Override public boolean decode(byte[] span, Collection<Span> out) { // ex DependencyLinker\n      return ThriftCodec.read(ReadBuffer.wrap(span), out);\n    }\n\n    @Override public boolean decodeList(byte[] spans, Collection<Span> out) { // ex getTrace\n      return ThriftCodec.readList(ReadBuffer.wrap(spans), out);\n    }\n\n    @Override public boolean decodeList(ByteBuffer spans, Collection<Span> out) {\n      return ThriftCodec.readList(ReadBuffer.wrapUnsafe(spans), out);\n    }\n\n    @Override @Nullable public Span decodeOne(byte[] span) {\n      return ThriftCodec.readOne(ReadBuffer.wrap(span));\n    }\n\n    @Override @Nullable public Span decodeOne(ByteBuffer span) {\n      return ThriftCodec.readOne(ReadBuffer.wrapUnsafe(span));\n    }\n\n    @Override public List<Span> decodeList(byte[] spans) {\n      return doDecodeList(this, spans);\n    }\n\n    @Override public List<Span> decodeList(ByteBuffer spans) {\n      return doDecodeList(this, spans);\n    }\n  },\n  /** Corresponds to the Zipkin v2 json format */\n  JSON_V2 {\n    @Override public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override public boolean decode(byte[] span, Collection<Span> out) { // ex DependencyLinker\n      return JsonCodec.read(new V2SpanReader(), ReadBuffer.wrap(span), out);\n    }\n\n    @Override public boolean decodeList(byte[] spans, Collection<Span> out) { // ex getTrace\n      return JsonCodec.readList(new V2SpanReader(), ReadBuffer.wrap(spans), out);\n    }\n\n    @Override public boolean decodeList(ByteBuffer spans, Collection<Span> out) {\n      return JsonCodec.readList(new V2SpanReader(), ReadBuffer.wrapUnsafe(spans), out);\n    }\n\n    @Override @Nullable public Span decodeOne(byte[] span) {\n      return JsonCodec.readOne(new V2SpanReader(), ReadBuffer.wrap(span));\n    }\n\n    @Override @Nullable public Span decodeOne(ByteBuffer span) {\n      return JsonCodec.readOne(new V2SpanReader(), ReadBuffer.wrapUnsafe(span));\n    }\n\n    @Override public List<Span> decodeList(byte[] spans) {\n      return doDecodeList(this, spans);\n    }\n\n    @Override public List<Span> decodeList(ByteBuffer spans) {\n      return doDecodeList(this, spans);\n    }\n  },\n  PROTO3 {\n    @Override public Encoding encoding() {\n      return Encoding.PROTO3;\n    }\n\n    @Override public boolean decode(byte[] span, Collection<Span> out) { // ex DependencyLinker\n      return Proto3Codec.read(ReadBuffer.wrap(span), out);\n    }\n\n    @Override public boolean decodeList(byte[] spans, Collection<Span> out) { // ex getTrace\n      return Proto3Codec.readList(ReadBuffer.wrap(spans), out);\n    }\n\n    @Override public boolean decodeList(ByteBuffer spans, Collection<Span> out) {\n      return Proto3Codec.readList(ReadBuffer.wrapUnsafe(spans), out);\n    }\n\n    @Override @Nullable public Span decodeOne(byte[] span) {\n      return Proto3Codec.readOne(ReadBuffer.wrap(span));\n    }\n\n    @Override @Nullable public Span decodeOne(ByteBuffer span) {\n      return Proto3Codec.readOne(ReadBuffer.wrapUnsafe(span));\n    }\n\n    @Override public List<Span> decodeList(byte[] spans) {\n      return doDecodeList(this, spans);\n    }\n\n    @Override public List<Span> decodeList(ByteBuffer spans) {\n      return doDecodeList(this, spans);\n    }\n  };\n\n  /**\n   * ByteBuffer implementation of {@link #decodeList(byte[])}.\n   *\n   * <p>Note: only use this when it is ok to modify the underlying {@link ByteBuffer#array()}.\n   */\n  public abstract boolean decodeList(ByteBuffer spans, Collection<Span> out);\n\n  /**\n   * ByteBuffer implementation of {@link #decodeList(byte[])}.\n   *\n   * <p>Note: only use this when it is ok to modify the underlying {@link ByteBuffer#array()}.\n   */\n  public abstract List<Span> decodeList(ByteBuffer spans);\n\n  /**\n   * ByteBuffer implementation of {@link #decodeOne(byte[])}\n   *\n   * <p>Note: only use this when it is ok to modify the underlying {@link ByteBuffer#array()}.\n   */\n  @Nullable public abstract Span decodeOne(ByteBuffer span);\n\n  static List<Span> doDecodeList(SpanBytesDecoder decoder, byte[] spans) {\n    List<Span> out = new ArrayList<>();\n    decoder.decodeList(spans, out);\n    return out;\n  }\n\n  static List<Span> doDecodeList(SpanBytesDecoder decoder, ByteBuffer spans) {\n    List<Span> out = new ArrayList<>();\n    decoder.decodeList(spans, out);\n    return out;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/codec/SpanBytesEncoder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.List;\nimport zipkin2.Span;\nimport zipkin2.internal.JsonCodec;\nimport zipkin2.internal.Proto3Codec;\nimport zipkin2.internal.V1JsonSpanWriter;\nimport zipkin2.internal.V1ThriftSpanWriter;\nimport zipkin2.internal.V2SpanWriter;\n\n/** Limited interface needed by those writing span reporters */\n@SuppressWarnings(\"ImmutableEnumChecker\") // because span is immutable\npublic enum SpanBytesEncoder implements BytesEncoder<Span> {\n  /** Corresponds to the Zipkin v1 json format (with tags as binary annotations) */\n  JSON_V1 {\n    @Override\n    public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override\n    public int sizeInBytes(Span input) {\n      return new V1JsonSpanWriter().sizeInBytes(input);\n    }\n\n    @Override\n    public byte[] encode(Span span) {\n      return JsonCodec.write(new V1JsonSpanWriter(), span);\n    }\n\n    @Override\n    public byte[] encodeList(List<Span> spans) {\n      return JsonCodec.writeList(new V1JsonSpanWriter(), spans);\n    }\n\n    @Override\n    public int encodeList(List<Span> spans, byte[] out, int pos) {\n      return JsonCodec.writeList(new V1JsonSpanWriter(), spans, out, pos);\n    }\n  },\n  /** Corresponds to the Zipkin v1 thrift format */\n  THRIFT {\n    @Override\n    public Encoding encoding() {\n      return Encoding.THRIFT;\n    }\n\n    @Override\n    public int sizeInBytes(Span input) {\n      return new V1ThriftSpanWriter().sizeInBytes(input);\n    }\n\n    @Override\n    public byte[] encode(Span span) {\n      return new V1ThriftSpanWriter().write(span);\n    }\n\n    @Override\n    public byte[] encodeList(List<Span> spans) {\n      return new V1ThriftSpanWriter().writeList(spans);\n    }\n\n    @Override\n    public int encodeList(List<Span> spans, byte[] out, int pos) {\n      return new V1ThriftSpanWriter().writeList(spans, out, pos);\n    }\n  },\n  /** Corresponds to the Zipkin v2 json format */\n  JSON_V2 {\n    final V2SpanWriter writer = new V2SpanWriter();\n\n    @Override\n    public Encoding encoding() {\n      return Encoding.JSON;\n    }\n\n    @Override\n    public int sizeInBytes(Span input) {\n      return writer.sizeInBytes(input);\n    }\n\n    @Override\n    public byte[] encode(Span span) {\n      return JsonCodec.write(writer, span);\n    }\n\n    @Override\n    public byte[] encodeList(List<Span> spans) {\n      return JsonCodec.writeList(writer, spans);\n    }\n\n    @Override\n    public int encodeList(List<Span> spans, byte[] out, int pos) {\n      return JsonCodec.writeList(writer, spans, out, pos);\n    }\n  },\n  PROTO3 {\n    final Proto3Codec codec = new Proto3Codec();\n\n    @Override\n    public Encoding encoding() {\n      return Encoding.PROTO3;\n    }\n\n    @Override\n    public int sizeInBytes(Span input) {\n      return codec.sizeInBytes(input);\n    }\n\n    @Override\n    public byte[] encode(Span span) {\n      return codec.write(span);\n    }\n\n    @Override\n    public byte[] encodeList(List<Span> spans) {\n      return codec.writeList(spans);\n    }\n\n    @Override\n    public int encodeList(List<Span> spans, byte[] out, int pos) {\n      return codec.writeList(spans, out, pos);\n    }\n  };\n\n  /** Allows you to encode a list of spans onto a specific offset. For example, when nesting */\n  public abstract int encodeList(List<Span> spans, byte[] out, int pos);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/AggregateCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\n/**\n * A call that blocks on others to complete before invoking a callback or returning from {@link\n * #execute()}. The first error will be returned upstream, later ones will be suppressed.\n *\n * @param <I> the type of returned from {@link Call#execute()}\n * @param <O> the type representing the aggregate success value\n */\npublic abstract class AggregateCall<I, O> extends Call.Base<O> {\n\n  public static Call<Void> newVoidCall(List<Call<Void>> calls) {\n    if (calls.isEmpty()) throw new IllegalArgumentException(\"calls were empty\");\n    if (calls.size() == 1) return calls.get(0);\n    return new AggregateVoidCall(calls);\n  }\n\n  static final class AggregateVoidCall extends AggregateCall<Void, Void> {\n    AggregateVoidCall(List<Call<Void>> calls) {\n      super(calls);\n    }\n\n    @Override protected Void newOutput() {\n      return null;\n    }\n\n    @Override protected void append(Void input, Void output) {\n    }\n\n    @Override public AggregateVoidCall clone() {\n      return new AggregateVoidCall(cloneCalls());\n    }\n  }\n\n  final Logger log = Logger.getLogger(getClass().getName());\n  final List<Call<I>> delegate;\n\n  protected AggregateCall(List<Call<I>> delegate) {\n    assert !delegate.isEmpty() : \"do not create empty aggregate calls\";\n    assert delegate.size() > 1 : \"do not create single-element aggregates\";\n    this.delegate = delegate;\n  }\n\n  protected abstract O newOutput();\n\n  protected abstract void append(I input, O output);\n\n  /** Customizes the aggregated result. For example, summarizing or making immutable. */\n  protected O finish(O output) {\n    return output;\n  }\n\n  @Override protected O doExecute() throws IOException {\n    int length = delegate.size();\n    Throwable firstError = null;\n    O result = newOutput();\n    for (int i = 0; i < length; i++) {\n      Call<I> call = delegate.get(i);\n      try {\n        append(call.execute(), result);\n      } catch (Throwable e) {\n        if (firstError == null) {\n          firstError = e;\n        } else if (log.isLoggable(Level.INFO)) {\n          log.log(Level.INFO, \"error from \" + call, e);\n        }\n      }\n    }\n    if (firstError == null) return finish(result);\n    if (firstError instanceof Error) throw (Error) firstError;\n    if (firstError instanceof RuntimeException) throw (RuntimeException) firstError;\n    throw (IOException) firstError;\n  }\n\n  @Override protected void doEnqueue(Callback<O> callback) {\n    int length = delegate.size();\n    AtomicInteger remaining = new AtomicInteger(length);\n    AtomicReference<Throwable> firstError = new AtomicReference<>();\n    O result = newOutput();\n    for (int i = 0; i < length; i++) {\n      Call<I> call = delegate.get(i);\n      call.enqueue(new CountdownCallback(call, remaining, firstError, result, callback));\n    }\n  }\n\n  @Override protected void doCancel() {\n    for (Call<I> iCall : delegate) {\n      iCall.cancel();\n    }\n  }\n\n  class CountdownCallback implements Callback<I> {\n    final Call<I> call;\n    final AtomicInteger remaining;\n    final AtomicReference<Throwable> firstError;\n    @Nullable final O result;\n    final Callback<O> callback;\n\n    CountdownCallback(Call<I> call, AtomicInteger remaining, AtomicReference<Throwable> firstError,\n      O result,\n      Callback<O> callback) {\n      this.call = call;\n      this.remaining = remaining;\n      this.firstError = firstError;\n      this.result = result;\n      this.callback = callback;\n    }\n\n    @Override public void onSuccess(I value) {\n      synchronized (callback) {\n        append(value, result);\n        if (remaining.decrementAndGet() > 0) return;\n        Throwable error = firstError.get();\n        if (error != null) {\n          callback.onError(error);\n        } else {\n          callback.onSuccess(finish(result));\n        }\n      }\n    }\n\n    @Override public synchronized void onError(Throwable throwable) {\n      if (log.isLoggable(Level.INFO)) {\n        log.log(Level.INFO, \"error from \" + call, throwable);\n      }\n      synchronized (callback) {\n        firstError.compareAndSet(null, throwable);\n        if (remaining.decrementAndGet() > 0) return;\n        callback.onError(firstError.get());\n      }\n    }\n  }\n\n  protected final List<Call<I>> cloneCalls() {\n    int length = delegate.size();\n    List<Call<I>> result = new ArrayList<>(length);\n    for (Call<I> iCall : delegate) {\n      result.add(iCall.clone());\n    }\n    return result;\n  }\n\n  public final List<Call<I>> delegate() {\n    return delegate;\n  }\n\n  @Override public String toString() {\n    return \"AggregateCall{\" + delegate + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/ClosedComponentException.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\npublic final class ClosedComponentException extends IllegalStateException {\n  static final long serialVersionUID = -4636520624634625689L;\n\n  /** Convenience constructor that ensures the message is never null. */\n  public ClosedComponentException() {\n    this(null);\n  }\n\n  public ClosedComponentException(String message) {\n    super(message != null ? message : \"closed\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/DateUtil.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.List;\nimport java.util.TimeZone;\nimport java.util.concurrent.TimeUnit;\n\npublic final class DateUtil {\n  static final TimeZone UTC = TimeZone.getTimeZone(\"UTC\");\n\n  /** For bucketed data floored to the day. For example, dependency links. */\n  public static long midnightUTC(long epochMillis) {\n    Calendar day = Calendar.getInstance(UTC);\n    day.setTimeInMillis(epochMillis);\n    day.set(Calendar.MILLISECOND, 0);\n    day.set(Calendar.SECOND, 0);\n    day.set(Calendar.MINUTE, 0);\n    day.set(Calendar.HOUR_OF_DAY, 0);\n    return day.getTimeInMillis();\n  }\n\n  public static List<Long> epochDays(long endTs, long lookback) {\n    long to = midnightUTC(endTs);\n    long startMillis = endTs - (lookback != 0 ? lookback : endTs);\n    long from = startMillis <= 0 ? 0 : midnightUTC(startMillis); // >= 1970\n\n    List<Long> days = new ArrayList<>();\n    for (long time = from; time <= to; time += TimeUnit.DAYS.toMillis(1)) {\n      days.add(time);\n    }\n    return days;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/DelayLimiter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.DelayQueue;\nimport java.util.concurrent.Delayed;\nimport java.util.concurrent.TimeUnit;\n\n/** Limits invocations of a given context to at most once per period. */\n// this is a dependency-free variant formerly served by an expiring guava cache\npublic final class DelayLimiter<C> {\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder {\n    long ttl = 0L;\n    TimeUnit ttlUnit = TimeUnit.MILLISECONDS;\n    int cardinality = 0;\n\n    /**\n     * When {@link #shouldInvoke(Object)} returns true, it will return false until this duration\n     * expires.\n     */\n    public Builder ttl(long ttl, TimeUnit ttlUnit) {\n      if (ttlUnit == null) throw new NullPointerException(\"ttlUnit == null\");\n      this.ttl = ttl;\n      this.ttlUnit = ttlUnit;\n      return this;\n    }\n\n    /**\n     * This bounds suppressions, useful because contexts can be accidentally unlimited cardinality.\n     */\n    public Builder cardinality(int cardinality) {\n      this.cardinality = cardinality;\n      return this;\n    }\n\n    public <C> DelayLimiter<C> build() {\n      if (ttl <= 0L) throw new IllegalArgumentException(\"ttl <= 0\");\n      if (cardinality <= 0) throw new IllegalArgumentException(\"cardinality <= 0\");\n      return new DelayLimiter<>(new SuppressionFactory(ttlUnit.toNanos(ttl)), cardinality);\n    }\n\n    Builder() {\n    }\n  }\n\n  final SuppressionFactory suppressionFactory;\n  final ConcurrentHashMap<C, Suppression<C>> cache = new ConcurrentHashMap<>();\n  final DelayQueue<Suppression<C>> suppressions = new DelayQueue<>();\n  final int cardinality;\n\n  DelayLimiter(SuppressionFactory suppressionFactory, int cardinality) {\n    this.suppressionFactory = suppressionFactory;\n    this.cardinality = cardinality;\n  }\n\n  /** Returns true if a given context should be invoked. */\n  public boolean shouldInvoke(C context) {\n    cleanupExpiredSuppressions();\n\n    if (cache.containsKey(context)) return false;\n\n    Suppression<C> suppression = suppressionFactory.create(context);\n\n    if (cache.putIfAbsent(context, suppression) != null) return false; // lost race\n\n    suppressions.offer(suppression);\n\n    // If we added an entry, it could make us go over the max size.\n    if (suppressions.size() > cardinality) removeOneSuppression();\n\n    return true;\n  }\n\n  void removeOneSuppression() {\n    Suppression<C> eldest;\n    while ((eldest = suppressions.peek()) != null) { // loop unless empty\n      if (suppressions.remove(eldest)) { // check for lost race\n        cache.remove(eldest.context, eldest);\n        break; // to ensure we don't remove two!\n      }\n    }\n  }\n\n  public void invalidate(C context) {\n    Suppression<C> suppression = cache.remove(context);\n    if (suppression != null) suppressions.remove(suppression);\n  }\n\n  public void clear() {\n    cache.clear();\n    suppressions.clear();\n  }\n\n  void cleanupExpiredSuppressions() {\n    Suppression<C> expiredSuppression;\n    while ((expiredSuppression = suppressions.poll()) != null) {\n      cache.remove(expiredSuppression.context, expiredSuppression);\n    }\n  }\n\n  static class SuppressionFactory { // not final for tests\n    final long ttlNanos;\n\n    SuppressionFactory(long ttlNanos) {\n      this.ttlNanos = ttlNanos;\n    }\n\n    long nanoTime() {\n      return System.nanoTime();\n    }\n\n    <C> Suppression<C> create(C context) {\n      return new Suppression<>(this, context, nanoTime() + ttlNanos);\n    }\n  }\n\n  static final class Suppression<C> implements Delayed {\n    final SuppressionFactory factory;\n    final C context;\n    final long expiration;\n\n    Suppression(SuppressionFactory factory, C context, long expiration) {\n      this.factory = factory;\n      this.context = context;\n      this.expiration = expiration;\n    }\n\n    @Override public long getDelay(TimeUnit unit) {\n      return unit.convert(expiration - factory.nanoTime(), TimeUnit.NANOSECONDS);\n    }\n\n    @Override public int compareTo(Delayed o) {\n      return Long.signum(expiration - ((Suppression) o).expiration);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Dependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport zipkin2.DependencyLink;\n\nimport static zipkin2.internal.ThriftCodec.skip;\nimport static zipkin2.internal.ThriftField.TYPE_I64;\nimport static zipkin2.internal.ThriftField.TYPE_LIST;\nimport static zipkin2.internal.ThriftField.TYPE_STOP;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.WriteBuffer.utf8SizeInBytes;\n\n/**\n * Internal as only cassandra serializes the start and end timestamps along with link data, and\n * those serialized timestamps are never read.\n *\n * @deprecated See https://github.com/openzipkin/zipkin/issues/1008\n */\npublic final class Dependencies {\n  static final ThriftField START_TS = new ThriftField(TYPE_I64, 1);\n  static final ThriftField END_TS = new ThriftField(TYPE_I64, 2);\n  static final ThriftField LINKS = new ThriftField(TYPE_LIST, 3);\n  static final DependencyLinkAdapter DEPENDENCY_LINK_ADAPTER = new DependencyLinkAdapter();\n\n  public List<DependencyLink> links() {\n    return links;\n  }\n\n  /** Reads from buffer serialized in TBinaryProtocol */\n  public static Dependencies fromThrift(ByteBuffer bytes) {\n    long startTs = 0L;\n    long endTs = 0L;\n    List<DependencyLink> links = Collections.emptyList();\n\n    ReadBuffer buffer = ReadBuffer.wrapUnsafe(bytes);\n    while (true) {\n      ThriftField thriftField = ThriftField.read(buffer);\n      if (thriftField.type == TYPE_STOP) break;\n\n      if (thriftField.isEqualTo(START_TS)) {\n        startTs = buffer.readLong();\n      } else if (thriftField.isEqualTo(END_TS)) {\n        endTs = buffer.readLong();\n      } else if (thriftField.isEqualTo(LINKS)) {\n        int length = ThriftCodec.readListLength(buffer);\n        if (length == 0) continue;\n        links = new ArrayList<>(length);\n        for (int i = 0; i < length; i++) {\n          links.add(DependencyLinkAdapter.read(buffer));\n        }\n      } else {\n        skip(buffer, thriftField.type);\n      }\n    }\n\n    return Dependencies.create(startTs, endTs, links);\n  }\n\n  /** Writes the current instance in TBinaryProtocol */\n  public ByteBuffer toThrift() {\n    byte[] result = new byte[sizeInBytes()];\n    write(WriteBuffer.wrap(result));\n    return ByteBuffer.wrap(result);\n  }\n\n  int sizeInBytes() {\n    int sizeInBytes = 0;\n    sizeInBytes += 3 + 8; // START_TS\n    sizeInBytes += 3 + 8; // END_TS\n    sizeInBytes += 3 + ThriftCodec.listSizeInBytes(DEPENDENCY_LINK_ADAPTER, links);\n    sizeInBytes++; // TYPE_STOP\n    return sizeInBytes;\n  }\n\n  void write(WriteBuffer buffer) {\n    START_TS.write(buffer);\n    ThriftCodec.writeLong(buffer, startTs);\n\n    END_TS.write(buffer);\n    ThriftCodec.writeLong(buffer, endTs);\n\n    LINKS.write(buffer);\n    ThriftCodec.writeList(DEPENDENCY_LINK_ADAPTER, links, buffer);\n\n    buffer.writeByte(TYPE_STOP);\n  }\n\n  /** timestamps are in epoch milliseconds */\n  public static Dependencies create(long startTs, long endTs, List<DependencyLink> links) {\n    return new Dependencies(startTs, endTs, links);\n  }\n\n  final long startTs, endTs;\n  final List<DependencyLink> links;\n\n  Dependencies(long startTs, long endTs, List<DependencyLink> links) {\n    this.startTs = startTs;\n    this.endTs = endTs;\n    if (links == null) throw new NullPointerException(\"links == null\");\n    this.links = links;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof Dependencies)) return false;\n    Dependencies that = (Dependencies) o;\n    return startTs == that.startTs && endTs == that.endTs && links.equals(that.links);\n  }\n\n  @Override\n  public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (int) (h ^ ((startTs >>> 32) ^ startTs));\n    h *= 1000003;\n    h ^= (int) (h ^ ((endTs >>> 32) ^ endTs));\n    h *= 1000003;\n    h ^= links.hashCode();\n    return h;\n  }\n\n  static final class DependencyLinkAdapter implements WriteBuffer.Writer<DependencyLink> {\n\n    static final ThriftField PARENT = new ThriftField(TYPE_STRING, 1);\n    static final ThriftField CHILD = new ThriftField(TYPE_STRING, 2);\n    static final ThriftField CALL_COUNT = new ThriftField(TYPE_I64, 4);\n    static final ThriftField ERROR_COUNT = new ThriftField(TYPE_I64, 5);\n\n    static DependencyLink read(ReadBuffer buffer) {\n      DependencyLink.Builder result = DependencyLink.newBuilder();\n      ThriftField thriftField;\n\n      while (true) {\n        thriftField = ThriftField.read(buffer);\n        if (thriftField.type == TYPE_STOP) break;\n\n        if (thriftField.isEqualTo(PARENT)) {\n          result.parent(buffer.readUtf8(buffer.readInt()));\n        } else if (thriftField.isEqualTo(CHILD)) {\n          result.child(buffer.readUtf8(buffer.readInt()));\n        } else if (thriftField.isEqualTo(CALL_COUNT)) {\n          result.callCount(buffer.readLong());\n        } else if (thriftField.isEqualTo(ERROR_COUNT)) {\n          result.errorCount(buffer.readLong());\n        } else {\n          skip(buffer, thriftField.type);\n        }\n      }\n\n      return result.build();\n    }\n\n    @Override public int sizeInBytes(DependencyLink value) {\n      int sizeInBytes = 0;\n      sizeInBytes += 3 + 4 + utf8SizeInBytes(value.parent());\n      sizeInBytes += 3 + 4 + utf8SizeInBytes(value.child());\n      sizeInBytes += 3 + 8; // CALL_COUNT\n      if (value.errorCount() > 0) sizeInBytes += 3 + 8; // ERROR_COUNT\n      sizeInBytes++; // TYPE_STOP\n      return sizeInBytes;\n    }\n\n    @Override public void write(DependencyLink value, WriteBuffer buffer) {\n      PARENT.write(buffer);\n      ThriftCodec.writeLengthPrefixed(buffer, value.parent());\n\n      CHILD.write(buffer);\n      ThriftCodec.writeLengthPrefixed(buffer, value.child());\n\n      CALL_COUNT.write(buffer);\n      ThriftCodec.writeLong(buffer, value.callCount());\n\n      if (value.errorCount() > 0) {\n        ERROR_COUNT.write(buffer);\n        ThriftCodec.writeLong(buffer, value.errorCount());\n      }\n\n      buffer.writeByte(TYPE_STOP);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/DependencyLinker.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.logging.Logger;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static java.util.logging.Level.FINE;\n\n/**\n * This parses a span tree into dependency links used by Web UI. Ex. http://zipkin/dependency\n *\n * <p>This implementation traverses the tree, and only creates links between {@link Kind#SERVER\n * server} spans. One exception is at the bottom of the trace tree. {@link Kind#CLIENT client} spans\n * that record their {@link Span#remoteEndpoint()} are included, as this accounts for uninstrumented\n * services. Spans with {@link Span#kind()} unset, but {@link Span#remoteEndpoint()} set are treated\n * the same as client spans.\n */\npublic final class DependencyLinker {\n  final Logger logger;\n  final SpanNode.Builder builder;\n  final Map<Pair, Long> callCounts = new LinkedHashMap<>();\n  final Map<Pair, Long> errorCounts = new LinkedHashMap<>();\n\n  public DependencyLinker() {\n    this(Logger.getLogger(DependencyLinker.class.getName()));\n  }\n\n  DependencyLinker(Logger logger) {\n    this.logger = logger;\n    this.builder = SpanNode.newBuilder(logger);\n  }\n\n  /** All {@code spans} must have the same trace id. */\n  public DependencyLinker putTrace(List<Span> spans) {\n    if (spans.isEmpty()) return this;\n    SpanNode traceTree = builder.build(spans);\n\n    if (logger.isLoggable(FINE)) logger.fine(\"traversing trace tree, breadth-first\");\n    for (Iterator<SpanNode> i = traceTree.traverse(); i.hasNext(); ) {\n      SpanNode current = i.next();\n      Span currentSpan = current.span();\n      if (logger.isLoggable(FINE)) {\n        logger.fine(\"processing \" + currentSpan);\n      }\n\n      Kind kind = currentSpan.kind();\n      // When processing links to a client span, we prefer the server's name. If we have no child\n      // spans, we proceed to use the name the client chose.\n      if (Kind.CLIENT.equals(kind) && !current.children().isEmpty()) {\n        continue;\n      }\n\n      String serviceName = currentSpan.localServiceName();\n      String remoteServiceName = currentSpan.remoteServiceName();\n      if (kind == null) {\n        // Treat unknown type of span as a client span if we know both sides\n        if (serviceName != null && remoteServiceName != null) {\n          kind = Kind.CLIENT;\n        } else {\n          logger.fine(\"non remote span; skipping\");\n          continue;\n        }\n      }\n\n      String child;\n      String parent;\n      switch (kind) {\n        case SERVER:\n        case CONSUMER:\n          child = serviceName;\n          parent = remoteServiceName;\n          if (current == traceTree) { // we are the root-most span.\n            if (parent == null) {\n              logger.fine(\"root's client is unknown; skipping\");\n              continue;\n            }\n          }\n          break;\n        case CLIENT:\n        case PRODUCER:\n          parent = serviceName;\n          child = remoteServiceName;\n          break;\n        default:\n          logger.fine(\"unknown kind; skipping\");\n          continue;\n      }\n\n      boolean isError = currentSpan.tags().containsKey(\"error\");\n      if (kind == Kind.PRODUCER || kind == Kind.CONSUMER) {\n        if (parent == null || child == null) {\n          logger.fine(\"cannot link messaging span to its broker; skipping\");\n        } else {\n          addLink(parent, child, isError);\n        }\n        continue;\n      }\n\n      // Local spans may be between the current node and its remote parent\n      Span remoteAncestor = firstRemoteAncestor(current);\n      String remoteAncestorName;\n      if (remoteAncestor != null\n        && (remoteAncestorName = remoteAncestor.localServiceName()) != null) {\n        // Some users accidentally put the remote service name on client annotations.\n        // Check for this and backfill a link from the nearest remote to that service as necessary.\n        if (kind == Kind.CLIENT && serviceName != null && !remoteAncestorName.equals(serviceName)) {\n          logger.fine(\"detected missing link to client span\");\n          addLink(remoteAncestorName, serviceName, false); // we don't know if there's an error here\n        }\n\n        if (kind == Kind.SERVER || parent == null) parent = remoteAncestorName;\n\n        // When an RPC is split between spans, we skip the child (server side). If our parent is a\n        // client, we need to check it for errors.\n        if (!isError && Kind.CLIENT.equals(remoteAncestor.kind()) &&\n          currentSpan.parentId() != null && currentSpan.parentId().equals(remoteAncestor.id())) {\n          isError = remoteAncestor.tags().containsKey(\"error\");\n        }\n      }\n\n      if (parent == null || child == null) {\n        logger.fine(\"cannot find remote ancestor; skipping\");\n        continue;\n      }\n\n      addLink(parent, child, isError);\n    }\n    return this;\n  }\n\n  Span firstRemoteAncestor(SpanNode current) {\n    SpanNode ancestor = current.parent();\n    while (ancestor != null) {\n      Span maybeRemote = ancestor.span();\n      if (maybeRemote != null && maybeRemote.kind() != null) {\n        if (logger.isLoggable(FINE)) logger.fine(\"found remote ancestor \" + maybeRemote);\n        return maybeRemote;\n      }\n      ancestor = ancestor.parent();\n    }\n    return null;\n  }\n\n  void addLink(String parent, String child, boolean isError) {\n    if (logger.isLoggable(FINE)) {\n      logger.fine(\"incrementing \" + (isError ? \"error \" : \"\") + \"link \" + parent + \" -> \" + child);\n    }\n    Pair key = new Pair(parent, child);\n    if (callCounts.containsKey(key)) {\n      callCounts.put(key, callCounts.get(key) + 1);\n    } else {\n      callCounts.put(key, 1L);\n    }\n    if (!isError) return;\n    if (errorCounts.containsKey(key)) {\n      errorCounts.put(key, errorCounts.get(key) + 1);\n    } else {\n      errorCounts.put(key, 1L);\n    }\n  }\n\n  public List<DependencyLink> link() {\n    return link(callCounts, errorCounts);\n  }\n\n  /** links are merged by mapping to parent/child and summing corresponding links */\n  public static List<DependencyLink> merge(Iterable<DependencyLink> in) {\n    Map<Pair, Long> callCounts = new LinkedHashMap<>();\n    Map<Pair, Long> errorCounts = new LinkedHashMap<>();\n\n    for (DependencyLink link : in) {\n      Pair parentChild = new Pair(link.parent(), link.child());\n      long callCount = callCounts.getOrDefault(parentChild, 0L);\n      callCount += link.callCount();\n      callCounts.put(parentChild, callCount);\n      long errorCount = errorCounts.containsKey(parentChild) ? errorCounts.get(parentChild) : 0L;\n      errorCount += link.errorCount();\n      errorCounts.put(parentChild, errorCount);\n    }\n\n    return link(callCounts, errorCounts);\n  }\n\n  static List<DependencyLink> link(Map<Pair, Long> callCounts,\n    Map<Pair, Long> errorCounts) {\n    List<DependencyLink> result = new ArrayList<>(callCounts.size());\n    for (Map.Entry<Pair, Long> entry : callCounts.entrySet()) {\n      Pair parentChild = entry.getKey();\n      result.add(DependencyLink.newBuilder()\n        .parent(parentChild.left)\n        .child(parentChild.right)\n        .callCount(entry.getValue())\n        .errorCount(errorCounts.getOrDefault(parentChild, 0L))\n        .build());\n    }\n    return result;\n  }\n\n  static final class Pair {\n    final String left, right;\n\n    Pair(String left, String right) {\n      this.left = left;\n      this.right = right;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (o == this) return true;\n      if (!(o instanceof Pair)) return false;\n      Pair that = (DependencyLinker.Pair) o;\n      return left.equals(that.left) && right.equals(that.right);\n    }\n\n    @Override\n    public int hashCode() {\n      int h$ = 1;\n      h$ *= 1000003;\n      h$ ^= left.hashCode();\n      h$ *= 1000003;\n      h$ ^= right.hashCode();\n      return h$;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/FilterTraces.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.storage.QueryRequest;\n\npublic final class FilterTraces implements Call.Mapper<List<List<Span>>, List<List<Span>>> {\n  /** Filters the mutable input based on the query */\n  public static Call.Mapper<List<List<Span>>, List<List<Span>>> create(QueryRequest request) {\n    return new FilterTraces(request);\n  }\n\n  final QueryRequest request;\n\n  FilterTraces(QueryRequest request) {\n    this.request = request;\n  }\n\n  @Override public List<List<Span>> map(List<List<Span>> input) {\n    int length = input.size();\n    if (length == 0) return input;\n    ArrayList<List<Span>> result = new ArrayList<>(length);\n    for (List<Span> next : input) {\n      if (request.test(next)) result.add(next);\n    }\n    return result;\n  }\n\n  @Override public String toString() {\n    return \"FilterTraces{request=\" + request + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/HexCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\n// code originally imported from zipkin.Util\npublic final class HexCodec {\n  public static final char[] HEX_DIGITS = {\n    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'\n  };\n\n  /**\n   * Parses a 1 to 32 character lower-hex string with no prefix into an unsigned long, tossing any\n   * bits higher than 64.\n   */\n  public static long lowerHexToUnsignedLong(String lowerHex) {\n    int length = lowerHex.length();\n    if (length < 1 || length > 32) throw isntLowerHexLong(lowerHex);\n\n    // trim off any high bits\n    int beginIndex = length > 16 ? length - 16 : 0;\n\n    return lowerHexToUnsignedLong(lowerHex, beginIndex);\n  }\n\n  /**\n   * Parses a 16 character lower-hex string with no prefix into an unsigned long, starting at the\n   * specified index.\n   */\n  public static long lowerHexToUnsignedLong(String lowerHex, int index) {\n    long result = 0;\n    for (int endIndex = Math.min(index + 16, lowerHex.length()); index < endIndex; index++) {\n      char c = lowerHex.charAt(index);\n      result <<= 4;\n      if (c >= '0' && c <= '9') {\n        result |= c - '0';\n      } else if (c >= 'a' && c <= 'f') {\n        result |= c - 'a' + 10;\n      } else {\n        throw isntLowerHexLong(lowerHex);\n      }\n    }\n    return result;\n  }\n\n  static NumberFormatException isntLowerHexLong(String lowerHex) {\n    throw new NumberFormatException(\n        lowerHex + \" should be a 1 to 32 character lower-hex string with no prefix\");\n  }\n\n  HexCodec() {}\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/JsonCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport static com.google.gson.stream.JsonToken.BOOLEAN;\nimport static com.google.gson.stream.JsonToken.NULL;\nimport static com.google.gson.stream.JsonToken.STRING;\nimport static java.lang.String.format;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * This explicitly constructs instances of model classes via manual parsing for a number of\n * reasons.\n *\n * <ul>\n *   <li>Eliminates the need to keep separate model classes for proto3 vs json\n *   <li>Avoids magic field initialization which, can miss constructor guards\n *   <li>Allows us to safely re-use the json form in toString methods\n *   <li>Encourages logic to be based on the json shape of objects\n *   <li>Ensures the order and naming of the fields in json is stable\n * </ul>\n *\n * <p>There is the up-front cost of creating this, and maintenance of this to consider. However,\n * this should be easy to justify as these objects don't change much at all.\n */\npublic final class JsonCodec {\n  // Hides gson types for internal use in other submodules\n  public static final class JsonReader {\n    final com.google.gson.stream.JsonReader delegate;\n\n    JsonReader(ReadBuffer buffer) {\n      delegate = new com.google.gson.stream.JsonReader(new InputStreamReader(buffer, UTF_8));\n    }\n\n    public void beginArray() throws IOException {\n      delegate.beginArray();\n    }\n\n    public boolean hasNext() throws IOException {\n      return delegate.hasNext();\n    }\n\n    public void endArray() throws IOException {\n      delegate.endArray();\n    }\n\n    public void beginObject() throws IOException {\n      delegate.beginObject();\n    }\n\n    public void endObject() throws IOException {\n      delegate.endObject();\n    }\n\n    public String nextName() throws IOException {\n      return delegate.nextName();\n    }\n\n    public String nextString() throws IOException {\n      return delegate.nextString();\n    }\n\n    public void skipValue() throws IOException {\n      delegate.skipValue();\n    }\n\n    public long nextLong() throws IOException {\n      return delegate.nextLong();\n    }\n\n    public String getPath() {\n      return delegate.getPath();\n    }\n\n    public boolean nextBoolean() throws IOException {\n      return delegate.nextBoolean();\n    }\n\n    public int nextInt() throws IOException {\n      return delegate.nextInt();\n    }\n\n    public boolean peekString() throws IOException {\n      return delegate.peek() == STRING;\n    }\n\n    public boolean peekBoolean() throws IOException {\n      return delegate.peek() == BOOLEAN;\n    }\n\n    public boolean peekNull() throws IOException {\n      return delegate.peek() == NULL;\n    }\n\n    @Override public String toString() {\n      return delegate.toString();\n    }\n  }\n\n  public interface JsonReaderAdapter<T> {\n    T fromJson(JsonReader reader) throws IOException;\n  }\n\n  public static <T> boolean read(\n    JsonReaderAdapter<T> adapter, ReadBuffer buffer, Collection<T> out) {\n    if (buffer.available() == 0) return false;\n    try {\n      out.add(adapter.fromJson(new JsonReader(buffer)));\n      return true;\n    } catch (Exception e) {\n      throw exceptionReading(adapter.toString(), e);\n    }\n  }\n\n  public static @Nullable <T> T readOne(JsonReaderAdapter<T> adapter, ReadBuffer buffer) {\n    List<T> out = new ArrayList<>(1); // TODO: could make single-element list w/o array\n    if (!read(adapter, buffer, out)) return null;\n    return out.get(0);\n  }\n\n  public static <T> boolean readList(\n    JsonReaderAdapter<T> adapter, ReadBuffer buffer, Collection<T> out) {\n    if (buffer.available() == 0) return false;\n    JsonReader reader = new JsonReader(buffer);\n    try {\n      reader.beginArray();\n      if (!reader.hasNext()) return false;\n      while (reader.hasNext()) out.add(adapter.fromJson(reader));\n      reader.endArray();\n      return true;\n    } catch (Exception e) {\n      throw exceptionReading(\"List<\" + adapter + \">\", e);\n    }\n  }\n\n  static <T> int sizeInBytes(WriteBuffer.Writer<T> writer, List<T> value) {\n    int length = value.size();\n    int sizeInBytes = 2; // []\n    if (length > 1) sizeInBytes += length - 1; // comma to join elements\n    for (T t : value) {\n      sizeInBytes += writer.sizeInBytes(t);\n    }\n    return sizeInBytes;\n  }\n\n  /** Inability to encode is a programming bug. */\n  public static <T> byte[] write(WriteBuffer.Writer<T> writer, T value) {\n    byte[] result = new byte[writer.sizeInBytes(value)];\n    WriteBuffer b = WriteBuffer.wrap(result);\n    try {\n      writer.write(value, b);\n    } catch (RuntimeException e) {\n      int lengthWritten = result.length;\n      for (int i = 0; i < result.length; i++) {\n        if (result[i] == 0) {\n          lengthWritten = i;\n          break;\n        }\n      }\n\n      // Don't use value directly in the message, as its toString might be implemented using this\n      // method. If that's the case, we'd stack overflow. Instead, emit what we've written so far.\n      String message =\n        format(\n          \"Bug found using %s to write %s as json. Wrote %s/%s bytes: %s\",\n          writer.getClass().getSimpleName(),\n          value.getClass().getSimpleName(),\n          lengthWritten,\n          result.length,\n          new String(result, 0, lengthWritten, UTF_8));\n      throw new AssertionError(message, e);\n    }\n    return result;\n  }\n\n  public static <T> byte[] writeList(WriteBuffer.Writer<T> writer, List<T> value) {\n    if (value.isEmpty()) return new byte[] {'[', ']'};\n    byte[] result = new byte[sizeInBytes(writer, value)];\n    writeList(writer, value, WriteBuffer.wrap(result));\n    return result;\n  }\n\n  public static <T> int writeList(WriteBuffer.Writer<T> writer, List<T> value, byte[] out,\n    int pos) {\n    if (value.isEmpty()) {\n      out[pos++] = '[';\n      out[pos++] = ']';\n      return 2;\n    }\n    int initialPos = pos;\n    WriteBuffer result = WriteBuffer.wrap(out, pos);\n    writeList(writer, value, result);\n    return result.pos() - initialPos;\n  }\n\n  public static <T> void writeList(WriteBuffer.Writer<T> writer, List<T> value, WriteBuffer b) {\n    b.writeByte('[');\n    for (int i = 0, length = value.size(); i < length; ) {\n      writer.write(value.get(i++), b);\n      if (i < length) b.writeByte(',');\n    }\n    b.writeByte(']');\n  }\n\n  static IllegalArgumentException exceptionReading(String type, Exception e) {\n    String cause = e.getMessage() == null ? \"Error\" : e.getMessage();\n    if (cause.contains(\"Expected BEGIN_OBJECT\")\n      || cause.contains(\"Expected BEGIN_ARRAY\")\n      || cause.contains(\"malformed\")) {\n      cause = \"Malformed\";\n    }\n    String message = format(\"%s reading %s from json\", cause, type);\n    throw new IllegalArgumentException(message, e);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/JsonEscaper.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\npublic final class JsonEscaper {\n  /** Exposed for ElasticSearch HttpBulkIndexer */\n  public static CharSequence jsonEscape(CharSequence v) {\n    int length = v.length();\n    if (length == 0) return v;\n\n    int afterReplacement = 0;\n    StringBuilder builder = null;\n    for (int i = 0; i < length; i++) {\n      char c = v.charAt(i);\n      String replacement;\n      if (c < 0x80) {\n        replacement = REPLACEMENT_CHARS[c];\n        if (replacement == null) continue;\n      } else if (c == '\\u2028') {\n        replacement = U2028;\n      } else if (c == '\\u2029') {\n        replacement = U2029;\n      } else {\n        continue;\n      }\n      if (afterReplacement < i) { // write characters between the last replacement and now\n        if (builder == null) builder = new StringBuilder(length);\n        builder.append(v, afterReplacement, i);\n      }\n      if (builder == null) builder = new StringBuilder(length);\n      builder.append(replacement);\n      afterReplacement = i + 1;\n    }\n    if (builder == null) return v; // then we didn't escape anything\n\n    if (afterReplacement < length) {\n      builder.append(v, afterReplacement, length);\n    }\n    return builder;\n  }\n\n  /*\n   * Escaping logic adapted from Moshi, which we couldn't use due to language level\n   *\n   * From RFC 7159, \"All Unicode characters may be placed within the\n   * quotation marks except for the characters that must be escaped:\n   * quotation mark, reverse solidus, and the control characters\n   * (U+0000 through U+001F).\"\n   *\n   * We also escape '\\u2028' and '\\u2029', which JavaScript interprets as\n   * newline characters. This prevents eval() from failing with a syntax\n   * error. http://code.google.com/p/google-gson/issues/detail?id=341\n   */\n  private static final String[] REPLACEMENT_CHARS;\n\n  static {\n    REPLACEMENT_CHARS = new String[128];\n    for (int i = 0; i <= 0x1f; i++) {\n      REPLACEMENT_CHARS[i] = String.format(\"\\\\u%04x\", i);\n    }\n    REPLACEMENT_CHARS['\"'] = \"\\\\\\\"\";\n    REPLACEMENT_CHARS['\\\\'] = \"\\\\\\\\\";\n    REPLACEMENT_CHARS['\\t'] = \"\\\\t\";\n    REPLACEMENT_CHARS['\\b'] = \"\\\\b\";\n    REPLACEMENT_CHARS['\\n'] = \"\\\\n\";\n    REPLACEMENT_CHARS['\\r'] = \"\\\\r\";\n    REPLACEMENT_CHARS['\\f'] = \"\\\\f\";\n  }\n\n  private static final String U2028 = \"\\\\u2028\";\n  private static final String U2029 = \"\\\\u2029\";\n\n  public static int jsonEscapedSizeInBytes(CharSequence v) {\n    boolean ascii = true;\n    int escapingOverhead = 0;\n    for (int i = 0, length = v.length(); i < length; i++) {\n      char c = v.charAt(i);\n      if (c == '\\u2028' || c == '\\u2029') {\n        escapingOverhead += 5;\n      } else if (c >= 0x80) {\n        ascii = false;\n      } else {\n        String maybeReplacement = REPLACEMENT_CHARS[c];\n        if (maybeReplacement != null) escapingOverhead += maybeReplacement.length() - 1;\n      }\n    }\n    if (ascii) return v.length() + escapingOverhead;\n    return WriteBuffer.utf8SizeInBytes(v) + escapingOverhead;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Nullable.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\n/**\n * Libraries such as Guice and AutoValue will process any annotation named {@code Nullable}. This\n * avoids a dependency on one of the many jsr305 jars, causes problems in OSGi and Java 9 projects\n * (where a project is also using jax-ws).\n */\n@java.lang.annotation.Documented\n@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)\npublic @interface Nullable {\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Proto3Codec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.Span;\n\nimport static java.lang.String.format;\nimport static zipkin2.internal.Proto3ZipkinFields.SPAN;\n\n// @Immutable\npublic final class Proto3Codec {\n\n  final Proto3SpanWriter writer = new Proto3SpanWriter();\n\n  public int sizeInBytes(Span input) {\n    return writer.sizeInBytes(input);\n  }\n\n  public byte[] write(Span span) {\n    return writer.write(span);\n  }\n\n  public byte[] writeList(List<Span> spans) {\n    return writer.writeList(spans);\n  }\n\n  public int writeList(List<Span> spans, byte[] out, int pos) {\n    return writer.writeList(spans, out, pos);\n  }\n\n  public static boolean read(ReadBuffer buffer, Collection<Span> out) {\n    if (buffer.available() == 0) return false;\n    try {\n      Span span = SPAN.read(buffer);\n      if (span == null) return false;\n      out.add(span);\n      return true;\n    } catch (RuntimeException e) {\n      throw exceptionReading(\"Span\", e);\n    }\n  }\n\n  public static @Nullable Span readOne(ReadBuffer buffer) {\n    try {\n      return SPAN.read(buffer);\n    } catch (RuntimeException e) {\n      throw exceptionReading(\"Span\", e);\n    }\n  }\n\n  public static boolean readList(ReadBuffer buffer, Collection<Span> out) {\n    int length = buffer.available();\n    if (length == 0) return false;\n    try {\n      while (buffer.pos() < length) {\n        Span span = SPAN.read(buffer);\n        if (span == null) return false;\n        out.add(span);\n      }\n    } catch (RuntimeException e) {\n      throw exceptionReading(\"List<Span>\", e);\n    }\n    return true;\n  }\n\n  static IllegalArgumentException exceptionReading(String type, Exception e) {\n    String cause = e.getMessage() == null ? \"Error\" : e.getMessage();\n    if (cause.contains(\"Malformed\")) cause = \"Malformed\";\n    String message = format(\"%s reading %s from proto3\", cause, type);\n    throw new IllegalArgumentException(message, e);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Proto3Fields.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport zipkin2.Endpoint;\n\nimport static zipkin2.internal.WriteBuffer.utf8SizeInBytes;\nimport static zipkin2.internal.WriteBuffer.varintSizeInBytes;\n\n/**\n * Everything here assumes the field numbers are less than 16, implying a 1 byte tag.\n */\n//@Immutable\nfinal class Proto3Fields {\n  /**\n   * Define the wire types, except the deprecated ones (groups)\n   *\n   * <p>See https://developers.google.com/protocol-buffers/docs/encoding#structure\n   */\n  static final int\n    WIRETYPE_VARINT = 0,\n    WIRETYPE_FIXED64 = 1,\n    WIRETYPE_LENGTH_DELIMITED = 2,\n    WIRETYPE_FIXED32 = 5;\n\n  static class Field {\n    final int fieldNumber;\n    final int wireType;\n    /**\n     * \"Each key in the streamed message is a varint with the value {@code (field_number << 3) |\n     * wire_type}\"\n     *\n     * <p>See https://developers.google.com/protocol-buffers/docs/encoding#structure\n     */\n    final int key;\n\n    Field(int key) {\n      this(key >>> 3, key & (1 << 3) - 1, key);\n    }\n\n    Field(int fieldNumber, int wireType, int key) {\n      this.fieldNumber = fieldNumber;\n      this.wireType = wireType;\n      this.key = key;\n    }\n\n    static int fieldNumber(int key, int byteL) {\n      int fieldNumber = key >>> 3;\n      if (fieldNumber != 0) return fieldNumber;\n      throw new IllegalArgumentException(\"Malformed: fieldNumber was zero at byte \" + byteL);\n    }\n\n    static int wireType(int key, int byteL) {\n      int wireType = key & (1 << 3) - 1;\n      if (wireType != 0 && wireType != 1 && wireType != 2 && wireType != 5) {\n        throw new IllegalArgumentException(\n          \"Malformed: invalid wireType \" + wireType + \" at byte \" + byteL);\n      }\n      return wireType;\n    }\n\n    static boolean skipValue(ReadBuffer buffer, int wireType) {\n      int remaining = buffer.available();\n      switch (wireType) {\n        case WIRETYPE_VARINT:\n          for (int i = 0; i < remaining; i++) {\n            if (buffer.readByte() >= 0) return true;\n          }\n          return false;\n        case WIRETYPE_FIXED64:\n          return buffer.skip(8) == 8;\n        case WIRETYPE_LENGTH_DELIMITED:\n          int length = buffer.readVarint32();\n          return buffer.skip(length) == length;\n        case WIRETYPE_FIXED32:\n          return buffer.skip(4) == 4;\n        default:\n          throw new IllegalArgumentException(\n            \"Malformed: invalid wireType \" + wireType + \" at byte \" + buffer.pos());\n      }\n    }\n  }\n\n  /**\n   * Leniently skips out null, but not on empty string, allowing tag \"error\" -> \"\" to serialize\n   * properly.\n   *\n   * <p>This won't result in empty {@link zipkin2.Span#name()} or {@link Endpoint#serviceName()}\n   * because in both cases constructors coerce empty values to null.\n   */\n  static abstract class LengthDelimitedField<T> extends Field {\n    LengthDelimitedField(int key) {\n      super(key);\n      assert wireType == WIRETYPE_LENGTH_DELIMITED;\n    }\n\n    final int sizeInBytes(T value) {\n      if (value == null) return 0;\n      int sizeOfValue = sizeOfValue(value);\n      return sizeOfLengthDelimitedField(sizeOfValue);\n    }\n\n    final void write(WriteBuffer b, T value) {\n      if (value == null) return;\n      int sizeOfValue = sizeOfValue(value);\n      b.writeByte(key);\n      b.writeVarint(sizeOfValue); // length prefix\n      writeValue(b, value);\n    }\n\n    /**\n     * Calling this after consuming the field key to ensures there's enough space for the data. Null\n     * is returned when the length prefix is zero.\n     */\n    final T readLengthPrefixAndValue(ReadBuffer b) {\n      int length = b.readVarint32();\n      if (length == 0) return null;\n      return readValue(b, length);\n    }\n\n    abstract int sizeOfValue(T value);\n\n    abstract void writeValue(WriteBuffer b, T value);\n\n    /** @param length is greater than zero */\n    abstract T readValue(ReadBuffer b, int length);\n  }\n\n  static class BytesField extends LengthDelimitedField<byte[]> {\n    BytesField(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(byte[] bytes) {\n      return bytes.length;\n    }\n\n    @Override void writeValue(WriteBuffer b, byte[] bytes) {\n      b.write(bytes);\n    }\n\n    @Override byte[] readValue(ReadBuffer b, int length) {\n      return b.readBytes(length);\n    }\n  }\n\n  static class HexField extends LengthDelimitedField<String> {\n    HexField(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(String hex) {\n      if (hex == null) return 0;\n      return hex.length() / 2;\n    }\n\n    @Override void writeValue(WriteBuffer b, String hex) {\n      // similar logic to okio.ByteString.decodeHex\n      for (int i = 0, length = hex.length(); i < length; i++) {\n        int d1 = decodeLowerHex(hex.charAt(i++)) << 4;\n        int d2 = decodeLowerHex(hex.charAt(i));\n        b.writeByte((byte) (d1 + d2));\n      }\n    }\n\n    static int decodeLowerHex(char c) {\n      if (c >= '0' && c <= '9') return c - '0';\n      if (c >= 'a' && c <= 'f') return c - 'a' + 10;\n      throw new AssertionError(\"not lowerHex \" + c); // bug\n    }\n\n    @Override String readValue(ReadBuffer buffer, int length) {\n      return buffer.readBytesAsHex(length);\n    }\n  }\n\n  static class Utf8Field extends LengthDelimitedField<String> {\n    Utf8Field(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(String utf8) {\n      return utf8 != null ? utf8SizeInBytes(utf8) : 0;\n    }\n\n    @Override void writeValue(WriteBuffer b, String utf8) {\n      b.writeUtf8(utf8);\n    }\n\n    @Override String readValue(ReadBuffer buffer, int length) {\n      return buffer.readUtf8(length);\n    }\n  }\n\n  static final class Fixed64Field extends Field {\n    Fixed64Field(int key) {\n      super(key);\n      assert wireType == WIRETYPE_FIXED64;\n    }\n\n    void write(WriteBuffer b, long number) {\n      if (number == 0) return;\n      b.writeByte(key);\n      b.writeLongLe(number);\n    }\n\n    int sizeInBytes(long number) {\n      if (number == 0) return 0;\n      return 1 + 8; // tag + 8 byte number\n    }\n\n    long readValue(ReadBuffer buffer) {\n      return buffer.readLongLe();\n    }\n  }\n\n  static class VarintField extends Field {\n    VarintField(int key) {\n      super(key);\n      assert wireType == WIRETYPE_VARINT;\n    }\n\n    int sizeInBytes(int number) {\n      return number != 0 ? 1 + varintSizeInBytes(number) : 0; // tag + varint\n    }\n\n    void write(WriteBuffer b, int number) {\n      if (number == 0) return;\n      b.writeByte(key);\n      b.writeVarint(number);\n    }\n\n    int sizeInBytes(long number) {\n      return number != 0 ? 1 + varintSizeInBytes(number) : 0; // tag + varint\n    }\n\n    void write(WriteBuffer b, long number) {\n      if (number == 0) return;\n      b.writeByte(key);\n      b.writeVarint(number);\n    }\n  }\n\n  static final class BooleanField extends Field {\n    BooleanField(int key) {\n      super(key);\n      assert wireType == WIRETYPE_VARINT;\n    }\n\n    int sizeInBytes(boolean bool) {\n      return bool ? 2 : 0; // tag + varint\n    }\n\n    void write(WriteBuffer b, boolean bool) {\n      if (!bool) return;\n      b.writeByte(key);\n      b.writeByte(1);\n    }\n\n    boolean read(ReadBuffer b) {\n      byte bool = b.readByte();\n      if (bool < 0 || bool > 1) {\n        throw new IllegalArgumentException(\"Malformed: invalid boolean value at byte \" + b.pos());\n      }\n      return bool == 1;\n    }\n  }\n\n  // added for completion as later we will skip fields we don't use\n  static final class Fixed32Field extends Field {\n    Fixed32Field(int key) {\n      super(key);\n      assert wireType == WIRETYPE_FIXED32;\n    }\n\n    int sizeInBytes(int number) {\n      if (number == 0) return 0;\n      return 1 + 4; // tag + 4 byte number\n    }\n  }\n\n  static int sizeOfLengthDelimitedField(int sizeInBytes) {\n    return 1 + varintSizeInBytes(sizeInBytes) + sizeInBytes; // tag + len + bytes\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Proto3SpanWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport zipkin2.Span;\n\nimport static zipkin2.internal.Proto3Fields.sizeOfLengthDelimitedField;\nimport static zipkin2.internal.Proto3ZipkinFields.SPAN;\n\n//@Immutable\nfinal class Proto3SpanWriter implements WriteBuffer.Writer<Span> {\n\n  static final byte[] EMPTY_ARRAY = new byte[0];\n\n  @Override public int sizeInBytes(Span span) {\n    return SPAN.sizeInBytes(span);\n  }\n\n  @Override public void write(Span value, WriteBuffer b) {\n    SPAN.write(b, value);\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  /** Encodes per ListOfSpans data wireType, where field one is repeated spans */\n  public byte[] writeList(List<Span> spans) {\n    int lengthOfSpans = spans.size();\n    if (lengthOfSpans == 0) return EMPTY_ARRAY;\n    if (lengthOfSpans == 1) return write(spans.get(0));\n\n    int sizeInBytes = 0;\n    int[] sizeOfValues = new int[lengthOfSpans];\n    for (int i = 0; i < lengthOfSpans; i++) {\n      int sizeOfValue = sizeOfValues[i] = SPAN.sizeOfValue(spans.get(i));\n      sizeInBytes += sizeOfLengthDelimitedField(sizeOfValue);\n    }\n    byte[] result = new byte[sizeInBytes];\n    WriteBuffer writeBuffer = WriteBuffer.wrap(result);\n    for (int i = 0; i < lengthOfSpans; i++) {\n      writeSpan(spans.get(i), sizeOfValues[i], writeBuffer);\n    }\n    return result;\n  }\n\n  byte[] write(Span onlySpan) {\n    int sizeOfValue = SPAN.sizeOfValue(onlySpan);\n    byte[] result = new byte[sizeOfLengthDelimitedField(sizeOfValue)];\n    writeSpan(onlySpan, sizeOfValue, WriteBuffer.wrap(result));\n    return result;\n  }\n\n  // prevents resizing twice\n  void writeSpan(Span span, int sizeOfSpan, WriteBuffer result) {\n    result.writeByte(SPAN.key);\n    result.writeVarint(sizeOfSpan); // length prefix\n    SPAN.writeValue(result, span);\n  }\n\n  int writeList(List<Span> spans, byte[] out, int pos) {\n    int lengthOfSpans = spans.size();\n    if (lengthOfSpans == 0) return 0;\n\n    WriteBuffer result = WriteBuffer.wrap(out, pos);\n    for (Span span : spans) {\n      SPAN.write(result, span);\n    }\n    return result.pos() - pos;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Proto3ZipkinFields.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.logging.Logger;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Proto3Fields.BooleanField;\nimport zipkin2.internal.Proto3Fields.Utf8Field;\n\nimport static java.util.logging.Level.FINE;\nimport static zipkin2.internal.Proto3Fields.BytesField;\nimport static zipkin2.internal.Proto3Fields.Field.fieldNumber;\nimport static zipkin2.internal.Proto3Fields.Field.skipValue;\nimport static zipkin2.internal.Proto3Fields.Field.wireType;\nimport static zipkin2.internal.Proto3Fields.Fixed64Field;\nimport static zipkin2.internal.Proto3Fields.HexField;\nimport static zipkin2.internal.Proto3Fields.LengthDelimitedField;\nimport static zipkin2.internal.Proto3Fields.VarintField;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_FIXED64;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_LENGTH_DELIMITED;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_VARINT;\n\n/** Keys are used in this class because while verbose, it allows us to use switch statements */\n//@Immutable\nfinal class Proto3ZipkinFields {\n  static final Logger LOG = Logger.getLogger(Proto3ZipkinFields.class.getName());\n  /** This is the only field in the ListOfSpans type */\n  static final SpanField SPAN = new SpanField();\n\n  static class EndpointField extends LengthDelimitedField<Endpoint> {\n    static final int SERVICE_NAME_KEY = (1 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int IPV4_KEY = (2 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int IPV6_KEY = (3 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int PORT_KEY = (4 << 3) | WIRETYPE_VARINT;\n\n    static final Utf8Field SERVICE_NAME = new Utf8Field(SERVICE_NAME_KEY);\n    static final BytesField IPV4 = new BytesField(IPV4_KEY);\n    static final BytesField IPV6 = new BytesField(IPV6_KEY);\n    static final VarintField PORT = new VarintField(PORT_KEY);\n\n    EndpointField(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(Endpoint value) {\n      int result = 0;\n      result += SERVICE_NAME.sizeInBytes(value.serviceName());\n      if (value.ipv4Bytes() != null) result += 6; // tag + size of 4 + 4 bytes\n      if (value.ipv6Bytes() != null) result += 18; // tag + size of 16 + 16 bytes\n      result += PORT.sizeInBytes(value.portAsInt());\n      return result;\n    }\n\n    @Override void writeValue(WriteBuffer b, Endpoint value) {\n      SERVICE_NAME.write(b, value.serviceName());\n      IPV4.write(b, value.ipv4Bytes());\n      IPV6.write(b, value.ipv6Bytes());\n      PORT.write(b, value.portAsInt());\n    }\n\n    @Override Endpoint readValue(ReadBuffer buffer, int length) {\n      int endPos = buffer.pos() + length;\n\n      // now, we are in the endpoint fields\n      Endpoint.Builder builder = Endpoint.newBuilder();\n      while (buffer.pos() < endPos) {\n        int nextKey = buffer.readVarint32();\n        switch (nextKey) {\n          case SERVICE_NAME_KEY:\n            builder.serviceName(SERVICE_NAME.readLengthPrefixAndValue(buffer));\n            break;\n          case IPV4_KEY:\n            builder.parseIp(IPV4.readLengthPrefixAndValue(buffer));\n            break;\n          case IPV6_KEY:\n            builder.parseIp(IPV6.readLengthPrefixAndValue(buffer));\n            break;\n          case PORT_KEY:\n            builder.port(buffer.readVarint32());\n            break;\n          default:\n            logAndSkip(buffer, nextKey);\n        }\n      }\n      return builder.build();\n    }\n  }\n\n  /** Contributes to a builder as opposed to reading values directly. Avoids allocation. */\n  static abstract class SpanBuilderField<T> extends LengthDelimitedField<T> {\n\n    SpanBuilderField(int key) {\n      super(key);\n    }\n\n    @Override final T readValue(ReadBuffer b, int length) {\n      throw new UnsupportedOperationException();\n    }\n\n    abstract boolean readLengthPrefixAndValue(ReadBuffer b, Span.Builder builder);\n  }\n\n  static class AnnotationField extends SpanBuilderField<Annotation> {\n    static final int TIMESTAMP_KEY = (1 << 3) | WIRETYPE_FIXED64;\n    static final int VALUE_KEY = (2 << 3) | WIRETYPE_LENGTH_DELIMITED;\n\n    static final Fixed64Field TIMESTAMP = new Fixed64Field(TIMESTAMP_KEY);\n    static final Utf8Field VALUE = new Utf8Field(VALUE_KEY);\n\n    AnnotationField(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(Annotation value) {\n      return TIMESTAMP.sizeInBytes(value.timestamp()) + VALUE.sizeInBytes(value.value());\n    }\n\n    @Override void writeValue(WriteBuffer b, Annotation value) {\n      TIMESTAMP.write(b, value.timestamp());\n      VALUE.write(b, value.value());\n    }\n\n    @Override boolean readLengthPrefixAndValue(ReadBuffer b, Span.Builder builder) {\n      int length = b.readVarint32();\n      if (length == 0) return false;\n      int endPos = b.pos() + length;\n\n      // now, we are in the annotation fields\n      long timestamp = 0L;\n      String value = null;\n      while (b.pos() < endPos) {\n        int nextKey = b.readVarint32();\n        switch (nextKey) {\n          case TIMESTAMP_KEY:\n            timestamp = TIMESTAMP.readValue(b);\n            break;\n          case VALUE_KEY:\n            value = VALUE.readLengthPrefixAndValue(b);\n            break;\n          default:\n            logAndSkip(b, nextKey);\n        }\n      }\n      if (timestamp == 0L || value == null) return false;\n      builder.addAnnotation(timestamp, value);\n      return true;\n    }\n  }\n\n  static final class TagField extends SpanBuilderField<Map.Entry<String, String>> {\n    // map<string,string> in proto is a special field with key, value\n    static final int KEY_KEY = (1 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int VALUE_KEY = (2 << 3) | WIRETYPE_LENGTH_DELIMITED;\n\n    static final Utf8Field KEY = new Utf8Field(KEY_KEY);\n    static final Utf8Field VALUE = new Utf8Field(VALUE_KEY);\n\n    TagField(int key) {\n      super(key);\n    }\n\n    @Override int sizeOfValue(Map.Entry<String, String> value) {\n      return KEY.sizeInBytes(value.getKey()) + VALUE.sizeInBytes(value.getValue());\n    }\n\n    @Override void writeValue(WriteBuffer b, Map.Entry<String, String> value) {\n      KEY.write(b, value.getKey());\n      VALUE.write(b, value.getValue());\n    }\n\n    @Override boolean readLengthPrefixAndValue(ReadBuffer b, Span.Builder builder) {\n      int length = b.readVarint32();\n      if (length == 0) return false;\n      int endPos = b.pos() + length;\n\n      // now, we are in the tag fields\n      String key = null, value = \"\"; // empty tags allowed\n      while (b.pos() < endPos) {\n        int nextKey = b.readVarint32();\n        switch (nextKey) {\n          case KEY_KEY:\n            key = KEY.readLengthPrefixAndValue(b);\n            break;\n          case VALUE_KEY:\n            String read = VALUE.readLengthPrefixAndValue(b);\n            if (read != null) value = read; // empty tags allowed\n            break;\n          default:\n            logAndSkip(b, nextKey);\n        }\n      }\n      if (key == null) return false;\n      builder.putTag(key, value);\n      return true;\n    }\n  }\n\n  static class SpanField extends LengthDelimitedField<Span> {\n    static final int TRACE_ID_KEY = (1 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int PARENT_ID_KEY = (2 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int ID_KEY = (3 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int KIND_KEY = (4 << 3) | WIRETYPE_VARINT;\n    static final int NAME_KEY = (5 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int TIMESTAMP_KEY = (6 << 3) | WIRETYPE_FIXED64;\n    static final int DURATION_KEY = (7 << 3) | WIRETYPE_VARINT;\n    static final int LOCAL_ENDPOINT_KEY = (8 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int REMOTE_ENDPOINT_KEY = (9 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int ANNOTATION_KEY = (10 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int TAG_KEY = (11 << 3) | WIRETYPE_LENGTH_DELIMITED;\n    static final int DEBUG_KEY = (12 << 3) | WIRETYPE_VARINT;\n    static final int SHARED_KEY = (13 << 3) | WIRETYPE_VARINT;\n\n    static final HexField TRACE_ID = new HexField(TRACE_ID_KEY);\n    static final HexField PARENT_ID = new HexField(PARENT_ID_KEY);\n    static final HexField ID = new HexField(ID_KEY);\n    static final VarintField KIND = new VarintField(KIND_KEY);\n    static final Utf8Field NAME = new Utf8Field(NAME_KEY);\n    static final Fixed64Field TIMESTAMP = new Fixed64Field(TIMESTAMP_KEY);\n    static final VarintField DURATION = new VarintField(DURATION_KEY);\n    static final EndpointField LOCAL_ENDPOINT = new EndpointField(LOCAL_ENDPOINT_KEY);\n    static final EndpointField REMOTE_ENDPOINT = new EndpointField(REMOTE_ENDPOINT_KEY);\n    static final AnnotationField ANNOTATION = new AnnotationField(ANNOTATION_KEY);\n    static final TagField TAG = new TagField(TAG_KEY);\n    static final BooleanField DEBUG = new BooleanField(DEBUG_KEY);\n    static final BooleanField SHARED = new BooleanField(SHARED_KEY);\n\n    SpanField() {\n      super((1 << 3) | WIRETYPE_LENGTH_DELIMITED);\n    }\n\n    @Override int sizeOfValue(Span span) {\n      int sizeOfSpan = TRACE_ID.sizeInBytes(span.traceId());\n      sizeOfSpan += PARENT_ID.sizeInBytes(span.parentId());\n      sizeOfSpan += ID.sizeInBytes(span.id());\n      sizeOfSpan += KIND.sizeInBytes(span.kind() != null ? 1 : 0);\n      sizeOfSpan += NAME.sizeInBytes(span.name());\n      sizeOfSpan += TIMESTAMP.sizeInBytes(span.timestampAsLong());\n      sizeOfSpan += DURATION.sizeInBytes(span.durationAsLong());\n      sizeOfSpan += LOCAL_ENDPOINT.sizeInBytes(span.localEndpoint());\n      sizeOfSpan += REMOTE_ENDPOINT.sizeInBytes(span.remoteEndpoint());\n\n      List<Annotation> annotations = span.annotations();\n      for (Annotation annotation : annotations) {\n        sizeOfSpan += ANNOTATION.sizeInBytes(annotation);\n      }\n\n      Map<String, String> tags = span.tags();\n      int tagCount = tags.size();\n      if (tagCount > 0) { // avoid allocating an iterator when empty\n        for (Map.Entry<String, String> entry : tags.entrySet()) {\n          sizeOfSpan += TAG.sizeInBytes(entry);\n        }\n      }\n\n      sizeOfSpan += DEBUG.sizeInBytes(Boolean.TRUE.equals(span.debug()));\n      sizeOfSpan += SHARED.sizeInBytes(Boolean.TRUE.equals(span.shared()));\n      return sizeOfSpan;\n    }\n\n    @Override void writeValue(WriteBuffer b, Span value) {\n      TRACE_ID.write(b, value.traceId());\n      PARENT_ID.write(b, value.parentId());\n      ID.write(b, value.id());\n      KIND.write(b, toByte(value.kind()));\n      NAME.write(b, value.name());\n      TIMESTAMP.write(b, value.timestampAsLong());\n      DURATION.write(b, value.durationAsLong());\n      LOCAL_ENDPOINT.write(b, value.localEndpoint());\n      REMOTE_ENDPOINT.write(b, value.remoteEndpoint());\n\n      List<Annotation> annotations = value.annotations();\n      for (Annotation annotation : annotations) {\n        ANNOTATION.write(b, annotation);\n      }\n\n      Map<String, String> tags = value.tags();\n      if (!tags.isEmpty()) { // avoid allocating an iterator when empty\n        for (Map.Entry<String, String> entry : tags.entrySet()) {\n          TAG.write(b, entry);\n        }\n      }\n\n      SpanField.DEBUG.write(b, Boolean.TRUE.equals(value.debug()));\n      SpanField.SHARED.write(b, Boolean.TRUE.equals(value.shared()));\n    }\n\n    // in java, there's no zero index for unknown\n    int toByte(Span.Kind kind) {\n      return kind != null ? kind.ordinal() + 1 : 0;\n    }\n\n    public Span read(ReadBuffer buffer) {\n      buffer.readVarint32(); // toss the key\n      return readLengthPrefixAndValue(buffer);\n    }\n\n    @Override Span readValue(ReadBuffer buffer, int length) {\n      buffer.require(length); // more convenient to check up-front vs partially read\n      int endPos = buffer.pos() + length;\n\n      // now, we are in the span fields\n      Span.Builder builder = Span.newBuilder();\n      while (buffer.pos() < endPos) {\n        int nextKey = buffer.readVarint32();\n        switch (nextKey) {\n          case TRACE_ID_KEY:\n            builder.traceId(TRACE_ID.readLengthPrefixAndValue(buffer));\n            break;\n          case PARENT_ID_KEY:\n            builder.parentId(PARENT_ID.readLengthPrefixAndValue(buffer));\n            break;\n          case ID_KEY:\n            builder.id(ID.readLengthPrefixAndValue(buffer));\n            break;\n          case KIND_KEY:\n            int kind = buffer.readVarint32();\n            if (kind == 0) break;\n            if (kind > Span.Kind.values().length) break;\n            builder.kind(Span.Kind.values()[kind - 1]);\n            break;\n          case NAME_KEY:\n            builder.name(NAME.readLengthPrefixAndValue(buffer));\n            break;\n          case TIMESTAMP_KEY:\n            builder.timestamp(TIMESTAMP.readValue(buffer));\n            break;\n          case DURATION_KEY:\n            builder.duration(buffer.readVarint64());\n            break;\n          case LOCAL_ENDPOINT_KEY:\n            builder.localEndpoint(LOCAL_ENDPOINT.readLengthPrefixAndValue(buffer));\n            break;\n          case REMOTE_ENDPOINT_KEY:\n            builder.remoteEndpoint(REMOTE_ENDPOINT.readLengthPrefixAndValue(buffer));\n            break;\n          case ANNOTATION_KEY:\n            ANNOTATION.readLengthPrefixAndValue(buffer, builder);\n            break;\n          case TAG_KEY:\n            TAG.readLengthPrefixAndValue(buffer, builder);\n            break;\n          case DEBUG_KEY:\n            if (DEBUG.read(buffer)) builder.debug(true);\n            break;\n          case SHARED_KEY:\n            if (SHARED.read(buffer)) builder.shared(true);\n            break;\n          default:\n            logAndSkip(buffer, nextKey);\n        }\n      }\n      return builder.build();\n    }\n  }\n\n  static void logAndSkip(ReadBuffer buffer, int nextKey) {\n    int nextWireType = wireType(nextKey, buffer.pos());\n    if (LOG.isLoggable(FINE)) {\n      int nextFieldNumber = fieldNumber(nextKey, buffer.pos());\n      LOG.fine(String.format(\"Skipping field: byte=%s, fieldNumber=%s, wireType=%s\",\n        buffer.pos(), nextFieldNumber, nextWireType));\n    }\n    skipValue(buffer, nextWireType);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/ReadBuffer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.internal.HexCodec.HEX_DIGITS;\n\n/** Read operations do bounds checks, as typically more errors occur reading than writing. */\npublic abstract class ReadBuffer extends InputStream {\n\n  /** Do not use the buffer passed here after, as it may be manipulated directly. */\n  public static ReadBuffer wrapUnsafe(ByteBuffer buffer) {\n    if (buffer.hasArray()) {\n      int offset = buffer.arrayOffset() + buffer.position();\n      return wrap(buffer.array(), offset, buffer.remaining());\n    }\n    return buffer.order() == ByteOrder.BIG_ENDIAN\n      ? new BigEndianByteBuffer(buffer)\n      : new LittleEndianByteBuffer(buffer);\n  }\n\n  public static ReadBuffer wrap(byte[] bytes) {\n    return wrap(bytes, 0, bytes.length);\n  }\n\n  public static ReadBuffer wrap(byte[] bytes, int pos, int length) {\n    return new ReadBuffer.Array(bytes, pos, length);\n  }\n\n  static final class BigEndianByteBuffer extends Buff {\n    BigEndianByteBuffer(ByteBuffer buf) {\n      super(buf);\n    }\n\n    @Override short readShort() {\n      require(2);\n      return buf.getShort();\n    }\n\n    @Override int readInt() {\n      require(4);\n      return buf.getInt();\n    }\n\n    @Override long readLong() {\n      require(8);\n      return buf.getLong();\n    }\n\n    @Override long readLongLe() {\n      return Long.reverseBytes(readLong());\n    }\n  }\n\n  static final class LittleEndianByteBuffer extends Buff {\n    LittleEndianByteBuffer(ByteBuffer buf) {\n      super(buf);\n    }\n\n    @Override short readShort() {\n      require(2);\n      return Short.reverseBytes(buf.getShort());\n    }\n\n    @Override int readInt() {\n      require(4);\n      return Integer.reverseBytes(buf.getInt());\n    }\n\n    @Override long readLong() {\n      return Long.reverseBytes(readLongLe());\n    }\n\n    @Override long readLongLe() {\n      require(8);\n      return buf.getLong();\n    }\n  }\n\n  static abstract class Buff extends ReadBuffer {\n    final ByteBuffer buf; // visible for testing\n\n    Buff(ByteBuffer buf) {\n      this.buf = buf;\n    }\n\n    @Override final byte readByteUnsafe() {\n      return buf.get();\n    }\n\n    @Override final byte[] readBytes(int length) {\n      require(length);\n      byte[] copy = new byte[length];\n      buf.get(copy);\n      return copy;\n    }\n\n    // Encoding zipkin data is supported in JRE 6, but decoding isn't.\n    @Override boolean tryReadAscii(char[] destination, int length) {\n      buf.mark(); // This is not Java 6\n      for (int i = 0; i < length; i++) {\n        byte b = buf.get();\n        if ((b & 0x80) != 0) {\n          buf.reset(); // This is not Java 6\n          return false;  // Not 7-bit ASCII character\n        }\n        destination[i] = (char) b;\n      }\n      return true;\n    }\n\n    @Override final String doReadUtf8(int length) {\n      return new String(readBytes(length), UTF_8);\n    }\n\n    @Override public int pos() {\n      return buf.position();\n    }\n\n    @Override public int read(byte[] dst, int offset, int length) {\n      if (available() == 0) return -1;\n      int toRead = checkReadArguments(dst, offset, length);\n      if (toRead == 0) return 0;\n      buf.get(dst, offset, toRead);\n      return toRead;\n    }\n\n    // Encoding zipkin data is supported in JRE 6, but decoding isn't.\n    @Override public long skip(long maxCount) {\n      int skipped = Math.max(available(), (int) maxCount);\n      buf.position(buf.position() + skipped); // This is not Java 6\n      return skipped;\n    }\n\n    @Override public int available() {\n      return buf.remaining();\n    }\n  }\n\n  static final class Array extends ReadBuffer {\n    final byte[] buf;\n    final int arrayOffset, length;\n    int offset;\n\n    Array(byte[] buf, int offset, int length) {\n      this.buf = buf;\n      this.arrayOffset = this.offset = offset;\n      this.length = length;\n    }\n\n    @Override byte readByteUnsafe() {\n      return buf[offset++];\n    }\n\n    @Override byte[] readBytes(int length) {\n      require(length);\n      byte[] result = new byte[length];\n      System.arraycopy(buf, offset, result, 0, length);\n      offset += length;\n      return result;\n    }\n\n    @Override public int read(byte[] dst, int offset, int length) {\n      if (available() == 0) return -1;\n      int toRead = checkReadArguments(dst, offset, length);\n      if (toRead == 0) return 0;\n      System.arraycopy(buf, this.offset, dst, 0, toRead);\n      this.offset += toRead;\n      return toRead;\n    }\n\n    @Override boolean tryReadAscii(char[] destination, int length) {\n      for (int i = 0; i < length; i++) {\n        byte b = buf[offset + i];\n        if ((b & 0x80) != 0) return false;  // Not 7-bit ASCII character\n        destination[i] = (char) b;\n      }\n      offset += length;\n      return true;\n    }\n\n    @Override String doReadUtf8(int length) {\n      String result = new String(buf, offset, length, UTF_8);\n      offset += length;\n      return result;\n    }\n\n    @Override short readShort() {\n      require(2);\n      return (short) ((buf[offset++] & 0xff) << 8 | (buf[offset++] & 0xff));\n    }\n\n    @Override int readInt() {\n      require(4);\n      int pos = this.offset;\n      this.offset = pos + 4;\n      return (buf[pos] & 0xff) << 24\n        | (buf[pos + 1] & 0xff) << 16\n        | (buf[pos + 2] & 0xff) << 8\n        | (buf[pos + 3] & 0xff);\n    }\n\n    /** Code is optimized for little endian as proto is the encouraged format. */\n    @Override long readLong() {\n      return Long.reverseBytes(readLongLe());\n    }\n\n    @Override long readLongLe() {\n      require(8);\n      int pos = this.offset;\n      this.offset = pos + 8;\n      return (buf[pos] & 0xffL)\n        | (buf[pos + 1] & 0xffL) << 8\n        | (buf[pos + 2] & 0xffL) << 16\n        | (buf[pos + 3] & 0xffL) << 24\n        | (buf[pos + 4] & 0xffL) << 32\n        | (buf[pos + 5] & 0xffL) << 40\n        | (buf[pos + 6] & 0xffL) << 48\n        | (buf[pos + 7] & 0xffL) << 56;\n    }\n\n    @Override public int pos() {\n      return offset - arrayOffset;\n    }\n\n    @Override public long skip(long maxCount) {\n      int toSkip = Math.min(available(), (int) maxCount);\n      offset += toSkip;\n      return toSkip;\n    }\n\n    @Override public int available() {\n      return length - (offset - arrayOffset);\n    }\n  }\n\n  @Override public abstract int read(byte[] dst, int offset, int length);\n\n  @Override public abstract long skip(long n);\n\n  @Override public abstract int available();\n\n  @Override public void close() {\n  }\n\n  @Override public void mark(int readlimit) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override public synchronized void reset() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override public boolean markSupported() {\n    return false;\n  }\n\n  /** only use when you've already ensured the length you need is available */\n  abstract byte readByteUnsafe();\n\n  final byte readByte() {\n    require(1);\n    return readByteUnsafe();\n  }\n\n  abstract byte[] readBytes(int length);\n\n  final String readUtf8(int length) {\n    if (length == 0) return \"\"; // ex error tag with no value\n    require(length);\n    if (length > RecyclableBuffers.SHORT_STRING_LENGTH) return doReadUtf8(length);\n\n    // Speculatively assume all 7-bit ASCII characters.. common in normal tags and names\n    char[] buffer = RecyclableBuffers.shortStringBuffer();\n    if (tryReadAscii(buffer, length)) {\n      return new String(buffer, 0, length);\n    }\n    return doReadUtf8(length);\n  }\n\n  abstract boolean tryReadAscii(char[] destination, int length);\n\n  abstract String doReadUtf8(int length);\n\n  abstract int pos();\n\n  abstract short readShort();\n\n  abstract int readInt();\n\n  abstract long readLong();\n\n  abstract long readLongLe();\n\n  @Override public final int read() {\n    return available() > 0 ? readByteUnsafe() : -1;\n  }\n\n  final String readBytesAsHex(int length) {\n    // All our hex fields are at most 32 characters.\n    if (length > 32) {\n      throw new IllegalArgumentException(\"hex field greater than 32 chars long: \" + length);\n    }\n\n    require(length);\n    char[] result = RecyclableBuffers.shortStringBuffer();\n    int hexLength = length * 2;\n    for (int i = 0; i < hexLength; i += 2) {\n      byte b = readByteUnsafe();\n      result[i + 0] = HEX_DIGITS[(b >> 4) & 0xf];\n      result[i + 1] = HEX_DIGITS[b & 0xf];\n    }\n    return new String(result, 0, hexLength);\n  }\n\n  /**\n   * @return the value read. Use {@link WriteBuffer#varintSizeInBytes(long)} to tell how many bytes.\n   * @throws IllegalArgumentException if more than 64 bits were encoded\n   */\n  // included in the main api as this is used commonly, for example reading proto tags\n  final int readVarint32() {\n    byte b; // negative number implies MSB set\n    if ((b = readByte()) >= 0) {\n      return b;\n    }\n    int result = b & 0x7f;\n\n    if ((b = readByte()) >= 0) {\n      return result | b << 7;\n    }\n    result |= (b & 0x7f) << 7;\n\n    if ((b = readByte()) >= 0) {\n      return result | b << 14;\n    }\n    result |= (b & 0x7f) << 14;\n\n    if ((b = readByte()) >= 0) {\n      return result | b << 21;\n    }\n    result |= (b & 0x7f) << 21;\n\n    b = readByte();\n    if ((b & 0xf0) != 0) {\n      throw new IllegalArgumentException(\"Greater than 32-bit varint at position \" + (pos() - 1));\n    }\n    return result | b << 28;\n  }\n\n  final long readVarint64() {\n    byte b; // negative number implies MSB set\n    if ((b = readByte()) >= 0) {\n      return b;\n    }\n\n    long result = b & 0x7f;\n    for (int i = 1; b < 0 && i < 10; i++) {\n      b = readByte();\n      if (i == 9 && (b & 0xf0) != 0) {\n        throw new IllegalArgumentException(\"Greater than 64-bit varint at position \" + (pos() - 1));\n      }\n      result |= (long) (b & 0x7f) << (i * 7);\n    }\n    return result;\n  }\n\n  final void require(int byteCount) {\n    if (this.available() < byteCount) {\n      throw new IllegalArgumentException(\n        \"Truncated: length \" + byteCount + \" > bytes available \" + this.available());\n    }\n  }\n\n  int checkReadArguments(byte[] dst, int offset, int length) {\n    if (dst == null) throw new NullPointerException();\n    if (offset < 0 || length < 0 || length > dst.length - offset) {\n      throw new IndexOutOfBoundsException();\n    }\n    return Math.min(available(), length);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/RecyclableBuffers.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\npublic final class RecyclableBuffers {\n  RecyclableBuffers() {\n  }\n\n  static final ThreadLocal<char[]> SHORT_STRING_BUFFER = new ThreadLocal<>();\n  /**\n   * Maximum character length constraint of most names, IP literals and IDs.\n   */\n  public static final int SHORT_STRING_LENGTH = 256;\n\n  /**\n   * Returns a {@link ThreadLocal} reused {@code char[]} for use when decoding bytes into hex, IP\n   * literals, or {@link #SHORT_STRING_LENGTH short strings}. The buffer must never be leaked\n   * outside the method. Most will {@link String#String(char[], int, int) copy it into a string}.\n   */\n  public static char[] shortStringBuffer() {\n    char[] shortStringBuffer = SHORT_STRING_BUFFER.get();\n    if (shortStringBuffer == null) {\n      shortStringBuffer = new char[SHORT_STRING_LENGTH];\n      SHORT_STRING_BUFFER.set(shortStringBuffer);\n    }\n    return shortStringBuffer;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/SpanNode.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.logging.Logger;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.lang.String.format;\nimport static java.util.logging.Level.FINE;\n\n/**\n * Convenience type representing a trace tree. Multiple Zipkin features require a trace tree. For\n * example, looking at network boundaries to correct clock skew and aggregating requests paths imply\n * visiting the tree.\n */\npublic final class SpanNode {\n  static final Comparator<SpanNode> NODE_COMPARATOR = new Comparator<SpanNode>() {\n    @Override public int compare(SpanNode left, SpanNode right) {\n      long x = left.span().timestampAsLong(), y = right.span().timestampAsLong();\n      return (x < y) ? -1 : ((x == y) ? 0 : 1); // Long.compareTo is JRE 7+\n    }\n  };\n\n  public static SpanNode.Builder newBuilder(Logger logger) {\n    return new SpanNode.Builder(logger);\n  }\n\n  /** Set via {@link #addChild(SpanNode)} */\n  @Nullable SpanNode parent;\n  /** Null when a synthetic root node */\n  @Nullable final Span span;\n  /** mutable to avoid allocating lists for childless nodes */\n  List<SpanNode> children = Collections.emptyList();\n\n  SpanNode(@Nullable Span span) {\n    this.span = span;\n  }\n\n  /** Returns the parent, or null if root */\n  @Nullable public SpanNode parent() {\n    return parent;\n  }\n\n  /** Returns the span, or null if a synthetic root node */\n  @Nullable public Span span() {\n    return span;\n  }\n\n  /** Returns the children of this node. */\n  public List<SpanNode> children() {\n    return children;\n  }\n\n  /** Traverses the tree, breadth-first. */\n  public Iterator<SpanNode> traverse() {\n    return new BreadthFirstIterator(this);\n  }\n\n  static final class BreadthFirstIterator implements Iterator<SpanNode> {\n    final ArrayDeque<SpanNode> queue = new ArrayDeque<>();\n\n    BreadthFirstIterator(SpanNode root) {\n      // since the input data could be headless, we first push onto the queue the root-most spans\n      if (root.span == null) { // synthetic root\n        for (int i = 0, length = root.children.size(); i < length; i++) {\n          queue.add(root.children.get(i));\n        }\n      } else {\n        queue.add(root);\n      }\n    }\n\n    @Override public boolean hasNext() {\n      return !queue.isEmpty();\n    }\n\n    @Override public SpanNode next() {\n      if (!hasNext()) throw new NoSuchElementException();\n      SpanNode result = queue.remove();\n      for (int i = 0, length = result.children.size(); i < length; i++) {\n        queue.add(result.children.get(i));\n      }\n      return result;\n    }\n\n    @Override public void remove() {\n      throw new UnsupportedOperationException(\"remove\");\n    }\n  }\n\n  /** Adds the child IFF it isn't already a child. */\n  SpanNode addChild(SpanNode child) {\n    if (child == null) throw new NullPointerException(\"child == null\");\n    if (child == this) throw new IllegalArgumentException(\"circular dependency on \" + this);\n    if (children.equals(Collections.emptyList())) children = new ArrayList<>();\n    children.add(child);\n    child.parent = this;\n    return this;\n  }\n\n  public static final class Builder {\n    final Logger logger;\n\n    Builder(Logger logger) {\n      this.logger = logger;\n    }\n\n    SpanNode rootSpan = null;\n    final Map<Object, SpanNode> keyToNode = new LinkedHashMap<>();\n    final Map<Object, Object> spanToParent = new LinkedHashMap<>();\n\n    void clear() {\n      rootSpan = null;\n      keyToNode.clear();\n      spanToParent.clear();\n    }\n\n    /**\n     * Builds a trace tree by merging and processing the input or returns an empty tree.\n     *\n     * <p>While the input can be incomplete or redundant, they must all be a part of the same trace\n     * (e.g. all share the same {@link Span#traceId()}).\n     */\n    public SpanNode build(List<Span> spans) {\n      if (spans.isEmpty()) throw new IllegalArgumentException(\"spans were empty\");\n      clear();\n\n      // In order to make a tree, we need clean data. This will merge any duplicates so that we\n      // don't have redundant leaves on the tree.\n      List<Span> cleaned = Trace.merge(spans);\n      int length = cleaned.size();\n      String traceId = cleaned.get(0).traceId();\n\n      if (logger.isLoggable(FINE)) logger.fine(\"building trace tree: traceId=\" + traceId);\n\n      // Next, index all the spans so that we can understand any relationships.\n      for (int i = 0; i < length; i++) {\n        index(cleaned.get(i));\n      }\n\n      // Now that we've index references to all spans, we can revise any parent-child relationships.\n      // Notably, by now, we can tell which is the root-most.\n      for (int i = 0; i < length; i++) {\n        process(cleaned.get(i));\n      }\n\n      // If we haven't found any root span, we can still make a tree using a synthetic node.\n      if (rootSpan == null) {\n        if (logger.isLoggable(FINE)) {\n          logger.fine(\"substituting dummy node for missing root span: traceId=\" + traceId);\n        }\n        rootSpan = new SpanNode(null);\n      }\n\n      // At this point, we have the most reliable parent-child relationships and can allocate spans\n      // corresponding the best place in the trace tree.\n      for (Map.Entry<Object, Object> entry : spanToParent.entrySet()) {\n        SpanNode child = keyToNode.get(entry.getKey());\n        SpanNode parent = keyToNode.get(entry.getValue());\n\n        if (parent == null) { // Handle headless by attaching spans missing parents to root\n          rootSpan.addChild(child);\n        } else {\n          parent.addChild(child);\n        }\n      }\n      sortTreeByTimestamp(rootSpan);\n      return rootSpan;\n    }\n\n    /** Sorts children at the same level by {@link Span#timestampAsLong()} ascending */\n    void sortTreeByTimestamp(SpanNode root) {\n      ArrayDeque<SpanNode> queue = new ArrayDeque<>();\n      queue.add(root);\n\n      while (!queue.isEmpty()) {\n        SpanNode current = queue.pop();\n        if (current.children().isEmpty()) continue;\n        Collections.sort(current.children(), NODE_COMPARATOR);\n        queue.addAll(current.children());\n      }\n    }\n\n    /**\n     * We index spans by (id, shared, localEndpoint) before processing them. This latter fields\n     * (shared, endpoint) are important because in zipkin (specifically B3), a server can share\n     * (re-use) the same ID as its client. This impacts processing quite a bit when multiple servers\n     * share one span ID.\n     *\n     * <p>In a Zipkin trace, a parent (client) and child (server) can share the same ID if in an\n     * RPC. If two different servers respond to the same client, the only way for us to tell which\n     * is which is by endpoint. Our goal is to retain full paths across multiple endpoints. Even\n     * though instrumentation should be configured in such a way that a client never sends the same\n     * span ID to multiple servers, it can happen. Accordingly, we index defensively including any\n     * endpoint data that might be available.\n     */\n    void index(Span span) {\n      Object idKey, parentKey;\n      if (Boolean.TRUE.equals(span.shared())) {\n        // we need to classify a shared span by its endpoint in case multiple servers respond to the\n        // same ID sent by the client.\n        idKey = createKey(span.id(), true, span.localEndpoint());\n        // the parent of a server span is a client, which is not ambiguous for a given span ID.\n        parentKey = span.id();\n      } else {\n        idKey = span.id();\n        parentKey = span.parentId();\n      }\n      spanToParent.put(idKey, parentKey);\n    }\n\n    /**\n     * Processing is taking a span and placing it at the most appropriate place in the trace tree.\n     * For example, if this is a {@link Span.Kind#SERVER} span, it would be a different node, and a\n     * child of its {@link Span.Kind#CLIENT} even if they share the same span ID.\n     *\n     * <p>Processing is defensive of typical problems in span reporting, such as depth-first. For\n     * example, depth-first reporting implies you can see spans missing their parent. Hence, the\n     * result of processing all spans can be a virtual root node.\n     */\n    void process(Span span) {\n      Endpoint endpoint = span.localEndpoint();\n      boolean shared = Boolean.TRUE.equals(span.shared());\n      Object key = createKey(span.id(), shared, span.localEndpoint());\n      Object noEndpointKey = endpoint != null ? createKey(span.id(), shared, null) : key;\n\n      Object parent = null;\n      if (shared) {\n        // Shared is a server span. It will very likely be on a different endpoint than the client.\n        // Clients are not ambiguous by ID, so we don't need to qualify by endpoint.\n        parent = span.id();\n      } else if (span.parentId() != null) {\n        // We are not a root span, and not a shared server span. Proceed in most specific to least.\n\n        // We could be the child of a shared server span (ex a local (intermediate) span on the same\n        // endpoint). This is the most specific case, so we try this first.\n        parent = createKey(span.parentId(), true, endpoint);\n        if (spanToParent.containsKey(parent)) {\n          spanToParent.put(noEndpointKey, parent);\n        } else {\n          // If there's no shared parent, fall back to normal case which is unqualified beyond ID.\n          parent = span.parentId();\n        }\n      } else { // we are root or don't know our parent\n        if (rootSpan != null) {\n          if (logger.isLoggable(FINE)) {\n            logger.fine(format(\n              \"attributing span missing parent to root: traceId=%s, rootSpanId=%s, spanId=%s\",\n              span.traceId(), rootSpan.span().id(), span.id()));\n          }\n        }\n      }\n\n      SpanNode node = new SpanNode(span);\n      // special-case root, and attribute missing parents to it. In\n      // other words, assume that the first root is the \"real\" root.\n      if (parent == null && rootSpan == null) {\n        rootSpan = node;\n        spanToParent.remove(noEndpointKey);\n      } else if (shared) {\n        // In the case of shared server span, we need to address it both ways, in case intermediate\n        // spans are lacking endpoint information.\n        keyToNode.put(key, node);\n        keyToNode.put(noEndpointKey, node);\n      } else {\n        keyToNode.put(noEndpointKey, node);\n      }\n    }\n  }\n\n  static Object createKey(String id, boolean shared, @Nullable Endpoint endpoint) {\n    if (!shared) return id;\n    return new SharedKey(id, endpoint);\n  }\n\n  /**\n   * A span in the tree is not always unique on ID. Sharing is allowed once per ID (Ex: in RPC).\n   * However, it is possible in a retry scenario for accidental duplicate ID sharing to occur\n   */\n  static final class SharedKey {\n    final String id;\n    @Nullable final Endpoint endpoint;\n\n    SharedKey(String id, @Nullable Endpoint endpoint) {\n      if (id == null) throw new NullPointerException(\"id == null\");\n      this.id = id;\n      this.endpoint = endpoint;\n    }\n\n    @Override public String toString() {\n      return \"SharedKey{id=\" + id + \", endpoint=\" + endpoint + \"}\";\n    }\n\n    @Override public boolean equals(Object o) {\n      if (o == this) return true;\n      if (!(o instanceof SharedKey)) return false;\n      SharedKey that = (SharedKey) o;\n      return id.equals(that.id) && Objects.equals(endpoint, that.endpoint);\n    }\n\n    @Override public int hashCode() {\n      int result = 1;\n      result *= 1000003;\n      result ^= id.hashCode();\n      result *= 1000003;\n      result ^= (endpoint == null) ? 0 : endpoint.hashCode();\n      return result;\n    }\n  }\n\n  @Override public String toString() {\n    List<Span> childrenSpans = new ArrayList<>();\n    for (SpanNode child : children) {\n      childrenSpans.add(child.span);\n    }\n    return \"SpanNode{parent=\" + (parent != null ? parent.span : null)\n      + \", span=\" + span + \", children=\" + childrenSpans + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/ThriftCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.EOFException;\nimport java.nio.BufferUnderflowException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.Span;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\nimport static zipkin2.internal.ThriftField.TYPE_BOOL;\nimport static zipkin2.internal.ThriftField.TYPE_BYTE;\nimport static zipkin2.internal.ThriftField.TYPE_DOUBLE;\nimport static zipkin2.internal.ThriftField.TYPE_I16;\nimport static zipkin2.internal.ThriftField.TYPE_I32;\nimport static zipkin2.internal.ThriftField.TYPE_I64;\nimport static zipkin2.internal.ThriftField.TYPE_LIST;\nimport static zipkin2.internal.ThriftField.TYPE_MAP;\nimport static zipkin2.internal.ThriftField.TYPE_SET;\nimport static zipkin2.internal.ThriftField.TYPE_STOP;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.ThriftField.TYPE_STRUCT;\n\n// @Immutable\npublic final class ThriftCodec {\n  // break vs recursing infinitely when skipping data\n  static final int MAX_SKIP_DEPTH = 2147483647;\n\n  final V1ThriftSpanWriter writer = new V1ThriftSpanWriter();\n\n  public int sizeInBytes(Span input) {\n    return writer.sizeInBytes(input);\n  }\n\n  public byte[] write(Span span) {\n    return writer.write(span);\n  }\n\n  /** Encoding overhead is thrift type plus 32-bit length prefix */\n  static <T> int listSizeInBytes(WriteBuffer.Writer<T> writer, List<T> values) {\n    int sizeInBytes = 5;\n    for (T value : values) {\n      sizeInBytes += writer.sizeInBytes(value);\n    }\n    return sizeInBytes;\n  }\n\n  public static boolean read(ReadBuffer buffer, Collection<Span> out) {\n    if (buffer.available() == 0) return false;\n    try {\n      V1Span v1Span = new V1ThriftSpanReader().read(buffer);\n      V1SpanConverter.create().convert(v1Span, out);\n      return true;\n    } catch (Exception e) {\n      throw exceptionReading(\"Span\", e);\n    }\n  }\n\n  @Nullable\n  public static Span readOne(ReadBuffer buffer) {\n    if (buffer.available() == 0) return null;\n    try {\n      V1Span v1Span = new V1ThriftSpanReader().read(buffer);\n      List<Span> out = new ArrayList<>(1);\n      V1SpanConverter.create().convert(v1Span, out);\n      return out.get(0);\n    } catch (Exception e) {\n      throw exceptionReading(\"Span\", e);\n    }\n  }\n\n  public static boolean readList(ReadBuffer buffer, Collection<Span> out) {\n    int length = buffer.available();\n    if (length == 0) return false;\n    try {\n      int listLength = readListLength(buffer);\n      if (listLength == 0) return false;\n      V1ThriftSpanReader reader = new V1ThriftSpanReader();\n      V1SpanConverter converter = V1SpanConverter.create();\n      for (int i = 0; i < listLength; i++) {\n        V1Span v1Span = reader.read(buffer);\n        converter.convert(v1Span, out);\n      }\n    } catch (Exception e) {\n      throw exceptionReading(\"List<Span>\", e);\n    }\n    return true;\n  }\n\n  static int readListLength(ReadBuffer buffer) {\n    buffer.readByte(); // we ignore the type\n    return buffer.readInt();\n  }\n\n  static <T> void writeList(WriteBuffer.Writer<T> writer, List<T> value, WriteBuffer buffer) {\n    int length = value.size();\n    writeListBegin(buffer, length);\n    for (int i = 0; i < length; i++) {\n      writer.write(value.get(i), buffer);\n    }\n  }\n\n  static IllegalArgumentException exceptionReading(String type, Exception e) {\n    String cause = e.getMessage() == null ? \"Error\" : e.getMessage();\n    if (e instanceof EOFException) cause = \"EOF\";\n    if (e instanceof IllegalStateException || e instanceof BufferUnderflowException) {\n      cause = \"Malformed\";\n    }\n    String message = String.format(\"%s reading %s from TBinary\", cause, type);\n    throw new IllegalArgumentException(message, e);\n  }\n\n  static void skip(ReadBuffer buffer, byte type) {\n    skip(buffer, type, MAX_SKIP_DEPTH);\n  }\n\n  static void skip(ReadBuffer buffer, byte type, int maxDepth) {\n    if (maxDepth <= 0) throw new IllegalStateException(\"Maximum skip depth exceeded\");\n    switch (type) {\n      case TYPE_BOOL:\n      case TYPE_BYTE:\n        buffer.skip(1);\n        break;\n      case TYPE_I16:\n        buffer.skip(2);\n        break;\n      case TYPE_I32:\n        buffer.skip(4);\n        break;\n      case TYPE_DOUBLE:\n      case TYPE_I64:\n        buffer.skip(8);\n        break;\n      case TYPE_STRING:\n        buffer.skip(buffer.readInt());\n        break;\n      case TYPE_STRUCT:\n        while (true) {\n          ThriftField thriftField = ThriftField.read(buffer);\n          if (thriftField.type == TYPE_STOP) return;\n          skip(buffer, thriftField.type, maxDepth - 1);\n        }\n      case TYPE_MAP:\n        byte keyType = buffer.readByte();\n        byte valueType = buffer.readByte();\n        for (int i = 0, length = buffer.readInt(); i < length; i++) {\n          skip(buffer, keyType, maxDepth - 1);\n          skip(buffer, valueType, maxDepth - 1);\n        }\n        break;\n      case TYPE_SET:\n      case TYPE_LIST:\n        byte elemType = buffer.readByte();\n        for (int i = 0, length = buffer.readInt(); i < length; i++) {\n          skip(buffer, elemType, maxDepth - 1);\n        }\n        break;\n      default: // types that don't need explicit skipping\n        break;\n    }\n  }\n\n  static void writeListBegin(WriteBuffer buffer, int size) {\n    buffer.writeByte(TYPE_STRUCT);\n    writeInt(buffer, size);\n  }\n\n  static void writeLengthPrefixed(WriteBuffer buffer, String utf8) {\n    writeInt(buffer, WriteBuffer.utf8SizeInBytes(utf8));\n    buffer.writeUtf8(utf8);\n  }\n\n  static void writeInt(WriteBuffer buf, int v) {\n    buf.writeByte((byte) ((v >>> 24L) & 0xff));\n    buf.writeByte((byte) ((v >>> 16L) & 0xff));\n    buf.writeByte((byte) ((v >>> 8L) & 0xff));\n    buf.writeByte((byte) (v & 0xff));\n  }\n\n  static void writeLong(WriteBuffer buf, long v) {\n    buf.writeByte((byte) ((v >>> 56L) & 0xff));\n    buf.writeByte((byte) ((v >>> 48L) & 0xff));\n    buf.writeByte((byte) ((v >>> 40L) & 0xff));\n    buf.writeByte((byte) ((v >>> 32L) & 0xff));\n    buf.writeByte((byte) ((v >>> 24L) & 0xff));\n    buf.writeByte((byte) ((v >>> 16L) & 0xff));\n    buf.writeByte((byte) ((v >>> 8L) & 0xff));\n    buf.writeByte((byte) (v & 0xff));\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/ThriftEndpointCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport zipkin2.Endpoint;\n\nimport static zipkin2.internal.ThriftCodec.skip;\nimport static zipkin2.internal.ThriftField.TYPE_I16;\nimport static zipkin2.internal.ThriftField.TYPE_I32;\nimport static zipkin2.internal.ThriftField.TYPE_STOP;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.WriteBuffer.utf8SizeInBytes;\n\nfinal class ThriftEndpointCodec {\n  static final byte[] INT_ZERO = {0, 0, 0, 0};\n  static final ThriftField IPV4 = new ThriftField(TYPE_I32, 1);\n  static final ThriftField PORT = new ThriftField(TYPE_I16, 2);\n  static final ThriftField SERVICE_NAME = new ThriftField(TYPE_STRING, 3);\n  static final ThriftField IPV6 = new ThriftField(TYPE_STRING, 4);\n\n  static Endpoint read(ReadBuffer buffer) {\n    Endpoint.Builder result = Endpoint.newBuilder();\n\n    while (true) {\n      ThriftField thriftField = ThriftField.read(buffer);\n      if (thriftField.type == TYPE_STOP) break;\n\n      if (thriftField.isEqualTo(IPV4)) {\n        int ipv4 = buffer.readInt();\n        if (ipv4 != 0) {\n          result.parseIp( // allocation is ok here as Endpoint.ipv4Bytes would anyway\n            new byte[] {\n              (byte) (ipv4 >> 24 & 0xff),\n              (byte) (ipv4 >> 16 & 0xff),\n              (byte) (ipv4 >> 8 & 0xff),\n              (byte) (ipv4 & 0xff)\n            });\n        }\n      } else if (thriftField.isEqualTo(PORT)) {\n        result.port(buffer.readShort() & 0xFFFF);\n      } else if (thriftField.isEqualTo(SERVICE_NAME)) {\n        result.serviceName(buffer.readUtf8(buffer.readInt()));\n      } else if (thriftField.isEqualTo(IPV6)) {\n        result.parseIp(buffer.readBytes(buffer.readInt()));\n      } else {\n        skip(buffer, thriftField.type);\n      }\n    }\n    return result.build();\n  }\n\n  static int sizeInBytes(Endpoint value) {\n    String serviceName = value.serviceName();\n    int sizeInBytes = 0;\n    sizeInBytes += 3 + 4; // IPV4\n    sizeInBytes += 3 + 2; // PORT\n    sizeInBytes += 3 + 4 + (serviceName != null ? utf8SizeInBytes(serviceName) : 0);\n    if (value.ipv6() != null) sizeInBytes += 3 + 4 + 16;\n    sizeInBytes++; // TYPE_STOP\n    return sizeInBytes;\n  }\n\n  static void write(Endpoint value, WriteBuffer buffer) {\n    IPV4.write(buffer);\n    buffer.write(value.ipv4Bytes() != null ? value.ipv4Bytes() : INT_ZERO);\n\n    PORT.write(buffer);\n    int port = value.portAsInt();\n    // write short!\n    buffer.writeByte((port >>> 8L) & 0xff);\n    buffer.writeByte(port & 0xff);\n\n    SERVICE_NAME.write(buffer);\n    ThriftCodec.writeLengthPrefixed(buffer, value.serviceName() != null ? value.serviceName() : \"\");\n\n    byte[] ipv6 = value.ipv6Bytes();\n    if (ipv6 != null) {\n      IPV6.write(buffer);\n      ThriftCodec.writeInt(buffer, 16);\n      buffer.write(ipv6);\n    }\n\n    buffer.writeByte(TYPE_STOP);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/ThriftField.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nfinal class ThriftField {\n  // taken from org.apache.thrift.protocol.TType\n  static final byte TYPE_STOP = 0;\n  static final byte TYPE_BOOL = 2;\n  static final byte TYPE_BYTE = 3;\n  static final byte TYPE_DOUBLE = 4;\n  static final byte TYPE_I16 = 6;\n  static final byte TYPE_I32 = 8;\n  static final byte TYPE_I64 = 10;\n  static final byte TYPE_STRING = 11;\n  static final byte TYPE_STRUCT = 12;\n  static final byte TYPE_MAP = 13;\n  static final byte TYPE_SET = 14;\n  static final byte TYPE_LIST = 15;\n\n  final byte type;\n  final int id;\n\n  ThriftField(byte type, int id) {\n    this.type = type;\n    this.id = id;\n  }\n\n  void write(WriteBuffer buffer) {\n    buffer.writeByte(type);\n    // Write ID as a short!\n    buffer.writeByte((id >>> 8L) & 0xff);\n    buffer.writeByte(id & 0xff);\n  }\n\n  static ThriftField read(ReadBuffer bytes) {\n    byte type = bytes.readByte();\n    return new ThriftField(type, type == TYPE_STOP ? TYPE_STOP : bytes.readShort());\n  }\n\n  boolean isEqualTo(ThriftField that) {\n    return this.type == that.type && this.id == that.id;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/Trace.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\npublic class Trace {\n  /*\n   * Spans can be sent in multiple parts. Also client and server spans can share the same ID. This\n   * merges both scenarios.\n   */\n  public static List<Span> merge(List<Span> spans) {\n    int length = spans.size();\n    if (length <= 1) return spans;\n    List<Span> result = new ArrayList<>(spans);\n    Collections.sort(result, CLEANUP_COMPARATOR);\n\n    // Let's cleanup any spans and pick the longest ID\n    String traceId = result.get(0).traceId();\n    for (int i = 1; i < length; i++) {\n      String nextTraceId = result.get(i).traceId();\n      if (traceId.length() != 32) traceId = nextTraceId;\n    }\n\n    // Now start any fixes or merging\n    Span last = null;\n    for (int i = 0; i < length; i++) {\n      Span span = result.get(i);\n      //String previousId = last.id();\n      boolean spanShared = Boolean.TRUE.equals(span.shared());\n\n      // Choose the longest trace ID\n      Span.Builder replacement = null;\n      if (span.traceId().length() != traceId.length()) {\n        replacement = span.toBuilder().traceId(traceId);\n      }\n\n      EndpointTracker localEndpoint = null;\n      while (i + 1 < length) {\n        Span next = result.get(i + 1);\n        String nextId = next.id();\n        if (!nextId.equals(span.id())) break;\n\n        if (localEndpoint == null) {\n          localEndpoint = new EndpointTracker();\n          localEndpoint.tryMerge(span.localEndpoint());\n        }\n\n        // This cautiously merges with the next span, if we think it was sent in multiple pieces.\n        boolean nextShared = Boolean.TRUE.equals(next.shared());\n        if (spanShared == nextShared && localEndpoint.tryMerge(next.localEndpoint())) {\n          if (replacement == null) replacement = span.toBuilder();\n          replacement.merge(next);\n\n          // remove the merged element\n          length--;\n          result.remove(i + 1);\n          continue;\n        }\n        break;\n      }\n\n      // Zipkin and B3 originally used the same span ID between client and server. Some\n      // instrumentation are inconsistent about adding the shared flag on the server side. Since we\n      // have the entire trace, and it is ordered client-first, we can correct a missing shared flag.\n      if (last != null && last.id().equals(span.id())) {\n        // Backfill missing shared flag as some instrumentation doesn't add it\n        if (last.kind() == Span.Kind.CLIENT && span.kind() == Span.Kind.SERVER && !spanShared) {\n          spanShared = true;\n          if (replacement == null) replacement = span.toBuilder();\n          replacement.shared(true);\n        }\n\n        if (spanShared && span.parentId() == null && last.parentId() != null) {\n          // handle a shared RPC server span that wasn't propagated its parent span ID\n          if (replacement == null) replacement = span.toBuilder();\n          replacement.parentId(last.parentId());\n        }\n      }\n\n      if (replacement != null) {\n        span = replacement.build();\n        result.set(i, span);\n      }\n      last = span;\n    }\n\n    return result;\n  }\n\n  static final Comparator<Span> CLEANUP_COMPARATOR = (left, right) -> {\n    if (left.equals(right)) return 0;\n    int bySpanId = left.id().compareTo(right.id());\n    if (bySpanId != 0) return bySpanId;\n    int byShared = compareShared(left, right);\n    if (byShared != 0) return byShared;\n    return compareEndpoint(left.localEndpoint(), right.localEndpoint());\n  };\n\n  // false or null first (client first)\n  static int compareShared(Span left, Span right) {\n    // If either are shared put it last\n    boolean leftShared = Boolean.TRUE.equals(left.shared());\n    boolean rightShared = Boolean.TRUE.equals(right.shared());\n    if (leftShared && rightShared) return 0; // both are shared, so skip out\n    if (leftShared) return 1;\n    if (rightShared) return -1;\n\n    // neither are shared, put the client spans first\n    boolean leftClient = Span.Kind.CLIENT.equals(left.kind());\n    boolean rightClient = Span.Kind.CLIENT.equals(right.kind());\n    if (leftClient && rightClient) return 0;\n    if (leftClient) return -1;\n    if (rightClient) return 1;\n    return 0; // neither are client spans\n  }\n\n  /**\n   * Put spans with null endpoints first, so that their data can be attached to the first span with\n   * the same ID and endpoint. It is possible that a server can get the same request on a different\n   * port. Not addressing this.\n   */\n  static int compareEndpoint(Endpoint left, Endpoint right) {\n    if (left == null) { // nulls first\n      return (right == null) ? 0 : -1;\n    } else if (right == null) {\n      return 1;\n    }\n    int byService = nullSafeCompareTo(left.serviceName(), right.serviceName());\n    if (byService != 0) return byService;\n    int byIpV4 = nullSafeCompareTo(left.ipv4(), right.ipv4());\n    if (byIpV4 != 0) return byIpV4;\n    return nullSafeCompareTo(left.ipv6(), right.ipv6());\n  }\n\n  static <T extends Comparable<T>> int nullSafeCompareTo(T left, T right) {\n    if (left == null) {\n      return (right == null) ? 0 : 1;\n    } else if (right == null) {\n      return -1;\n    } else {\n      return left.compareTo(right);\n    }\n  }\n\n  /**\n   * Sometimes endpoints can be sent in pieces. This tracks the whether we should merge with\n   * something, or if it has a different identity.\n   */\n  static final class EndpointTracker {\n    String serviceName, ipv4, ipv6;\n    int port;\n\n    boolean tryMerge(Endpoint endpoint) {\n      if (endpoint == null) return true;\n      if (serviceName != null &&\n        endpoint.serviceName() != null && !serviceName.equals(endpoint.serviceName())) {\n        return false;\n      }\n      if (ipv4 != null && endpoint.ipv4() != null && !ipv4.equals(endpoint.ipv4())) {\n        return false;\n      }\n      if (ipv6 != null && endpoint.ipv6() != null && !ipv6.equals(endpoint.ipv6())) {\n        return false;\n      }\n      if (port != 0 && endpoint.portAsInt() != 0 && port != endpoint.portAsInt()) {\n        return false;\n      }\n      if (serviceName == null) serviceName = endpoint.serviceName();\n      if (ipv4 == null) ipv4 = endpoint.ipv4();\n      if (ipv6 == null) ipv6 = endpoint.ipv6();\n      if (port == 0) port = endpoint.portAsInt();\n      return true;\n    }\n  }\n\n  Trace() {\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/TracesAdapter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.Traces;\n\npublic final class TracesAdapter implements Traces {\n  final SpanStore delegate;\n\n  public TracesAdapter(SpanStore spanStore) {\n    this.delegate = spanStore;\n  }\n\n  @Override public Call<List<Span>> getTrace(String traceId) {\n    return delegate.getTrace(traceId);\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n    if (traceIds == null) throw new NullPointerException(\"traceIds == null\");\n\n    List<Call<List<Span>>> calls = new ArrayList<>();\n    for (String traceId : traceIds) {\n      calls.add(getTrace(Span.normalizeTraceId(traceId)));\n    }\n\n    if (calls.isEmpty()) return Call.emptyList();\n    if (calls.size() == 1) return calls.get(0).map(ToListOfTraces.INSTANCE);\n    return new ScatterGather(calls);\n  }\n\n  enum ToListOfTraces implements Call.Mapper<List<Span>, List<List<Span>>> {\n    INSTANCE;\n\n    @Override public List<List<Span>> map(List<Span> input) {\n      return input.isEmpty() ? Collections.<List<Span>>emptyList()\n        : Collections.singletonList(input);\n    }\n\n    @Override public String toString() {\n      return \"ToListOfTraces()\";\n    }\n  }\n\n  static final class ScatterGather extends AggregateCall<List<Span>, List<List<Span>>> {\n    ScatterGather(List<Call<List<Span>>> calls) {\n      super(calls);\n    }\n\n    @Override protected List<List<Span>> newOutput() {\n      return new ArrayList<>();\n    }\n\n    @Override protected void append(List<Span> input, List<List<Span>> output) {\n      if (!input.isEmpty()) output.add(input);\n    }\n\n    @Override public ScatterGather clone() {\n      return new ScatterGather(cloneCalls());\n    }\n  }\n\n  @Override public String toString() {\n    return \"TracesAdapter{\" + delegate + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V1JsonSpanReader.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.JsonCodec.JsonReader;\nimport zipkin2.internal.JsonCodec.JsonReaderAdapter;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\nimport static zipkin2.internal.JsonCodec.exceptionReading;\nimport static zipkin2.internal.V2SpanReader.ENDPOINT_READER;\n\npublic final class V1JsonSpanReader implements JsonReaderAdapter<V1Span> {\n\n  V1Span.Builder builder;\n\n  public boolean readList(ReadBuffer buffer, Collection<Span> out) {\n    if (buffer.available() == 0) return false;\n    V1SpanConverter converter = V1SpanConverter.create();\n    JsonReader reader = new JsonReader(buffer);\n    try {\n      reader.beginArray();\n      if (!reader.hasNext()) return false;\n      while (reader.hasNext()) {\n        V1Span result = fromJson(reader);\n        converter.convert(result, out);\n      }\n      reader.endArray();\n      return true;\n    } catch (Exception e) {\n      throw exceptionReading(\"List<Span>\", e);\n    }\n  }\n\n  @Override public V1Span fromJson(JsonReader reader) throws IOException {\n    if (builder == null) {\n      builder = V1Span.newBuilder();\n    } else {\n      builder.clear();\n    }\n    reader.beginObject();\n    while (reader.hasNext()) {\n      String nextName = reader.nextName();\n      if (nextName.equals(\"traceId\")) {\n        builder.traceId(reader.nextString());\n        continue;\n      } else if (nextName.equals(\"id\")) {\n        builder.id(reader.nextString());\n        continue;\n      } else if (reader.peekNull()) {\n        reader.skipValue();\n        continue;\n      }\n\n      // read any optional fields\n      if (nextName.equals(\"name\")) {\n        builder.name(reader.nextString());\n      } else if (nextName.equals(\"parentId\")) {\n        builder.parentId(reader.nextString());\n      } else if (nextName.equals(\"timestamp\")) {\n        builder.timestamp(reader.nextLong());\n      } else if (nextName.equals(\"duration\")) {\n        builder.duration(reader.nextLong());\n      } else if (nextName.equals(\"annotations\")) {\n        reader.beginArray();\n        while (reader.hasNext()) readAnnotation(reader);\n        reader.endArray();\n      } else if (nextName.equals(\"binaryAnnotations\")) {\n        reader.beginArray();\n        while (reader.hasNext()) readBinaryAnnotation(reader);\n        reader.endArray();\n      } else if (nextName.equals(\"debug\")) {\n        if (reader.nextBoolean()) builder.debug(true);\n      } else {\n        reader.skipValue();\n      }\n    }\n    reader.endObject();\n    return builder.build();\n  }\n\n  void readAnnotation(JsonReader reader) throws IOException {\n    String nextName;\n    reader.beginObject();\n    Long timestamp = null;\n    String value = null;\n    Endpoint endpoint = null;\n    while (reader.hasNext()) {\n      nextName = reader.nextName();\n      if (nextName.equals(\"timestamp\")) {\n        timestamp = reader.nextLong();\n      } else if (nextName.equals(\"value\")) {\n        value = reader.nextString();\n      } else if (nextName.equals(\"endpoint\") && !reader.peekNull()) {\n        endpoint = ENDPOINT_READER.fromJson(reader);\n      } else {\n        reader.skipValue();\n      }\n    }\n    if (timestamp == null || value == null) {\n      throw new IllegalArgumentException(\"Incomplete annotation at \" + reader.getPath());\n    }\n    reader.endObject();\n    builder.addAnnotation(timestamp, value, endpoint);\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  void readBinaryAnnotation(JsonReader reader) throws IOException {\n    String key = null;\n    Endpoint endpoint = null;\n    Boolean booleanValue = null;\n    String stringValue = null;\n\n    reader.beginObject();\n    while (reader.hasNext()) {\n      String nextName = reader.nextName();\n      if (reader.peekNull()) {\n        reader.skipValue();\n        continue;\n      }\n\n      if (nextName.equals(\"key\")) {\n        key = reader.nextString();\n      } else if (nextName.equals(\"value\")) {\n        if (reader.peekString()) {\n          stringValue = reader.nextString();\n        } else if (reader.peekBoolean()) {\n          booleanValue = reader.nextBoolean();\n        } else {\n          reader.skipValue();\n        }\n      } else if (nextName.equals(\"endpoint\")) {\n        endpoint = ENDPOINT_READER.fromJson(reader);\n      } else {\n        reader.skipValue();\n      }\n    }\n\n    if (key == null) {\n      throw new IllegalArgumentException(\"No key at \" + reader.getPath());\n    }\n    reader.endObject();\n\n    if (stringValue != null) {\n      builder.addBinaryAnnotation(key, stringValue, endpoint);\n    } else if (booleanValue != null && booleanValue && endpoint != null) {\n      if (key.equals(\"sa\") || key.equals(\"ca\") || key.equals(\"ma\")) {\n        builder.addBinaryAnnotation(key, endpoint);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V1JsonSpanWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport zipkin2.Span;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V2SpanConverter;\n\n/** This type isn't thread-safe: it re-uses state to avoid re-allocations in conversion loops. */\n// @Immutable\npublic final class V1JsonSpanWriter implements WriteBuffer.Writer<Span> {\n  final V2SpanConverter converter = V2SpanConverter.create();\n  final V1SpanWriter v1SpanWriter = new V1SpanWriter();\n\n  @Override public int sizeInBytes(Span value) {\n    V1Span v1Span = converter.convert(value);\n    return v1SpanWriter.sizeInBytes(v1Span);\n  }\n\n  @Override public void write(Span value, WriteBuffer b) {\n    V1Span v1Span = converter.convert(value);\n    v1SpanWriter.write(v1Span, b);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V1SpanWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport zipkin2.Endpoint;\nimport zipkin2.v1.V1Annotation;\nimport zipkin2.v1.V1BinaryAnnotation;\nimport zipkin2.v1.V1Span;\n\nimport static zipkin2.internal.JsonEscaper.jsonEscape;\nimport static zipkin2.internal.JsonEscaper.jsonEscapedSizeInBytes;\nimport static zipkin2.internal.V2SpanWriter.endpointSizeInBytes;\nimport static zipkin2.internal.V2SpanWriter.writeAnnotation;\nimport static zipkin2.internal.WriteBuffer.asciiSizeInBytes;\n\n/** This type is only used to backport the v1 read api as it returns v1 json. */\n// @Immutable\npublic final class V1SpanWriter implements WriteBuffer.Writer<V1Span> {\n\n  @Override public int sizeInBytes(V1Span value) {\n    int sizeInBytes = 29; // {\"traceId\":\"xxxxxxxxxxxxxxxx\"\n    if (value.traceIdHigh() != 0L) sizeInBytes += 16;\n    if (value.parentId() != 0L) {\n      sizeInBytes += 30; // ,\"parentId\":\"0123456789abcdef\"\n    }\n    sizeInBytes += 24; // ,\"id\":\"0123456789abcdef\"\n    sizeInBytes += 10; // ,\"name\":\"\"\n    if (value.name() != null) {\n      sizeInBytes += jsonEscapedSizeInBytes(value.name());\n    }\n    if (value.timestamp() != 0L) {\n      sizeInBytes += 13; // ,\"timestamp\":\n      sizeInBytes += asciiSizeInBytes(value.timestamp());\n    }\n    if (value.duration() != 0L) {\n      sizeInBytes += 12; // ,\"duration\":\n      sizeInBytes += asciiSizeInBytes(value.duration());\n    }\n\n    int annotationCount = value.annotations().size();\n    Endpoint lastEndpoint = null;\n    int lastEndpointSize = 0;\n    if (annotationCount > 0) {\n      sizeInBytes += 17; // ,\"annotations\":[]\n      if (annotationCount > 1) sizeInBytes += annotationCount - 1; // comma to join elements\n      for (int i = 0; i < annotationCount; i++) {\n        V1Annotation a = value.annotations().get(i);\n        Endpoint endpoint = a.endpoint();\n        int endpointSize;\n        if (endpoint == null) {\n          endpointSize = 0;\n        } else if (endpoint.equals(lastEndpoint)) {\n          endpointSize = lastEndpointSize;\n        } else {\n          lastEndpoint = endpoint;\n          endpointSize = lastEndpointSize = endpointSizeInBytes(endpoint, true);\n        }\n        sizeInBytes += V2SpanWriter.annotationSizeInBytes(a.timestamp(), a.value(), endpointSize);\n      }\n    }\n\n    int binaryAnnotationCount = value.binaryAnnotations().size();\n    if (binaryAnnotationCount > 0) {\n      sizeInBytes += 23; // ,\"binaryAnnotations\":[]\n      if (binaryAnnotationCount > 1) sizeInBytes += binaryAnnotationCount - 1; // commas\n      for (int i = 0; i < binaryAnnotationCount; ) {\n        V1BinaryAnnotation a = value.binaryAnnotations().get(i++);\n        Endpoint endpoint = a.endpoint();\n        int endpointSize;\n        if (endpoint == null) {\n          endpointSize = 0;\n        } else if (endpoint.equals(lastEndpoint)) {\n          endpointSize = lastEndpointSize;\n        } else {\n          lastEndpoint = endpoint;\n          endpointSize = lastEndpointSize = endpointSizeInBytes(endpoint, true);\n        }\n        if (a.stringValue() != null) {\n          sizeInBytes += binaryAnnotationSizeInBytes(a.key(), a.stringValue(), endpointSize);\n        } else {\n          sizeInBytes += 37; // {\"key\":\"NN\",\"value\":true,\"endpoint\":}\n          sizeInBytes += endpointSize;\n        }\n      }\n    }\n\n    if (Boolean.TRUE.equals(value.debug())) sizeInBytes += 13; // ,\"debug\":true\n    return ++sizeInBytes; // }\n  }\n\n  @Override public void write(V1Span value, WriteBuffer b) {\n    b.writeAscii(\"{\\\"traceId\\\":\\\"\");\n    if (value.traceIdHigh() != 0L) b.writeLongHex(value.traceIdHigh());\n    b.writeLongHex(value.traceId());\n    b.writeByte('\"');\n    if (value.parentId() != 0L) {\n      b.writeAscii(\",\\\"parentId\\\":\\\"\");\n      b.writeLongHex(value.parentId());\n      b.writeByte('\"');\n    }\n    b.writeAscii(\",\\\"id\\\":\\\"\");\n    b.writeLongHex(value.id());\n    b.writeByte('\"');\n    b.writeAscii(\",\\\"name\\\":\\\"\");\n    if (value.name() != null) b.writeUtf8(jsonEscape(value.name()));\n    b.writeByte('\"');\n\n    if (value.timestamp() != 0L) {\n      b.writeAscii(\",\\\"timestamp\\\":\");\n      b.writeAscii(value.timestamp());\n    }\n    if (value.duration() != 0L) {\n      b.writeAscii(\",\\\"duration\\\":\");\n      b.writeAscii(value.duration());\n    }\n\n    int annotationCount = value.annotations().size();\n    Endpoint lastEndpoint = null;\n    byte[] lastEndpointBytes = null;\n    if (annotationCount > 0) {\n      b.writeAscii(\",\\\"annotations\\\":[\");\n      for (int i = 0; i < annotationCount; ) {\n        V1Annotation a = value.annotations().get(i++);\n        Endpoint endpoint = a.endpoint();\n        byte[] endpointBytes;\n        if (endpoint == null) {\n          endpointBytes = null;\n        } else if (endpoint.equals(lastEndpoint)) {\n          endpointBytes = lastEndpointBytes;\n        } else {\n          lastEndpoint = endpoint;\n          endpointBytes = lastEndpointBytes = legacyEndpointBytes(endpoint);\n        }\n        writeAnnotation(a.timestamp(), a.value(), endpointBytes, b);\n        if (i < annotationCount) b.writeByte(',');\n      }\n      b.writeByte(']');\n    }\n    int binaryAnnotationCount = value.binaryAnnotations().size();\n    if (binaryAnnotationCount > 0) {\n      b.writeAscii(\",\\\"binaryAnnotations\\\":[\");\n      for (int i = 0; i < binaryAnnotationCount; ) {\n        V1BinaryAnnotation a = value.binaryAnnotations().get(i++);\n        Endpoint endpoint = a.endpoint();\n        byte[] endpointBytes;\n        if (endpoint == null) {\n          endpointBytes = null;\n        } else if (endpoint.equals(lastEndpoint)) {\n          endpointBytes = lastEndpointBytes;\n        } else {\n          lastEndpoint = endpoint;\n          endpointBytes = lastEndpointBytes = legacyEndpointBytes(endpoint);\n        }\n        if (a.stringValue() != null) {\n          writeBinaryAnnotation(a.key(), a.stringValue(), endpointBytes, b);\n        } else {\n          b.writeAscii(\"{\\\"key\\\":\\\"\");\n          b.writeAscii(a.key());\n          b.writeAscii(\"\\\",\\\"value\\\":true,\\\"endpoint\\\":\");\n          b.write(endpointBytes);\n          b.writeByte('}');\n        }\n        if (i < binaryAnnotationCount) b.writeByte(',');\n      }\n      b.writeByte(']');\n    }\n    if (Boolean.TRUE.equals(value.debug())) {\n      b.writeAscii(\",\\\"debug\\\":true\");\n    }\n    b.writeByte('}');\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  static byte[] legacyEndpointBytes(@Nullable Endpoint localEndpoint) {\n    if (localEndpoint == null) return null;\n    byte[] result = new byte[endpointSizeInBytes(localEndpoint, true)];\n    V2SpanWriter.writeEndpoint(localEndpoint, WriteBuffer.wrap(result), true);\n    return result;\n  }\n\n  static int binaryAnnotationSizeInBytes(String key, String value, int endpointSize) {\n    int sizeInBytes = 21; // {\"key\":\"\",\"value\":\"\"}\n    sizeInBytes += jsonEscapedSizeInBytes(key);\n    sizeInBytes += jsonEscapedSizeInBytes(value);\n    if (endpointSize != 0) {\n      sizeInBytes += 12; // ,\"endpoint\":\n      sizeInBytes += endpointSize;\n    }\n    return sizeInBytes;\n  }\n\n  static void writeBinaryAnnotation(String key, String value, @Nullable byte[] endpoint,\n    WriteBuffer b) {\n    b.writeAscii(\"{\\\"key\\\":\\\"\");\n    b.writeUtf8(jsonEscape(key));\n    b.writeAscii(\"\\\",\\\"value\\\":\\\"\");\n    b.writeUtf8(jsonEscape(value));\n    b.writeByte('\"');\n    if (endpoint != null) {\n      b.writeAscii(\",\\\"endpoint\\\":\");\n      b.write(endpoint);\n    }\n    b.writeAscii(\"}\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V1ThriftSpanReader.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport zipkin2.Endpoint;\nimport zipkin2.v1.V1Span;\n\nimport static zipkin2.internal.ThriftCodec.readListLength;\nimport static zipkin2.internal.ThriftCodec.skip;\nimport static zipkin2.internal.ThriftField.TYPE_I32;\nimport static zipkin2.internal.ThriftField.TYPE_I64;\nimport static zipkin2.internal.ThriftField.TYPE_STOP;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.ThriftField.TYPE_STRUCT;\nimport static zipkin2.internal.V1ThriftSpanWriter.ANNOTATIONS;\nimport static zipkin2.internal.V1ThriftSpanWriter.BINARY_ANNOTATIONS;\nimport static zipkin2.internal.V1ThriftSpanWriter.DEBUG;\nimport static zipkin2.internal.V1ThriftSpanWriter.DURATION;\nimport static zipkin2.internal.V1ThriftSpanWriter.ID;\nimport static zipkin2.internal.V1ThriftSpanWriter.NAME;\nimport static zipkin2.internal.V1ThriftSpanWriter.PARENT_ID;\nimport static zipkin2.internal.V1ThriftSpanWriter.TIMESTAMP;\nimport static zipkin2.internal.V1ThriftSpanWriter.TRACE_ID;\nimport static zipkin2.internal.V1ThriftSpanWriter.TRACE_ID_HIGH;\n\npublic final class V1ThriftSpanReader {\n  static final String ONE = Character.toString((char) 1);\n\n  public static V1ThriftSpanReader create() {\n    return new V1ThriftSpanReader();\n  }\n\n  V1Span.Builder builder = V1Span.newBuilder();\n\n  public V1Span read(ReadBuffer buffer) {\n    if (builder == null) {\n      builder = V1Span.newBuilder();\n    } else {\n      builder.clear();\n    }\n\n    ThriftField thriftField;\n\n    while (true) {\n      thriftField = ThriftField.read(buffer);\n      if (thriftField.type == TYPE_STOP) break;\n\n      if (thriftField.isEqualTo(TRACE_ID_HIGH)) {\n        builder.traceIdHigh(buffer.readLong());\n      } else if (thriftField.isEqualTo(TRACE_ID)) {\n        builder.traceId(buffer.readLong());\n      } else if (thriftField.isEqualTo(NAME)) {\n        builder.name(buffer.readUtf8(buffer.readInt()));\n      } else if (thriftField.isEqualTo(ID)) {\n        builder.id(buffer.readLong());\n      } else if (thriftField.isEqualTo(PARENT_ID)) {\n        builder.parentId(buffer.readLong());\n      } else if (thriftField.isEqualTo(ANNOTATIONS)) {\n        int length = readListLength(buffer);\n        for (int i = 0; i < length; i++) {\n          AnnotationReader.read(buffer, builder);\n        }\n      } else if (thriftField.isEqualTo(BINARY_ANNOTATIONS)) {\n        int length = readListLength(buffer);\n        for (int i = 0; i < length; i++) {\n          BinaryAnnotationReader.read(buffer, builder);\n        }\n      } else if (thriftField.isEqualTo(DEBUG)) {\n        builder.debug(buffer.readByte() == 1);\n      } else if (thriftField.isEqualTo(TIMESTAMP)) {\n        builder.timestamp(buffer.readLong());\n      } else if (thriftField.isEqualTo(DURATION)) {\n        builder.duration(buffer.readLong());\n      } else {\n        skip(buffer, thriftField.type);\n      }\n    }\n\n    return builder.build();\n  }\n\n  static final class AnnotationReader {\n    static final ThriftField TIMESTAMP = new ThriftField(TYPE_I64, 1);\n    static final ThriftField VALUE = new ThriftField(TYPE_STRING, 2);\n    static final ThriftField ENDPOINT = new ThriftField(TYPE_STRUCT, 3);\n\n    static void read(ReadBuffer buffer, V1Span.Builder builder) {\n      long timestamp = 0;\n      String value = null;\n      Endpoint endpoint = null;\n\n      ThriftField thriftField;\n      while (true) {\n        thriftField = ThriftField.read(buffer);\n        if (thriftField.type == TYPE_STOP) break;\n\n        if (thriftField.isEqualTo(TIMESTAMP)) {\n          timestamp = buffer.readLong();\n        } else if (thriftField.isEqualTo(VALUE)) {\n          value = buffer.readUtf8(buffer.readInt());\n        } else if (thriftField.isEqualTo(ENDPOINT)) {\n          endpoint = ThriftEndpointCodec.read(buffer);\n        } else {\n          skip(buffer, thriftField.type);\n        }\n      }\n\n      if (timestamp == 0 || value == null) return;\n      builder.addAnnotation(timestamp, value, endpoint);\n    }\n  }\n\n  static final class BinaryAnnotationReader {\n    static final ThriftField KEY = new ThriftField(TYPE_STRING, 1);\n    static final ThriftField VALUE = new ThriftField(TYPE_STRING, 2);\n    static final ThriftField TYPE = new ThriftField(TYPE_I32, 3);\n    static final ThriftField ENDPOINT = new ThriftField(TYPE_STRUCT, 4);\n\n    static void read(ReadBuffer buffer, V1Span.Builder builder) {\n      String key = null;\n      String value = null;\n      Endpoint endpoint = null;\n      boolean isBoolean = false;\n      boolean isString = false;\n\n      while (true) {\n        ThriftField thriftField = ThriftField.read(buffer);\n        if (thriftField.type == TYPE_STOP) break;\n        if (thriftField.isEqualTo(KEY)) {\n          key = buffer.readUtf8(buffer.readInt());\n        } else if (thriftField.isEqualTo(VALUE)) {\n          value = buffer.readUtf8(buffer.readInt());\n        } else if (thriftField.isEqualTo(TYPE)) {\n          switch (buffer.readInt()) {\n            case 0:\n              isBoolean = true;\n              break;\n            case 6:\n              isString = true;\n              break;\n          }\n        } else if (thriftField.isEqualTo(ENDPOINT)) {\n          endpoint = ThriftEndpointCodec.read(buffer);\n        } else {\n          skip(buffer, thriftField.type);\n        }\n      }\n      if (key == null || value == null) return;\n      if (isString) {\n        builder.addBinaryAnnotation(key, value, endpoint);\n      } else if (isBoolean && ONE.equals(value) && endpoint != null) {\n        if (key.equals(\"sa\") || key.equals(\"ca\") || key.equals(\"ma\")) {\n          builder.addBinaryAnnotation(key, endpoint);\n        }\n      }\n    }\n  }\n\n  V1ThriftSpanReader() {\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V1ThriftSpanWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.v1.V1Annotation;\nimport zipkin2.v1.V1BinaryAnnotation;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V2SpanConverter;\n\nimport static zipkin2.internal.ThriftField.TYPE_BOOL;\nimport static zipkin2.internal.ThriftField.TYPE_I32;\nimport static zipkin2.internal.ThriftField.TYPE_I64;\nimport static zipkin2.internal.ThriftField.TYPE_LIST;\nimport static zipkin2.internal.ThriftField.TYPE_STOP;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.ThriftField.TYPE_STRUCT;\nimport static zipkin2.internal.WriteBuffer.utf8SizeInBytes;\n\n/** This type isn't thread-safe: it re-uses state to avoid re-allocations in conversion loops. */\n// @Immutable\npublic final class V1ThriftSpanWriter implements WriteBuffer.Writer<Span> {\n  static final ThriftField TRACE_ID = new ThriftField(TYPE_I64, 1);\n  static final ThriftField TRACE_ID_HIGH = new ThriftField(TYPE_I64, 12);\n  static final ThriftField NAME = new ThriftField(TYPE_STRING, 3);\n  static final ThriftField ID = new ThriftField(TYPE_I64, 4);\n  static final ThriftField PARENT_ID = new ThriftField(TYPE_I64, 5);\n  static final ThriftField ANNOTATIONS = new ThriftField(TYPE_LIST, 6);\n  static final ThriftField BINARY_ANNOTATIONS = new ThriftField(TYPE_LIST, 8);\n  static final ThriftField DEBUG = new ThriftField(TYPE_BOOL, 9);\n  static final ThriftField TIMESTAMP = new ThriftField(TYPE_I64, 10);\n  static final ThriftField DURATION = new ThriftField(TYPE_I64, 11);\n\n  static final byte[] EMPTY_ARRAY = new byte[0];\n\n  final V2SpanConverter converter = V2SpanConverter.create();\n\n  @Override public int sizeInBytes(Span value) {\n    V1Span v1Span = converter.convert(value);\n\n    int endpointSize =\n        value.localEndpoint() != null ? ThriftEndpointCodec.sizeInBytes(value.localEndpoint()) : 0;\n\n    int sizeInBytes = 3 + 8; // TRACE_ID\n    if (v1Span.traceIdHigh() != 0) sizeInBytes += 3 + 8; // TRACE_ID_HIGH\n    if (v1Span.parentId() != 0) sizeInBytes += 3 + 8; // PARENT_ID\n    sizeInBytes += 3 + 8; // ID\n    sizeInBytes += 3 + 4; // NAME\n    if (value.name() != null) sizeInBytes += utf8SizeInBytes(value.name());\n\n    // we write list thriftFields even when empty to match finagle serialization\n    sizeInBytes += 3 + 5; // ANNOTATION field + list overhead\n    for (int i = 0, length = v1Span.annotations().size(); i < length; i++) {\n      int valueSize = utf8SizeInBytes(v1Span.annotations().get(i).value());\n      sizeInBytes += ThriftAnnotationWriter.sizeInBytes(valueSize, endpointSize);\n    }\n\n    sizeInBytes += 3 + 5; // BINARY_ANNOTATION field + list overhead\n    for (int i = 0, length = v1Span.binaryAnnotations().size(); i < length; i++) {\n      V1BinaryAnnotation b = v1Span.binaryAnnotations().get(i);\n      int keySize = utf8SizeInBytes(b.key());\n      if (b.stringValue() != null) {\n        int valueSize = utf8SizeInBytes(b.stringValue());\n        sizeInBytes += ThriftBinaryAnnotationWriter.sizeInBytes(keySize, valueSize, endpointSize);\n      } else {\n        int remoteEndpointSize = ThriftEndpointCodec.sizeInBytes(b.endpoint());\n        sizeInBytes += ThriftBinaryAnnotationWriter.sizeInBytes(keySize, 1, remoteEndpointSize);\n      }\n    }\n\n    if (v1Span.debug() != null) sizeInBytes += 3 + 1; // DEBUG\n    if (v1Span.timestamp() != 0L) sizeInBytes += 3 + 8; // TIMESTAMP\n    if (v1Span.duration() != 0L) sizeInBytes += 3 + 8; // DURATION\n\n    sizeInBytes++; // TYPE_STOP\n    return sizeInBytes;\n  }\n\n  @Override public void write(Span value, WriteBuffer buffer) {\n    V1Span v1Span = converter.convert(value);\n    byte[] endpointBytes = legacyEndpointBytes(value.localEndpoint());\n\n    TRACE_ID.write(buffer);\n    ThriftCodec.writeLong(buffer, v1Span.traceId());\n\n    NAME.write(buffer);\n    ThriftCodec.writeLengthPrefixed(buffer, value.name() != null ? value.name() : \"\");\n\n    ID.write(buffer);\n    ThriftCodec.writeLong(buffer, v1Span.id());\n\n    if (v1Span.parentId() != 0L) {\n      PARENT_ID.write(buffer);\n      ThriftCodec.writeLong(buffer, v1Span.parentId());\n    }\n\n    // we write list thriftFields even when empty to match finagle serialization\n    ANNOTATIONS.write(buffer);\n    writeAnnotations(buffer, v1Span, endpointBytes);\n\n    BINARY_ANNOTATIONS.write(buffer);\n    writeBinaryAnnotations(buffer, v1Span, endpointBytes);\n\n    if (v1Span.debug() != null) {\n      DEBUG.write(buffer);\n      buffer.writeByte(v1Span.debug() ? 1 : 0);\n    }\n\n    if (v1Span.timestamp() != 0L) {\n      TIMESTAMP.write(buffer);\n      ThriftCodec.writeLong(buffer, v1Span.timestamp());\n    }\n    if (v1Span.duration() != 0L) {\n      DURATION.write(buffer);\n      ThriftCodec.writeLong(buffer, v1Span.duration());\n    }\n\n    if (v1Span.traceIdHigh() != 0L) {\n      TRACE_ID_HIGH.write(buffer);\n      ThriftCodec.writeLong(buffer, v1Span.traceIdHigh());\n    }\n\n    buffer.writeByte(TYPE_STOP);\n  }\n\n  static void writeAnnotations(WriteBuffer buffer, V1Span v1Span, byte[] endpointBytes) {\n    int annotationCount = v1Span.annotations().size();\n    ThriftCodec.writeListBegin(buffer, annotationCount);\n    for (int i = 0; i < annotationCount; i++) {\n      V1Annotation a = v1Span.annotations().get(i);\n      ThriftAnnotationWriter.write(a.timestamp(), a.value(), endpointBytes, buffer);\n    }\n  }\n\n  static void writeBinaryAnnotations(WriteBuffer buffer, V1Span v1Span, byte[] endpointBytes) {\n    int binaryAnnotationCount = v1Span.binaryAnnotations().size();\n    ThriftCodec.writeListBegin(buffer, binaryAnnotationCount);\n    for (int i = 0; i < binaryAnnotationCount; i++) {\n      V1BinaryAnnotation a = v1Span.binaryAnnotations().get(i);\n      byte[] ep = a.stringValue() != null ? endpointBytes : legacyEndpointBytes(a.endpoint());\n      ThriftBinaryAnnotationWriter.write(a.key(), a.stringValue(), ep, buffer);\n    }\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  public byte[] writeList(List<Span> spans) {\n    int lengthOfSpans = spans.size();\n    if (lengthOfSpans == 0) return EMPTY_ARRAY;\n\n    byte[] result = new byte[ThriftCodec.listSizeInBytes(this, spans)];\n    ThriftCodec.writeList(this, spans, WriteBuffer.wrap(result));\n    return result;\n  }\n\n  public byte[] write(Span onlySpan) {\n    byte[] result = new byte[sizeInBytes(onlySpan)];\n    write(onlySpan, WriteBuffer.wrap(result));\n    return result;\n  }\n\n  public int writeList(List<Span> spans, byte[] out, int pos) {\n    int lengthOfSpans = spans.size();\n    if (lengthOfSpans == 0) return 0;\n\n    WriteBuffer result = WriteBuffer.wrap(out, pos);\n    ThriftCodec.writeList(this, spans, result);\n\n    return result.pos() - pos;\n  }\n\n  static byte[] legacyEndpointBytes(@Nullable Endpoint localEndpoint) {\n    if (localEndpoint == null) return null;\n    byte[] result = new byte[ThriftEndpointCodec.sizeInBytes(localEndpoint)];\n    ThriftEndpointCodec.write(localEndpoint, WriteBuffer.wrap(result));\n    return result;\n  }\n\n  static class ThriftAnnotationWriter {\n\n    static final ThriftField TIMESTAMP = new ThriftField(TYPE_I64, 1);\n    static final ThriftField VALUE = new ThriftField(TYPE_STRING, 2);\n    static final ThriftField ENDPOINT = new ThriftField(TYPE_STRUCT, 3);\n\n    static int sizeInBytes(int valueSizeInBytes, int endpointSizeInBytes) {\n      int sizeInBytes = 0;\n      sizeInBytes += 3 + 8; // TIMESTAMP\n      sizeInBytes += 3 + 4 + valueSizeInBytes;\n      if (endpointSizeInBytes > 0) sizeInBytes += 3 + endpointSizeInBytes;\n      sizeInBytes++; // TYPE_STOP\n      return sizeInBytes;\n    }\n\n    static void write(long timestamp, String value, byte[] endpointBytes, WriteBuffer buffer) {\n      TIMESTAMP.write(buffer);\n      ThriftCodec.writeLong(buffer, timestamp);\n\n      VALUE.write(buffer);\n      ThriftCodec.writeLengthPrefixed(buffer, value);\n\n      if (endpointBytes != null) {\n        ENDPOINT.write(buffer);\n        buffer.write(endpointBytes);\n      }\n      buffer.writeByte(TYPE_STOP);\n    }\n  }\n\n  static class ThriftBinaryAnnotationWriter {\n\n    static final ThriftField KEY = new ThriftField(TYPE_STRING, 1);\n    static final ThriftField VALUE = new ThriftField(TYPE_STRING, 2);\n    static final ThriftField TYPE = new ThriftField(TYPE_I32, 3);\n    static final ThriftField ENDPOINT = new ThriftField(TYPE_STRUCT, 4);\n\n    static int sizeInBytes(int keySize, int valueSize, int endpointSizeInBytes) {\n      int sizeInBytes = 0;\n      sizeInBytes += 3 + 4 + keySize;\n      sizeInBytes += 3 + 4 + valueSize;\n      sizeInBytes += 3 + 4; // TYPE\n      if (endpointSizeInBytes > 0) sizeInBytes += 3 + endpointSizeInBytes;\n      sizeInBytes++; // TYPE_STOP\n      return sizeInBytes;\n    }\n\n    static void write(String key, String stringValue, byte[] endpointBytes, WriteBuffer buffer) {\n      KEY.write(buffer);\n      ThriftCodec.writeLengthPrefixed(buffer, key);\n\n      VALUE.write(buffer);\n      int type = 0;\n      if (stringValue != null) {\n        type = 6;\n        ThriftCodec.writeInt(buffer, utf8SizeInBytes(stringValue));\n        buffer.writeUtf8(stringValue);\n      } else {\n        ThriftCodec.writeInt(buffer, 1);\n        buffer.writeByte(1);\n      }\n\n      TYPE.write(buffer);\n      ThriftCodec.writeInt(buffer, type);\n\n      if (endpointBytes != null) {\n        ENDPOINT.write(buffer);\n        buffer.write(endpointBytes);\n      }\n\n      buffer.writeByte(TYPE_STOP);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V2SpanReader.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.JsonCodec.JsonReader;\nimport zipkin2.internal.JsonCodec.JsonReaderAdapter;\n\npublic final class V2SpanReader implements JsonReaderAdapter<Span> {\n  Span.Builder builder;\n\n  @Override public Span fromJson(JsonReader reader) throws IOException {\n    if (builder == null) {\n      builder = Span.newBuilder();\n    } else {\n      builder.clear();\n    }\n    reader.beginObject();\n    while (reader.hasNext()) {\n      String nextName = reader.nextName();\n      if (nextName.equals(\"traceId\")) {\n        builder.traceId(reader.nextString());\n        continue;\n      } else if (nextName.equals(\"id\")) {\n        builder.id(reader.nextString());\n        continue;\n      } else if (reader.peekNull()) {\n        reader.skipValue();\n        continue;\n      }\n\n      // read any optional fields\n      if (nextName.equals(\"parentId\")) {\n        builder.parentId(reader.nextString());\n      } else if (nextName.equals(\"kind\")) {\n        builder.kind(Span.Kind.valueOf(reader.nextString()));\n      } else if (nextName.equals(\"name\")) {\n        builder.name(reader.nextString());\n      } else if (nextName.equals(\"timestamp\")) {\n        builder.timestamp(reader.nextLong());\n      } else if (nextName.equals(\"duration\")) {\n        builder.duration(reader.nextLong());\n      } else if (nextName.equals(\"localEndpoint\")) {\n        builder.localEndpoint(ENDPOINT_READER.fromJson(reader));\n      } else if (nextName.equals(\"remoteEndpoint\")) {\n        builder.remoteEndpoint(ENDPOINT_READER.fromJson(reader));\n      } else if (nextName.equals(\"annotations\")) {\n        reader.beginArray();\n        while (reader.hasNext()) {\n          reader.beginObject();\n          Long timestamp = null;\n          String value = null;\n          while (reader.hasNext()) {\n            nextName = reader.nextName();\n            if (nextName.equals(\"timestamp\")) {\n              timestamp = reader.nextLong();\n            } else if (nextName.equals(\"value\")) {\n              value = reader.nextString();\n            } else {\n              reader.skipValue();\n            }\n          }\n          if (timestamp == null || value == null) {\n            throw new IllegalArgumentException(\"Incomplete annotation at \" + reader.getPath());\n          }\n          reader.endObject();\n          builder.addAnnotation(timestamp, value);\n        }\n        reader.endArray();\n      } else if (nextName.equals(\"tags\")) {\n        reader.beginObject();\n        while (reader.hasNext()) {\n          String key = reader.nextName();\n          if (reader.peekNull()) {\n            throw new IllegalArgumentException(\"No value at \" + reader.getPath());\n          }\n          builder.putTag(key, reader.nextString());\n        }\n        reader.endObject();\n      } else if (nextName.equals(\"debug\")) {\n        if (reader.nextBoolean()) builder.debug(true);\n      } else if (nextName.equals(\"shared\")) {\n        if (reader.nextBoolean()) builder.shared(true);\n      } else {\n        reader.skipValue();\n      }\n    }\n    reader.endObject();\n    return builder.build();\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  static final JsonReaderAdapter<Endpoint> ENDPOINT_READER = new JsonReaderAdapter<Endpoint>() {\n    @Override public Endpoint fromJson(JsonReader reader) throws IOException {\n      Endpoint.Builder result = Endpoint.newBuilder();\n      reader.beginObject();\n      boolean readField = false;\n      while (reader.hasNext()) {\n        String nextName = reader.nextName();\n        if (reader.peekNull()) {\n          reader.skipValue();\n          continue;\n        }\n        if (nextName.equals(\"serviceName\")) {\n          result.serviceName(reader.nextString());\n          readField = true;\n        } else if (nextName.equals(\"ipv4\") || nextName.equals(\"ipv6\")) {\n          result.parseIp(reader.nextString());\n          readField = true;\n        } else if (nextName.equals(\"port\")) {\n          result.port(reader.nextInt());\n          readField = true;\n        } else {\n          reader.skipValue();\n        }\n      }\n      reader.endObject();\n      return readField ? result.build() : null;\n    }\n\n    @Override public String toString() {\n      return \"Endpoint\";\n    }\n  };\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/V2SpanWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static zipkin2.internal.JsonEscaper.jsonEscape;\nimport static zipkin2.internal.JsonEscaper.jsonEscapedSizeInBytes;\nimport static zipkin2.internal.WriteBuffer.asciiSizeInBytes;\n\n// @Immutable\npublic final class V2SpanWriter implements WriteBuffer.Writer<Span> {\n  @Override public int sizeInBytes(Span value) {\n    int sizeInBytes = 13; // {\"traceId\":\"\"\n    sizeInBytes += value.traceId().length();\n    if (value.parentId() != null) {\n      sizeInBytes += 30; // ,\"parentId\":\"0123456789abcdef\"\n    }\n    sizeInBytes += 24; // ,\"id\":\"0123456789abcdef\"\n    if (value.kind() != null) {\n      sizeInBytes += 10; // ,\"kind\":\"\"\n      sizeInBytes += value.kind().name().length();\n    }\n    if (value.name() != null) {\n      sizeInBytes += 10; // ,\"name\":\"\"\n      sizeInBytes += jsonEscapedSizeInBytes(value.name());\n    }\n    if (value.timestampAsLong() != 0L) {\n      sizeInBytes += 13; // ,\"timestamp\":\n      sizeInBytes += asciiSizeInBytes(value.timestampAsLong());\n    }\n    if (value.durationAsLong() != 0L) {\n      sizeInBytes += 12; // ,\"duration\":\n      sizeInBytes += asciiSizeInBytes(value.durationAsLong());\n    }\n    if (value.localEndpoint() != null) {\n      sizeInBytes += 17; // ,\"localEndpoint\":\n      sizeInBytes += endpointSizeInBytes(value.localEndpoint(), false);\n    }\n    if (value.remoteEndpoint() != null) {\n      sizeInBytes += 18; // ,\"remoteEndpoint\":\n      sizeInBytes += endpointSizeInBytes(value.remoteEndpoint(), false);\n    }\n    if (!value.annotations().isEmpty()) {\n      sizeInBytes += 17; // ,\"annotations\":[]\n      int length = value.annotations().size();\n      if (length > 1) sizeInBytes += length - 1; // comma to join elements\n      for (int i = 0; i < length; i++) {\n        Annotation a = value.annotations().get(i);\n        sizeInBytes += annotationSizeInBytes(a.timestamp(), a.value(), 0);\n      }\n    }\n    if (!value.tags().isEmpty()) {\n      sizeInBytes += 10; // ,\"tags\":{}\n      int tagCount = value.tags().size();\n      if (tagCount > 1) sizeInBytes += tagCount - 1; // comma to join elements\n      for (Map.Entry<String, String> entry : value.tags().entrySet()) {\n        sizeInBytes += 5; // \"\":\"\"\n        sizeInBytes += jsonEscapedSizeInBytes(entry.getKey());\n        sizeInBytes += jsonEscapedSizeInBytes(entry.getValue());\n      }\n    }\n    if (Boolean.TRUE.equals(value.debug())) {\n      sizeInBytes += 13; // ,\"debug\":true\n    }\n    if (Boolean.TRUE.equals(value.shared())) {\n      sizeInBytes += 14; // ,\"shared\":true\n    }\n    return ++sizeInBytes; // }\n  }\n\n  @Override public void write(Span value, WriteBuffer b) {\n    b.writeAscii(\"{\\\"traceId\\\":\\\"\");\n    b.writeAscii(value.traceId());\n    b.writeByte('\"');\n    if (value.parentId() != null) {\n      b.writeAscii(\",\\\"parentId\\\":\\\"\");\n      b.writeAscii(value.parentId());\n      b.writeByte('\"');\n    }\n    b.writeAscii(\",\\\"id\\\":\\\"\");\n    b.writeAscii(value.id());\n    b.writeByte('\"');\n    if (value.kind() != null) {\n      b.writeAscii(\",\\\"kind\\\":\\\"\");\n      b.writeAscii(value.kind().toString());\n      b.writeByte('\"');\n    }\n    if (value.name() != null) {\n      b.writeAscii(\",\\\"name\\\":\\\"\");\n      b.writeUtf8(jsonEscape(value.name()));\n      b.writeByte('\"');\n    }\n    if (value.timestampAsLong() != 0L) {\n      b.writeAscii(\",\\\"timestamp\\\":\");\n      b.writeAscii(value.timestampAsLong());\n    }\n    if (value.durationAsLong() != 0L) {\n      b.writeAscii(\",\\\"duration\\\":\");\n      b.writeAscii(value.durationAsLong());\n    }\n    if (value.localEndpoint() != null) {\n      b.writeAscii(\",\\\"localEndpoint\\\":\");\n      writeEndpoint(value.localEndpoint(), b, false);\n    }\n    if (value.remoteEndpoint() != null) {\n      b.writeAscii(\",\\\"remoteEndpoint\\\":\");\n      writeEndpoint(value.remoteEndpoint(), b, false);\n    }\n    if (!value.annotations().isEmpty()) {\n      b.writeAscii(\",\\\"annotations\\\":\");\n      b.writeByte('[');\n      for (int i = 0, length = value.annotations().size(); i < length; ) {\n        Annotation a = value.annotations().get(i++);\n        writeAnnotation(a.timestamp(), a.value(), null, b);\n        if (i < length) b.writeByte(',');\n      }\n      b.writeByte(']');\n    }\n    if (!value.tags().isEmpty()) {\n      b.writeAscii(\",\\\"tags\\\":{\");\n      Iterator<Map.Entry<String, String>> i = value.tags().entrySet().iterator();\n      while (i.hasNext()) {\n        Map.Entry<String, String> entry = i.next();\n        b.writeByte('\"');\n        b.writeUtf8(jsonEscape(entry.getKey()));\n        b.writeAscii(\"\\\":\\\"\");\n        b.writeUtf8(jsonEscape(entry.getValue()));\n        b.writeByte('\"');\n        if (i.hasNext()) b.writeByte(',');\n      }\n      b.writeByte('}');\n    }\n    if (Boolean.TRUE.equals(value.debug())) {\n      b.writeAscii(\",\\\"debug\\\":true\");\n    }\n    if (Boolean.TRUE.equals(value.shared())) {\n      b.writeAscii(\",\\\"shared\\\":true\");\n    }\n    b.writeByte('}');\n  }\n\n  @Override public String toString() {\n    return \"Span\";\n  }\n\n  static int endpointSizeInBytes(Endpoint value, boolean writeEmptyServiceName) {\n    int sizeInBytes = 1; // {\n    String serviceName = value.serviceName();\n    if (serviceName == null && writeEmptyServiceName) serviceName = \"\";\n    if (serviceName != null) {\n      sizeInBytes += 16; // \"serviceName\":\"\"\n      sizeInBytes += jsonEscapedSizeInBytes(serviceName);\n    }\n    if (value.ipv4() != null) {\n      if (sizeInBytes != 1) sizeInBytes++; // ,\n      sizeInBytes += 9; // \"ipv4\":\"\"\n      sizeInBytes += value.ipv4().length();\n    }\n    if (value.ipv6() != null) {\n      if (sizeInBytes != 1) sizeInBytes++; // ,\n      sizeInBytes += 9; // \"ipv6\":\"\"\n      sizeInBytes += value.ipv6().length();\n    }\n    int port = value.portAsInt();\n    if (port != 0) {\n      if (sizeInBytes != 1) sizeInBytes++; // ,\n      sizeInBytes += 7; // \"port\":\n      sizeInBytes += asciiSizeInBytes(port);\n    }\n    return ++sizeInBytes; // }\n  }\n\n  static void writeEndpoint(Endpoint value, WriteBuffer b, boolean writeEmptyServiceName) {\n    b.writeByte('{');\n    boolean wroteField = false;\n    String serviceName = value.serviceName();\n    if (serviceName == null && writeEmptyServiceName) serviceName = \"\";\n    if (serviceName != null) {\n      b.writeAscii(\"\\\"serviceName\\\":\\\"\");\n      b.writeUtf8(jsonEscape(serviceName));\n      b.writeByte('\"');\n      wroteField = true;\n    }\n    if (value.ipv4() != null) {\n      if (wroteField) b.writeByte(',');\n      b.writeAscii(\"\\\"ipv4\\\":\\\"\");\n      b.writeAscii(value.ipv4());\n      b.writeByte('\"');\n      wroteField = true;\n    }\n    if (value.ipv6() != null) {\n      if (wroteField) b.writeByte(',');\n      b.writeAscii(\"\\\"ipv6\\\":\\\"\");\n      b.writeAscii(value.ipv6());\n      b.writeByte('\"');\n      wroteField = true;\n    }\n    int port = value.portAsInt();\n    if (port != 0) {\n      if (wroteField) b.writeByte(',');\n      b.writeAscii(\"\\\"port\\\":\");\n      b.writeAscii(port);\n    }\n    b.writeByte('}');\n  }\n\n  static int annotationSizeInBytes(long timestamp, String value, int endpointSizeInBytes) {\n    int sizeInBytes = 25; // {\"timestamp\":,\"value\":\"\"}\n    sizeInBytes += asciiSizeInBytes(timestamp);\n    sizeInBytes += jsonEscapedSizeInBytes(value);\n    if (endpointSizeInBytes != 0) {\n      sizeInBytes += 12; // ,\"endpoint\":\n      sizeInBytes += endpointSizeInBytes;\n    }\n    return sizeInBytes;\n  }\n\n  static void writeAnnotation(long timestamp, String value, @Nullable byte[] endpoint,\n    WriteBuffer b) {\n    b.writeAscii(\"{\\\"timestamp\\\":\");\n    b.writeAscii(timestamp);\n    b.writeAscii(\",\\\"value\\\":\\\"\");\n    b.writeUtf8(jsonEscape(value));\n    b.writeByte('\"');\n    if (endpoint != null) {\n      b.writeAscii(\",\\\"endpoint\\\":\");\n      b.write(endpoint);\n    }\n    b.writeByte('}');\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/internal/WriteBuffer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport static zipkin2.internal.HexCodec.HEX_DIGITS;\n\n/**\n * Writes are unsafe as they do no bounds checks. This means you should take care to allocate or\n * wrap an array at least as big as you need prior to writing. As it is possible to calculate size\n * prior to writing, overrunning a buffer is a programming error.\n */\npublic final class WriteBuffer {\n  public interface Writer<T> {\n    int sizeInBytes(T value);\n\n    void write(T value, WriteBuffer buffer);\n  }\n\n  public static WriteBuffer wrap(byte[] bytes) {\n    return wrap(bytes, 0);\n  }\n\n  public static WriteBuffer wrap(byte[] bytes, int pos) {\n    return new WriteBuffer(bytes, pos);\n  }\n\n  final byte[] buf;\n  int pos;\n\n  WriteBuffer(byte[] buf, int pos) {\n    this.buf = buf;\n    this.pos = pos;\n  }\n\n  public void writeByte(int v) {\n    buf[pos++] = (byte) (v & 0xff);\n  }\n\n  public void write(byte[] v) {\n    System.arraycopy(v, 0, buf, pos, v.length);\n    pos += v.length;\n  }\n\n  void writeBackwards(long v) {\n    int lastPos = pos + asciiSizeInBytes(v); // We write backwards from right to left.\n    pos = lastPos;\n    while (v != 0) {\n      int digit = (int) (v % 10);\n      buf[--lastPos] = (byte) HEX_DIGITS[digit];\n      v /= 10;\n    }\n  }\n\n  /** Inspired by {@code okio.Buffer.writeLong} */\n  public void writeLongHex(long v) {\n    int pos = this.pos;\n    writeHexByte(buf, pos + 0, (byte) ((v >>> 56L) & 0xff));\n    writeHexByte(buf, pos + 2, (byte) ((v >>> 48L) & 0xff));\n    writeHexByte(buf, pos + 4, (byte) ((v >>> 40L) & 0xff));\n    writeHexByte(buf, pos + 6, (byte) ((v >>> 32L) & 0xff));\n    writeHexByte(buf, pos + 8, (byte) ((v >>> 24L) & 0xff));\n    writeHexByte(buf, pos + 10, (byte) ((v >>> 16L) & 0xff));\n    writeHexByte(buf, pos + 12, (byte) ((v >>> 8L) & 0xff));\n    writeHexByte(buf, pos + 14, (byte) (v & 0xff));\n    this.pos = pos + 16;\n  }\n\n  static void writeHexByte(byte[] data, int pos, byte b) {\n    data[pos + 0] = (byte) HEX_DIGITS[(b >> 4) & 0xf];\n    data[pos + 1] = (byte) HEX_DIGITS[b & 0xf];\n  }\n\n  int pos() {\n    return pos;\n  }\n\n  public void writeAscii(String v) {\n    for (int i = 0, length = v.length(); i < length; i++) {\n      writeByte(v.charAt(i) & 0xff);\n    }\n  }\n\n  /**\n   * This transcodes a UTF-16 Java String to UTF-8 bytes.\n   *\n   * <p>This looks most similar to {@code io.netty.buffer.ByteBufUtil.writeUtf8(AbstractByteBuf,\n   * int, CharSequence, int)} v4.1, modified including features to address ASCII runs of text.\n   */\n  public void writeUtf8(CharSequence string) {\n    for (int i = 0, len = string.length(); i < len; i++) {\n      char ch = string.charAt(i);\n      if (ch < 0x80) { // 7-bit ASCII character\n        writeByte(ch);\n        // This could be an ASCII run, or possibly entirely ASCII\n        while (i < len - 1) {\n          ch = string.charAt(i + 1);\n          if (ch >= 0x80) break;\n          i++;\n          writeByte(ch); // another 7-bit ASCII character\n        }\n      } else if (ch < 0x800) { // 11-bit character\n        writeByte(0xc0 | (ch >> 6));\n        writeByte(0x80 | (ch & 0x3f));\n      } else if (ch < 0xd800 || ch > 0xdfff) { // 16-bit character\n        writeByte(0xe0 | (ch >> 12));\n        writeByte(0x80 | ((ch >> 6) & 0x3f));\n        writeByte(0x80 | (ch & 0x3f));\n      } else { // Possibly a 21-bit character\n        if (!Character.isHighSurrogate(ch)) { // Malformed or not UTF-8\n          writeByte('?');\n          continue;\n        }\n        if (i == len - 1) { // Truncated or not UTF-8\n          writeByte('?');\n          break;\n        }\n        char low = string.charAt(++i);\n        if (!Character.isLowSurrogate(low)) { // Malformed or not UTF-8\n          writeByte('?');\n          writeByte(Character.isHighSurrogate(low) ? '?' : low);\n          continue;\n        }\n        // Write the 21-bit character using 4 bytes\n        // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630\n        int codePoint = Character.toCodePoint(ch, low);\n        writeByte(0xf0 | (codePoint >> 18));\n        writeByte(0x80 | ((codePoint >> 12) & 0x3f));\n        writeByte(0x80 | ((codePoint >> 6) & 0x3f));\n        writeByte(0x80 | (codePoint & 0x3f));\n      }\n    }\n  }\n\n  // Adapted from okio.Buffer.writeDecimalLong\n  public void writeAscii(long v) {\n    if (v == 0) {\n      writeByte('0');\n      return;\n    }\n\n    if (v == Long.MIN_VALUE) {\n      writeAscii(\"-9223372036854775808\");\n      return;\n    }\n\n    if (v < 0) {\n      writeByte('-');\n      v = -v; // needs to be positive so we can use this for an array index\n    }\n\n    writeBackwards(v);\n  }\n\n  // com.squareup.wire.ProtoWriter.writeVarint v2.3.0\n  void writeVarint(int v) {\n    while ((v & ~0x7f) != 0) {\n      writeByte((byte) ((v & 0x7f) | 0x80));\n      v >>>= 7;\n    }\n    writeByte((byte) v);\n  }\n\n  // com.squareup.wire.ProtoWriter.writeVarint v2.3.0\n  void writeVarint(long v) {\n    while ((v & ~0x7fL) != 0) {\n      writeByte((byte) ((v & 0x7f) | 0x80));\n      v >>>= 7;\n    }\n    writeByte((byte) v);\n  }\n\n  void writeLongLe(long v) {\n    writeByte((byte) (v & 0xff));\n    writeByte((byte) ((v >> 8) & 0xff));\n    writeByte((byte) ((v >> 16) & 0xff));\n    writeByte((byte) ((v >> 24) & 0xff));\n    writeByte((byte) ((v >> 32) & 0xff));\n    writeByte((byte) ((v >> 40) & 0xff));\n    writeByte((byte) ((v >> 48) & 0xff));\n    writeByte((byte) ((v >> 56) & 0xff));\n  }\n\n  /**\n   * This returns the bytes needed to transcode a UTF-16 Java String to UTF-8 bytes.\n   *\n   * <p>Originally based on\n   * http://stackoverflow.com/questions/8511490/calculating-length-in-utf-8-of-java-string-without-actually-encoding-it\n   *\n   * <p>Later, ASCII run and malformed surrogate logic borrowed from okio.Utf8\n   */\n  // TODO: benchmark vs https://github.com/protocolbuffers/protobuf/blob/master/java/core/src/main/java/com/google/protobuf/Utf8.java#L240\n  // there seem to be less branches for for strings without surrogates\n  public static int utf8SizeInBytes(CharSequence string) {\n    int sizeInBytes = 0;\n    for (int i = 0, len = string.length(); i < len; i++) {\n      char ch = string.charAt(i);\n      if (ch < 0x80) {\n        sizeInBytes++; // 7-bit ASCII character\n        // This could be an ASCII run, or possibly entirely ASCII\n        while (i < len - 1) {\n          ch = string.charAt(i + 1);\n          if (ch >= 0x80) break;\n          i++;\n          sizeInBytes++; // another 7-bit ASCII character\n        }\n      } else if (ch < 0x800) {\n        sizeInBytes += 2; // 11-bit character\n      } else if (ch < 0xd800 || ch > 0xdfff) {\n        sizeInBytes += 3; // 16-bit character\n      } else {\n        int low = i + 1 < len ? string.charAt(i + 1) : 0;\n        if (ch > 0xdbff || low < 0xdc00 || low > 0xdfff) {\n          sizeInBytes++; // A malformed surrogate, which yields '?'.\n        } else {\n          // A 21-bit character\n          sizeInBytes += 4;\n          i++;\n        }\n      }\n    }\n    return sizeInBytes;\n  }\n\n  /**\n   * Binary search for character width which favors matching lower numbers.\n   *\n   * <p>Adapted from okio.Buffer\n   */\n  public static int asciiSizeInBytes(long v) {\n    if (v == 0) return 1;\n    if (v == Long.MIN_VALUE) return 20;\n\n    boolean negative = false;\n    if (v < 0) {\n      v = -v; // making this positive allows us to compare using less-than\n      negative = true;\n    }\n    int width =\n      v < 100000000L\n        ? v < 10000L\n        ? v < 100L ? v < 10L ? 1 : 2 : v < 1000L ? 3 : 4\n        : v < 1000000L ? v < 100000L ? 5 : 6 : v < 10000000L ? 7 : 8\n        : v < 1000000000000L\n          ? v < 10000000000L ? v < 1000000000L ? 9 : 10 : v < 100000000000L ? 11 : 12\n          : v < 1000000000000000L\n            ? v < 10000000000000L ? 13 : v < 100000000000000L ? 14 : 15\n            : v < 100000000000000000L\n              ? v < 10000000000000000L ? 16 : 17\n              : v < 1000000000000000000L ? 18 : 19;\n    return negative ? width + 1 : width; // conditionally add room for negative sign\n  }\n\n  /**\n   * A base 128 varint encodes 7 bits at a time, this checks how many bytes are needed to represent\n   * the value.\n   *\n   * <p>See https://developers.google.com/protocol-buffers/docs/encoding#varints\n   *\n   * <p>This logic is the same as {@code com.squareup.wire.ProtoWriter.varint32Size} v2.3.0 which\n   * benchmarked faster than loop variants of the frequently copy/pasted VarInt.varIntSize\n   */\n  public static int varintSizeInBytes(int value) {\n    if ((value & (0xffffffff << 7)) == 0) return 1;\n    if ((value & (0xffffffff << 14)) == 0) return 2;\n    if ((value & (0xffffffff << 21)) == 0) return 3;\n    if ((value & (0xffffffff << 28)) == 0) return 4;\n    return 5;\n  }\n\n  /** Like {@link #varintSizeInBytes(int)}, except for uint64. */\n  // TODO: benchmark vs https://github.com/protocolbuffers/protobuf/blob/master/java/core/src/main/java/com/google/protobuf/CodedOutputStream.java#L770\n  // Since trace IDs are random, I guess they cover the entire spectrum of varint sizes and probably would especially benefit from this.\n  public static int varintSizeInBytes(long v) {\n    if ((v & (0xffffffffffffffffL << 7)) == 0) return 1;\n    if ((v & (0xffffffffffffffffL << 14)) == 0) return 2;\n    if ((v & (0xffffffffffffffffL << 21)) == 0) return 3;\n    if ((v & (0xffffffffffffffffL << 28)) == 0) return 4;\n    if ((v & (0xffffffffffffffffL << 35)) == 0) return 5;\n    if ((v & (0xffffffffffffffffL << 42)) == 0) return 6;\n    if ((v & (0xffffffffffffffffL << 49)) == 0) return 7;\n    if ((v & (0xffffffffffffffffL << 56)) == 0) return 8;\n    if ((v & (0xffffffffffffffffL << 63)) == 0) return 9;\n    return 10;\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/AutocompleteTags.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport zipkin2.Call;\n\n/**\n * Provides autocomplete functionality by providing values for a given tag key, usually derived from\n * {@link SpanConsumer}.\n */\npublic interface AutocompleteTags {\n\n  /**\n   * Retrieves the list of tag getKeys whose values may be returned by {@link #getValues(String)}.\n   *\n   * @see StorageComponent.Builder#autocompleteKeys(List)\n   */\n  Call<List<String>> getKeys();\n\n  /**\n   * Retrieves the list of values, if the input is configured for autocompletion. If a key is not\n   * configured, or there are no values available, an empty result will be returned.\n   *\n   * @throws IllegalArgumentException if the input is empty.\n   * @see StorageComponent.Builder#autocompleteKeys(List)\n   */\n  Call<List<String>> getValues(String key);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/ForwardingStorageComponent.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.io.IOException;\nimport zipkin2.CheckResult;\n\n/**\n * We provide a forwarding variant of the storage component for use cases such as trace decoration,\n * or throttling.\n *\n * <p>Extending this is better than extending {@link StorageComponent} directly because it reduces\n * risk of accidentally masking new methods. For example, if you extended storage component and\n * later a new feature for cache control was added, that feature would be blocked until the wrapper\n * was re-compiled. Such would be worse in most cases than not having decoration on new methods.\n *\n * @since 2.16\n */\npublic abstract class ForwardingStorageComponent extends StorageComponent {\n  /** Constructor for use by subclasses. */\n  protected ForwardingStorageComponent() {\n  }\n\n  /**\n   * The delegate is a method as opposed to a field, to allow for flexibility. For example, this\n   * allows you to choose to make a final or lazy field, or no field at all.\n   */\n  protected abstract StorageComponent delegate();\n\n  @Override public SpanConsumer spanConsumer() {\n    return delegate().spanConsumer();\n  }\n\n  @Override public Traces traces() {\n    return delegate().traces();\n  }\n\n  @Override public SpanStore spanStore() {\n    return delegate().spanStore();\n  }\n\n  @Override public AutocompleteTags autocompleteTags() {\n    return delegate().autocompleteTags();\n  }\n\n  @Override public ServiceAndSpanNames serviceAndSpanNames() {\n    return delegate().serviceAndSpanNames();\n  }\n\n  @Override public CheckResult check() {\n    return delegate().check();\n  }\n\n  @Override public boolean isOverCapacity(Throwable e) {\n    return delegate().isOverCapacity(e);\n  }\n\n  @Override public void close() throws IOException {\n    delegate().close();\n  }\n\n  @Override public String toString() {\n    return delegate().toString();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/GroupByTraceId.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport zipkin2.Call;\nimport zipkin2.Span;\n\nimport static zipkin2.storage.StrictTraceId.lowerTraceId;\n\n/**\n * A mapper that groups unorganized input spans by trace ID. Useful when preparing a result for\n * {@link SpanStore#getTraces(QueryRequest)}.\n */\npublic final class GroupByTraceId implements Call.Mapper<List<Span>, List<List<Span>>> {\n  public static Call.Mapper<List<Span>, List<List<Span>>> create(boolean strictTraceId) {\n    return new GroupByTraceId(strictTraceId);\n  }\n\n  final boolean strictTraceId;\n\n  GroupByTraceId(boolean strictTraceId) {\n    this.strictTraceId = strictTraceId;\n  }\n\n  @SuppressWarnings(\"MixedMutabilityReturnType\")\n  @Override public List<List<Span>> map(List<Span> input) {\n    if (input.isEmpty()) return Collections.emptyList();\n\n    Map<String, List<Span>> groupedByTraceId = new LinkedHashMap<>();\n    for (Span span : input) {\n      String traceId = span.traceId();\n      if (!strictTraceId) traceId = lowerTraceId(traceId);\n      if (!groupedByTraceId.containsKey(traceId)) {\n        groupedByTraceId.put(traceId, new ArrayList<>());\n      }\n      groupedByTraceId.get(traceId).add(span);\n    }\n    // Modifiable so that StrictTraceId can filter without allocating a new list\n    return new ArrayList<>(groupedByTraceId.values());\n  }\n\n  @Override public String toString() {\n    return \"GroupByTraceId{strictTraceId=\" + strictTraceId + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/InMemoryStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.DependencyLinker;\n\n/**\n * Test storage component that keeps all spans in memory, accepting them on the calling thread.\n *\n * <p>Internally, spans are indexed on 64-bit trace ID\n *\n * <p>Here's an example of some traces in memory:\n *\n * <pre>{@code\n * spansByTraceIdTimeStamp:\n *    <aaaa,July 4> --> ( spanA(time:July 4, traceId:aaaa, service:foo, name:GET),\n *                        spanB(time:July 4, traceId:aaaa, service:bar, name:GET) )\n *    <cccc,July 4> --> ( spanC(time:July 4, traceId:aaaa, service:foo, name:GET) )\n *    <bbbb,July 5> --> ( spanD(time:July 5, traceId:bbbb, service:biz, name:GET) )\n *    <bbbb,July 6> --> ( spanE(time:July 6, traceId:bbbb) service:foo, name:POST )\n *\n * traceIdToTraceIdTimeStamps:\n *    aaaa --> [ <aaaa,July 4> ]\n *    bbbb --> [ <bbbb,July 5>, <bbbb,July 6> ]\n *    cccc --> [ <cccc,July 4> ]\n *\n * serviceToTraceIds:\n *    foo --> [ <aaaa>, <cccc>, <bbbb> ]\n *    bar --> [ <aaaa> ]\n *    biz --> [ <bbbb> ]\n *\n * serviceToSpanNames:\n *    bar --> ( GET )\n *    biz --> ( GET )\n *    foo --> ( GET, POST )\n * }</pre>\n */\npublic final class InMemoryStorage extends StorageComponent implements SpanStore, SpanConsumer,\n  AutocompleteTags, ServiceAndSpanNames, Traces {\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder extends StorageComponent.Builder {\n    boolean strictTraceId = true, searchEnabled = true;\n    int maxSpanCount = 500000;\n    List<String> autocompleteKeys = Collections.emptyList();\n\n    @Override public Builder strictTraceId(boolean strictTraceId) {\n      this.strictTraceId = strictTraceId;\n      return this;\n    }\n\n    @Override public Builder searchEnabled(boolean searchEnabled) {\n      this.searchEnabled = searchEnabled;\n      return this;\n    }\n\n    @Override public Builder autocompleteKeys(List<String> autocompleteKeys) {\n      if (autocompleteKeys == null) throw new NullPointerException(\"autocompleteKeys == null\");\n      this.autocompleteKeys = autocompleteKeys;\n      return this;\n    }\n\n    /** Eldest traces are removed to ensure spans in memory don't exceed this value */\n    public Builder maxSpanCount(int maxSpanCount) {\n      if (maxSpanCount <= 0) throw new IllegalArgumentException(\"maxSpanCount <= 0\");\n      this.maxSpanCount = maxSpanCount;\n      return this;\n    }\n\n    @Override public InMemoryStorage build() {\n      return new InMemoryStorage(this);\n    }\n  }\n\n  /**\n   * Primary source of data is this map, which includes spans ordered descending by timestamp. All\n   * other maps are derived from the span values here. This uses a list for the spans, so that it is\n   * visible (via /api/v2/trace/{traceId}) when instrumentation report the same spans multiple\n   * times.\n   */\n  private final SortedMultimap<TraceIdTimestamp, Span> spansByTraceIdTimestamp =\n    new SortedMultimap<TraceIdTimestamp, Span>(TIMESTAMP_DESCENDING) {\n      @Override Collection<Span> valueContainer() {\n        return new LinkedHashSet<>();\n      }\n    };\n\n  /** This supports span lookup by {@link Span#traceId() lower 64-bits of the trace ID} */\n  private final SortedMultimap<String, TraceIdTimestamp> traceIdToTraceIdTimestamps =\n    new SortedMultimap<String, TraceIdTimestamp>(STRING_COMPARATOR) {\n      @Override Collection<TraceIdTimestamp> valueContainer() {\n        return new LinkedHashSet<>();\n      }\n    };\n  /** This is an index of {@link Span#traceId()} by {@link Endpoint#serviceName() service name} */\n  private final ServiceNameToTraceIds serviceToTraceIds = new ServiceNameToTraceIds();\n  /** This is an index of {@link Span#name()} by {@link Endpoint#serviceName() service name} */\n  private final SortedMultimap<String, String> serviceToSpanNames =\n    new SortedMultimap<String, String>(STRING_COMPARATOR) {\n      @Override Collection<String> valueContainer() {\n        return new LinkedHashSet<>();\n      }\n    };\n  /**\n   * This is an index of {@link Span#remoteServiceName()} by {@link Endpoint#serviceName() service\n   * name}\n   */\n  private final SortedMultimap<String, String> serviceToRemoteServiceNames =\n    new SortedMultimap<String, String>(STRING_COMPARATOR) {\n      @Override Collection<String> valueContainer() {\n        return new LinkedHashSet<>();\n      }\n    };\n\n  private final SortedMultimap<String, String> autocompleteTags =\n    new SortedMultimap<String, String>(STRING_COMPARATOR) {\n      @Override Collection<String> valueContainer() {\n        return new LinkedHashSet<>();\n      }\n    };\n\n  final boolean strictTraceId, searchEnabled;\n  final int maxSpanCount;\n  final Call<List<String>> autocompleteKeysCall;\n  final Set<String> autocompleteKeys;\n  final AtomicInteger acceptedSpanCount = new AtomicInteger();\n\n  InMemoryStorage(Builder builder) {\n    this.strictTraceId = builder.strictTraceId;\n    this.searchEnabled = builder.searchEnabled;\n    this.maxSpanCount = builder.maxSpanCount;\n    this.autocompleteKeysCall = Call.create(builder.autocompleteKeys);\n    this.autocompleteKeys = new LinkedHashSet<>(builder.autocompleteKeys);\n  }\n\n  public int acceptedSpanCount() {\n    return acceptedSpanCount.get();\n  }\n\n  public synchronized void clear() {\n    acceptedSpanCount.set(0);\n    traceIdToTraceIdTimestamps.clear();\n    spansByTraceIdTimestamp.clear();\n    serviceToTraceIds.clear();\n    serviceToRemoteServiceNames.clear();\n    serviceToSpanNames.clear();\n    autocompleteTags.clear();\n  }\n\n  @Override public Call<Void> accept(List<Span> spans) {\n    return new StoreSpansCall(spans);\n  }\n\n  synchronized void doAccept(List<Span> spans) {\n    int delta = spans.size();\n    acceptedSpanCount.addAndGet(delta);\n\n    int spansToRecover = (spansByTraceIdTimestamp.size() + delta) - maxSpanCount;\n    evictToRecoverSpans(spansToRecover);\n    for (Span span : spans) {\n      long timestamp = span.timestampAsLong() / 1000L;\n      String lowTraceId = lowTraceId(span.traceId());\n      TraceIdTimestamp traceIdTimeStamp = new TraceIdTimestamp(lowTraceId, timestamp);\n      spansByTraceIdTimestamp.put(traceIdTimeStamp, span);\n      traceIdToTraceIdTimestamps.put(lowTraceId, traceIdTimeStamp);\n\n      if (!searchEnabled) continue;\n      String serviceName = span.localServiceName();\n      if (serviceName != null) {\n        serviceToTraceIds.put(serviceName, lowTraceId);\n        String remoteServiceName = span.remoteServiceName();\n        if (remoteServiceName != null) {\n          serviceToRemoteServiceNames.put(serviceName, remoteServiceName);\n        }\n        String spanName = span.name();\n        if (spanName != null) {\n          serviceToSpanNames.put(serviceName, spanName);\n        }\n      }\n      for (Map.Entry<String, String> tag : span.tags().entrySet()) {\n        if (autocompleteKeys.contains(tag.getKey())) {\n          autocompleteTags.put(tag.getKey(), tag.getValue());\n        }\n      }\n    }\n  }\n\n  final class StoreSpansCall extends Call.Base<Void> {\n    final List<Span> spans;\n\n    StoreSpansCall(List<Span> spans) {\n      this.spans = spans;\n    }\n\n    @Override protected Void doExecute() {\n      doAccept(spans);\n      return null;\n    }\n\n    @Override protected void doEnqueue(Callback<Void> callback) {\n      try {\n        callback.onSuccess(doExecute());\n      } catch (Throwable t) {\n        propagateIfFatal(t);\n        callback.onError(t);\n      }\n    }\n\n    @Override public Call<Void> clone() {\n      return new StoreSpansCall(spans);\n    }\n\n    @Override public String toString() {\n      return \"StoreSpansCall{\" + spans + \"}\";\n    }\n  }\n\n  /** Returns the count of spans evicted. */\n  int evictToRecoverSpans(int spansToRecover) {\n    int spansEvicted = 0;\n    while (spansToRecover > 0) {\n      int spansInOldestTrace = deleteOldestTrace();\n      spansToRecover -= spansInOldestTrace;\n      spansEvicted += spansInOldestTrace;\n    }\n    return spansEvicted;\n  }\n\n  /** Returns the count of spans evicted. */\n  private int deleteOldestTrace() {\n    int spansEvicted = 0;\n    String lowTraceId = spansByTraceIdTimestamp.delegate.lastKey().lowTraceId;\n    Collection<TraceIdTimestamp> traceIdTimeStamps = traceIdToTraceIdTimestamps.remove(lowTraceId);\n    for (TraceIdTimestamp traceIdTimeStamp : traceIdTimeStamps) {\n      Collection<Span> spans = spansByTraceIdTimestamp.remove(traceIdTimeStamp);\n      spansEvicted += spans.size();\n    }\n    if (searchEnabled) {\n      for (String orphanedService : serviceToTraceIds.removeServiceIfTraceId(lowTraceId)) {\n        serviceToRemoteServiceNames.remove(orphanedService);\n        serviceToSpanNames.remove(orphanedService);\n      }\n    }\n    return spansEvicted;\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {\n    return getTraces(request, strictTraceId);\n  }\n\n  synchronized Call<List<List<Span>>> getTraces(QueryRequest request, boolean strictTraceId) {\n    Set<String> lowTraceIdsInRange = traceIdsDescendingByTimestamp(request);\n    if (lowTraceIdsInRange.isEmpty()) return Call.emptyList();\n\n    List<List<Span>> result = new ArrayList<>();\n    for (Iterator<String> lowTraceId = lowTraceIdsInRange.iterator();\n      lowTraceId.hasNext() && result.size() < request.limit(); ) {\n      List<Span> next = spansByTraceId(lowTraceId.next());\n      if (!request.test(next)) continue;\n      if (!strictTraceId) {\n        result.add(next);\n        continue;\n      }\n\n      // re-run the query as now spans are strictly grouped\n      for (List<Span> strictTrace : strictByTraceId(next)) {\n        if (request.test(strictTrace)) result.add(strictTrace);\n      }\n    }\n\n    return Call.create(result);\n  }\n\n  static Collection<List<Span>> strictByTraceId(List<Span> next) {\n    Map<String, List<Span>> groupedByTraceId = new LinkedHashMap<>();\n    for (Span span : next) {\n      String traceId = span.traceId();\n      if (!groupedByTraceId.containsKey(traceId)) {\n        groupedByTraceId.put(traceId, new ArrayList<>());\n      }\n      groupedByTraceId.get(traceId).add(span);\n    }\n    return groupedByTraceId.values();\n  }\n\n  /** Used for testing. Returns all traces unconditionally. */\n  public synchronized List<List<Span>> getTraces() {\n    List<List<Span>> result = new ArrayList<>();\n    for (String lowTraceId : traceIdToTraceIdTimestamps.keySet()) {\n      List<Span> sameTraceId = spansByTraceId(lowTraceId);\n      if (strictTraceId) {\n        result.addAll(strictByTraceId(sameTraceId));\n      } else {\n        result.add(sameTraceId);\n      }\n    }\n    return result;\n  }\n\n  /** Used for testing. Returns all dependency links unconditionally. */\n  public synchronized List<DependencyLink> getDependencies() {\n    return getDependencyLinks(traceIdToTraceIdTimestamps.keySet());\n  }\n\n  Set<String> traceIdsDescendingByTimestamp(QueryRequest request) {\n    if (!searchEnabled) return Collections.emptySet();\n\n    Collection<TraceIdTimestamp> traceIdTimestamps =\n      request.serviceName() != null\n        ? traceIdTimestampsByServiceName(request.serviceName())\n        : spansByTraceIdTimestamp.keySet();\n\n    if (traceIdTimestamps == null || traceIdTimestamps.isEmpty()) return Collections.emptySet();\n\n    return lowTraceIdsInRange(traceIdTimestamps, request.endTs, request.lookback);\n  }\n\n  static Set<String> lowTraceIdsInRange(\n    Collection<TraceIdTimestamp> descendingByTimestamp, long endTs, long lookback) {\n    long beginTs = endTs - lookback;\n    Set<String> result = new LinkedHashSet<>();\n    for (TraceIdTimestamp traceIdTimestamp : descendingByTimestamp) {\n      if (traceIdTimestamp.timestamp >= beginTs && traceIdTimestamp.timestamp <= endTs) {\n        result.add(traceIdTimestamp.lowTraceId);\n      }\n    }\n    return Collections.unmodifiableSet(result);\n  }\n\n  @Override public synchronized Call<List<Span>> getTrace(String traceId) {\n    traceId = Span.normalizeTraceId(traceId);\n    List<Span> spans = spansByTraceId(lowTraceId(traceId));\n    if (spans.isEmpty()) return Call.emptyList();\n    if (!strictTraceId) return Call.create(spans);\n\n    List<Span> filtered = new ArrayList<>(spans);\n    Iterator<Span> iterator = filtered.iterator();\n    while (iterator.hasNext()) {\n      if (!iterator.next().traceId().equals(traceId)) {\n        iterator.remove();\n      }\n    }\n    return Call.create(filtered);\n  }\n\n  @Override public synchronized Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n    Set<String> normalized = new LinkedHashSet<>();\n    for (String traceId : traceIds) {\n      normalized.add(Span.normalizeTraceId(traceId));\n    }\n\n    // Our index is by lower-64 bit trace ID, so let's build trace IDs to fetch\n    Set<String> lower64Bit = new LinkedHashSet<>();\n    for (String traceId : normalized) {\n      lower64Bit.add(lowTraceId(traceId));\n    }\n\n    List<List<Span>> result = new ArrayList<>();\n    for (String lowTraceId : lower64Bit) {\n      List<Span> sameTraceId = spansByTraceId(lowTraceId);\n      if (strictTraceId) {\n        for (List<Span> trace : strictByTraceId(sameTraceId)) {\n          if (normalized.contains(trace.get(0).traceId())) {\n            result.add(trace);\n          }\n        }\n      } else {\n        result.add(sameTraceId);\n      }\n    }\n\n    return Call.create(result);\n  }\n\n  @Override public synchronized Call<List<String>> getServiceNames() {\n    if (!searchEnabled) return Call.emptyList();\n    return Call.create(new ArrayList<>(serviceToTraceIds.keySet()));\n  }\n\n  @Override public synchronized Call<List<String>> getRemoteServiceNames(String service) {\n    if (service.isEmpty() || !searchEnabled) return Call.emptyList();\n    service = service.toLowerCase(Locale.ROOT); // service names are always lowercase!\n    return Call.create(\n        new ArrayList<>(serviceToRemoteServiceNames.get(service)));\n  }\n\n  @Override public synchronized Call<List<String>> getSpanNames(String service) {\n    if (service.isEmpty() || !searchEnabled) return Call.emptyList();\n    service = service.toLowerCase(Locale.ROOT); // service names are always lowercase!\n    return Call.create(new ArrayList<>(serviceToSpanNames.get(service)));\n  }\n\n  @Override\n  public synchronized Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n    if (endTs <= 0) throw new IllegalArgumentException(\"endTs <= 0\");\n    if (lookback <= 0) throw new IllegalArgumentException(\"lookback <= 0\");\n\n    Set<String> lowTraceIdsInRange =\n      lowTraceIdsInRange(spansByTraceIdTimestamp.keySet(), endTs, lookback);\n    List<DependencyLink> links = getDependencyLinks(lowTraceIdsInRange);\n    return Call.create(links);\n  }\n\n  // We don't have a query parameter for strictTraceId when fetching dependency links, so we\n  // ignore traceIdHigh. Otherwise, a single trace can appear as two, doubling callCount.\n  List<DependencyLink> getDependencyLinks(Set<String> lowTraceIdsInRange) {\n    if (lowTraceIdsInRange.isEmpty()) return Collections.emptyList();\n    DependencyLinker linksBuilder = new DependencyLinker();\n    for (String lowTraceId : lowTraceIdsInRange) {\n      linksBuilder.putTrace(spansByTraceId(lowTraceId));\n    }\n    return linksBuilder.link();\n  }\n\n  @Override public synchronized Call<List<String>> getKeys() {\n    if (!searchEnabled) return Call.emptyList();\n    return autocompleteKeysCall.clone();\n  }\n\n  @Override public synchronized Call<List<String>> getValues(String key) {\n    if (key == null) throw new NullPointerException(\"key == null\");\n    if (key.isEmpty()) throw new IllegalArgumentException(\"key was empty\");\n    if (!searchEnabled) return Call.emptyList();\n    return Call.create(new ArrayList<>(autocompleteTags.get(key)));\n  }\n\n  static final Comparator<String> STRING_COMPARATOR = new Comparator<String>() {\n    @Override public int compare(String left, String right) {\n      if (left == null) return -1;\n      return left.compareTo(right);\n    }\n\n    @Override public String toString() {\n      return \"String::compareTo\";\n    }\n  };\n\n  static final Comparator<TraceIdTimestamp> TIMESTAMP_DESCENDING =\n    new Comparator<TraceIdTimestamp>() {\n      @Override public int compare(TraceIdTimestamp left, TraceIdTimestamp right) {\n        long x = left.timestamp, y = right.timestamp;\n        int result = Long.compare(x, y); // Long.compareTo is JRE 7+\n        if (result != 0) return -result; // use negative as we are descending\n        return right.lowTraceId.compareTo(left.lowTraceId);\n      }\n\n      @Override public String toString() {\n        return \"TimestampDescending{}\";\n      }\n    };\n\n  static final class ServiceNameToTraceIds extends SortedMultimap<String, String> {\n    ServiceNameToTraceIds() {\n      super(STRING_COMPARATOR);\n    }\n\n    @Override Set<String> valueContainer() {\n      return new LinkedHashSet<>();\n    }\n\n    /** Returns service names orphaned by removing the trace ID */\n    Set<String> removeServiceIfTraceId(String lowTraceId) {\n      Set<String> result = new LinkedHashSet<>();\n      for (Map.Entry<String, Collection<String>> entry : delegate.entrySet()) {\n        Collection<String> lowTraceIds = entry.getValue();\n        if (lowTraceIds.remove(lowTraceId) && lowTraceIds.isEmpty()) {\n          result.add(entry.getKey());\n        }\n      }\n      delegate.keySet().removeAll(result);\n      return result;\n    }\n  }\n\n  // Not synchronized as every exposed method on the enclosing type is\n  abstract static class SortedMultimap<K, V> {\n    final SortedMap<K, Collection<V>> delegate;\n    int size = 0;\n\n    SortedMultimap(Comparator<K> comparator) {\n      delegate = new TreeMap<>(comparator);\n    }\n\n    abstract Collection<V> valueContainer();\n\n    Set<K> keySet() {\n      return delegate.keySet();\n    }\n\n    int size() {\n      return size;\n    }\n\n    void put(K key, V value) {\n      Collection<V> valueContainer = delegate.get(key);\n      if (valueContainer == null) {\n        delegate.put(key, valueContainer = valueContainer());\n      }\n      if (valueContainer.add(value)) size++;\n    }\n\n    Collection<V> remove(K key) {\n      Collection<V> value = delegate.remove(key);\n      if (value != null) size -= value.size();\n      return value;\n    }\n\n    void clear() {\n      delegate.clear();\n      size = 0;\n    }\n\n    Collection<V> get(K key) {\n      Collection<V> result = delegate.get(key);\n      return result != null ? result : Collections.emptySet();\n    }\n  }\n\n  List<Span> spansByTraceId(String lowTraceId) {\n    List<Span> sameTraceId = new ArrayList<>();\n    for (TraceIdTimestamp traceIdTimestamp : traceIdToTraceIdTimestamps.get(lowTraceId)) {\n      sameTraceId.addAll(spansByTraceIdTimestamp.get(traceIdTimestamp));\n    }\n    return sameTraceId;\n  }\n\n  Collection<TraceIdTimestamp> traceIdTimestampsByServiceName(String serviceName) {\n    List<TraceIdTimestamp> traceIdTimestamps = new ArrayList<>();\n    for (String lowTraceId : serviceToTraceIds.get(serviceName)) {\n      traceIdTimestamps.addAll(traceIdToTraceIdTimestamps.get(lowTraceId));\n    }\n    traceIdTimestamps.sort(TIMESTAMP_DESCENDING);\n    return traceIdTimestamps;\n  }\n\n  static String lowTraceId(String traceId) {\n    return traceId.length() == 32 ? traceId.substring(16) : traceId;\n  }\n\n  @Override public InMemoryStorage traces() {\n    return this;\n  }\n\n  @Override public InMemoryStorage spanStore() {\n    return this;\n  }\n\n  @Override public InMemoryStorage autocompleteTags() {\n    return this;\n  }\n\n  @Override public InMemoryStorage serviceAndSpanNames() {\n    return this;\n  }\n\n  @Override public SpanConsumer spanConsumer() {\n    return this;\n  }\n\n  @Override public void close() {\n  }\n\n  static final class TraceIdTimestamp {\n    final String lowTraceId;\n    final long timestamp;\n\n    TraceIdTimestamp(String lowTraceId, long timestamp) {\n      this.lowTraceId = lowTraceId;\n      this.timestamp = timestamp;\n    }\n\n    @Override public boolean equals(Object o) {\n      if (o == this) return true;\n      if (!(o instanceof TraceIdTimestamp)) return false;\n      TraceIdTimestamp that = (TraceIdTimestamp) o;\n      return lowTraceId.equals(that.lowTraceId) && timestamp == that.timestamp;\n    }\n\n    @Override public int hashCode() {\n      int h$ = 1;\n      h$ *= 1000003;\n      h$ ^= lowTraceId.hashCode();\n      h$ *= 1000003;\n      h$ ^= (int) ((timestamp >>> 32) ^ timestamp);\n      return h$;\n    }\n  }\n\n  @Override public String toString() {\n    return \"InMemoryStorage{}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/QueryRequest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport zipkin2.Annotation;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\n\n/**\n * Invoking this request retrieves traces matching the below filters.\n *\n * <p>Results should be filtered against {@link #endTs}, subject to {@link #limit} and {@link\n * #lookback}. For example, if endTs is 10:20 today, limit is 10, and lookback is 7 days, traces\n * returned should be those nearest to 10:20 today, not 10:20 a week ago.\n *\n * <p>Time units of {@link #endTs} and {@link #lookback} are milliseconds as opposed to\n * microseconds, the grain of {@link Span#timestamp()}. Milliseconds is a more familiar and\n * supported granularity for query, index and windowing functions.\n */\npublic final class QueryRequest {\n  /**\n   * When present, corresponds to the {@link Span#localServiceName() local service name} and\n   * constrains all other parameters.\n   *\n   * @see ServiceAndSpanNames#getServiceNames()\n   */\n  @Nullable public String serviceName() {\n    return serviceName;\n  }\n\n  /**\n   * When present, only include traces with this {@link Span#remoteServiceName() remote service\n   * name}.\n   *\n   * @see ServiceAndSpanNames#getRemoteServiceNames(String)\n   */\n  @Nullable public String remoteServiceName() {\n    return remoteServiceName;\n  }\n\n  /**\n   * When present, only include traces with this {@link Span#name()}\n   *\n   * @see ServiceAndSpanNames#getSpanNames(String)\n   */\n  @Nullable public String spanName() {\n    return spanName;\n  }\n\n  /**\n   * When an input value is the empty string, include traces whose {@link Span#annotations()}\n   * include a value in this set, or where {@link Span#tags()} include a key is in this set. When\n   * not, include traces whose {@link Span#tags()} an entry in this map.\n   *\n   * <p>Multiple entries are combined with AND, and AND against other conditions.\n   */\n  public Map<String, String> annotationQuery() {\n    return annotationQuery;\n  }\n\n  /**\n   * Only return traces whose {@link Span#duration()} is greater than or equal to minDuration\n   * microseconds.\n   */\n  @Nullable public Long minDuration() {\n    return minDuration;\n  }\n\n  /**\n   * Only return traces whose {@link Span#duration()} is less than or equal to maxDuration\n   * microseconds. Only valid with {@link #minDuration}.\n   */\n  @Nullable public Long maxDuration() {\n    return maxDuration;\n  }\n\n  /**\n   * Only return traces where all {@link Span#timestamp()} are at or before this time in epoch\n   * milliseconds. Defaults to current time.\n   */\n  public long endTs() {\n    return endTs;\n  }\n\n  /**\n   * Only return traces where all {@link Span#timestamp()} are at or after (endTs - lookback) in\n   * milliseconds. Defaults to endTs.\n   */\n  public long lookback() {\n    return lookback;\n  }\n\n  /** Maximum number of traces to return. Defaults to 10 */\n  public int limit() {\n    return limit;\n  }\n\n  /**\n   * Corresponds to query parameter \"annotationQuery\". Ex. \"http.method=GET and error\"\n   *\n   * @see QueryRequest.Builder#parseAnnotationQuery(String)\n   */\n  @Nullable public String annotationQueryString() {\n    StringBuilder result = new StringBuilder();\n\n    for (Iterator<Map.Entry<String, String>> i = annotationQuery().entrySet().iterator();\n      i.hasNext(); ) {\n      Map.Entry<String, String> next = i.next();\n      if (next.getKey().isEmpty()) {\n        continue; // Values can be empty, but keys cannot. Don't err as we didn't before.\n      }\n      result.append(next.getKey());\n      if (!next.getValue().isEmpty()) result.append('=').append(next.getValue());\n      if (i.hasNext()) result.append(\" and \");\n    }\n\n    return result.length() > 0 ? result.toString() : null;\n  }\n\n  public Builder toBuilder() {\n    return new Builder(this);\n  }\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder {\n    String serviceName, remoteServiceName, spanName;\n    Map<String, String> annotationQuery = Collections.emptyMap();\n    Long minDuration, maxDuration;\n    long endTs, lookback;\n    int limit;\n\n    Builder(QueryRequest source) {\n      serviceName = source.serviceName;\n      remoteServiceName = source.remoteServiceName;\n      spanName = source.spanName;\n      annotationQuery = source.annotationQuery;\n      minDuration = source.minDuration;\n      maxDuration = source.maxDuration;\n      endTs = source.endTs;\n      lookback = source.lookback;\n      limit = source.limit;\n    }\n\n    /** Sets {@link QueryRequest#serviceName()} */\n    public Builder serviceName(@Nullable String serviceName) {\n      this.serviceName = serviceName;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#remoteServiceName()} */\n    public Builder remoteServiceName(@Nullable String remoteServiceName) {\n      this.remoteServiceName = remoteServiceName;\n      return this;\n    }\n\n    /**\n     * This ignores the reserved span name \"all\".\n     *\n     * @see QueryRequest#spanName()\n     */\n    public Builder spanName(@Nullable String spanName) {\n      this.spanName = spanName;\n      return this;\n    }\n\n    /**\n     * Corresponds to query parameter \"annotationQuery\". Ex. \"http.method=GET and error\". Parameter\n     * keys and values are trimmed.\n     *\n     * @see QueryRequest#annotationQueryString()\n     */\n    public Builder parseAnnotationQuery(@Nullable String annotationQuery) {\n      if (annotationQuery == null || annotationQuery.isEmpty()) return this;\n      Map<String, String> map = new LinkedHashMap<>();\n      for (String ann : annotationQuery.split(\" and \", 100)) {\n        int idx = ann.indexOf('=');\n        if (idx == -1) {\n          // put the annotation only if there is no key present already, prevents overriding more specific tags\n          ann = ann.trim();\n          if (!map.containsKey(ann)) map.put(ann, \"\");\n        } else {\n          // tag\n          String[] keyValue = ann.split(\"=\", 2);\n          // tags are put regardless, i.e. last tag wins\n          map.put(ann.substring(0, idx).trim(),\n            keyValue.length < 2 ? \"\" : ann.substring(idx + 1).trim());\n        }\n      }\n      return annotationQuery(map);\n    }\n\n    /** Sets {@link QueryRequest#annotationQuery()} */\n    public Builder annotationQuery(Map<String, String> annotationQuery) {\n      if (annotationQuery == null) throw new NullPointerException(\"annotationQuery == null\");\n      this.annotationQuery = annotationQuery;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#minDuration()} */\n    public Builder minDuration(@Nullable Long minDuration) {\n      this.minDuration = minDuration;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#maxDuration()} */\n    public Builder maxDuration(@Nullable Long maxDuration) {\n      this.maxDuration = maxDuration;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#endTs()} */\n    public Builder endTs(long endTs) {\n      this.endTs = endTs;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#lookback()} */\n    public Builder lookback(long lookback) {\n      this.lookback = lookback;\n      return this;\n    }\n\n    /** Sets {@link QueryRequest#limit()} */\n    public Builder limit(int limit) {\n      this.limit = limit;\n      return this;\n    }\n\n    public QueryRequest build() {\n      // coerce service and span names to lowercase\n      if (serviceName != null) serviceName = serviceName.toLowerCase(Locale.ROOT);\n      if (remoteServiceName != null) remoteServiceName = remoteServiceName.toLowerCase(Locale.ROOT);\n      if (spanName != null) spanName = spanName.toLowerCase(Locale.ROOT);\n\n      if (\"\".equals(serviceName)) serviceName = null;\n      if (\"\".equals(remoteServiceName)) remoteServiceName = null;\n      if (\"\".equals(spanName) || \"all\".equals(spanName)) spanName = null;\n\n      if (endTs <= 0) throw new IllegalArgumentException(\"endTs <= 0\");\n      if (limit <= 0) throw new IllegalArgumentException(\"limit <= 0\");\n      if (lookback <= 0) throw new IllegalArgumentException(\"lookback <= 0\");\n      if (minDuration != null) {\n        if (minDuration <= 0) throw new IllegalArgumentException(\"minDuration <= 0\");\n        if (maxDuration != null && maxDuration < minDuration) {\n          throw new IllegalArgumentException(\"maxDuration < minDuration\");\n        }\n      } else if (maxDuration != null) {\n        throw new IllegalArgumentException(\"maxDuration is only valid with minDuration\");\n      }\n\n      return new QueryRequest(\n        serviceName,\n        remoteServiceName,\n        spanName,\n        annotationQuery,\n        minDuration,\n        maxDuration,\n        endTs,\n        lookback,\n        limit\n      );\n    }\n\n    Builder() {\n    }\n  }\n\n  /**\n   * Tests the supplied trace against the current request.\n   *\n   * <p>This is used when the backend cannot fully refine a trace query.\n   */\n  public boolean test(List<Span> spans) {\n    // v2 returns raw spans in any order, get the root's timestamp or the first timestamp\n    long timestamp = 0L;\n    for (Span span : spans) {\n      if (span.timestampAsLong() == 0L) continue;\n      if (span.parentId() == null) {\n        timestamp = span.timestampAsLong();\n        break;\n      }\n      if (timestamp == 0L || timestamp > span.timestampAsLong()) {\n        timestamp = span.timestampAsLong();\n      }\n    }\n    if (timestamp == 0L ||\n      timestamp < (endTs() - lookback()) * 1000 ||\n      timestamp > endTs() * 1000) {\n      return false;\n    }\n    boolean testedDuration = minDuration() == null && maxDuration() == null;\n\n    String serviceNameToMatch = serviceName();\n    String remoteServiceNameToMatch = remoteServiceName();\n    String spanNameToMatch = spanName();\n    Map<String, String> annotationQueryRemaining = new LinkedHashMap<>(annotationQuery());\n\n    for (Span span : spans) {\n      String localServiceName = span.localServiceName();\n\n      // service name, when present, constrains other queries.\n      if (serviceName() == null || serviceName().equals(localServiceName)) {\n        serviceNameToMatch = null;\n        for (Annotation a : span.annotations()) {\n          if (\"\".equals(annotationQueryRemaining.get(a.value()))) {\n            annotationQueryRemaining.remove(a.value());\n          }\n        }\n        for (Map.Entry<String, String> t : span.tags().entrySet()) {\n          String value = annotationQueryRemaining.get(t.getKey());\n          if (value == null) continue;\n          if (value.isEmpty() || value.equals(t.getValue())) {\n            annotationQueryRemaining.remove(t.getKey());\n          }\n        }\n        if (remoteServiceNameToMatch != null && remoteServiceNameToMatch.equals(\n          span.remoteServiceName())) {\n          remoteServiceNameToMatch = null;\n        }\n        if (spanNameToMatch != null && spanNameToMatch.equals(span.name())) {\n          spanNameToMatch = null;\n        }\n        if (!testedDuration) {\n          if (minDuration() != null && maxDuration() != null) {\n            testedDuration =\n              span.durationAsLong() >= minDuration() && span.durationAsLong() <= maxDuration();\n          } else if (minDuration() != null) {\n            testedDuration = span.durationAsLong() >= minDuration();\n          }\n        }\n      }\n    }\n    return (serviceName() == null || serviceNameToMatch == null)\n      && remoteServiceNameToMatch == null\n      && spanNameToMatch == null\n      && annotationQueryRemaining.isEmpty()\n      && testedDuration;\n  }\n\n  final String serviceName, remoteServiceName, spanName;\n  final Map<String, String> annotationQuery;\n  final Long minDuration, maxDuration;\n  final long endTs, lookback;\n  final int limit;\n\n  QueryRequest(\n    @Nullable String serviceName,\n    @Nullable String remoteServiceName,\n    @Nullable String spanName,\n    Map<String, String> annotationQuery,\n    @Nullable Long minDuration,\n    @Nullable Long maxDuration,\n    long endTs,\n    long lookback,\n    int limit) {\n    this.serviceName = serviceName;\n    this.remoteServiceName = remoteServiceName;\n    this.spanName = spanName;\n    this.annotationQuery = annotationQuery;\n    this.minDuration = minDuration;\n    this.maxDuration = maxDuration;\n    this.endTs = endTs;\n    this.lookback = lookback;\n    this.limit = limit;\n  }\n\n  @Override public String toString() {\n    String result = \"QueryRequest{\";\n    result += (\"endTs=\" + endTs + \", \");\n    result += (\"lookback=\" + lookback + \", \");\n    if (serviceName != null) result += (\"serviceName=\" + serviceName + \", \");\n    if (remoteServiceName != null) result += (\"remoteServiceName=\" + remoteServiceName + \", \");\n    if (spanName != null) result += (\"spanName=\" + spanName + \", \");\n    if (!annotationQuery.isEmpty()) result += (\"annotationQuery=\" + annotationQuery + \", \");\n    if (minDuration != null) result += (\"minDuration=\" + minDuration + \", \");\n    if (maxDuration != null) result += (\"maxDuration=\" + maxDuration + \", \");\n    return result + \"limit=\" + limit + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/ServiceAndSpanNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\n/**\n * Provides autocomplete functionality by providing values for service and span names, usually\n * derived from {@link SpanConsumer}.\n */\npublic interface ServiceAndSpanNames {\n\n  /**\n   * Retrieves all {@link Span#localEndpoint() local} {@link Endpoint#serviceName() service names},\n   * sorted lexicographically.\n   */\n  Call<List<String>> getServiceNames();\n\n  /**\n   * Retrieves all {@link Span#remoteEndpoint() remote} {@link Endpoint#serviceName() service names}\n   * recorded by a {@link Span#localEndpoint() service}, sorted lexicographically.\n   */\n  Call<List<String>> getRemoteServiceNames(String serviceName);\n\n  /**\n   * Retrieves all {@link Span#name() span names} recorded by a {@link Span#localEndpoint()\n   * service}, sorted lexicographically.\n   */\n  Call<List<String>> getSpanNames(String serviceName);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/SpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.Span;\n\n// @FunctionalInterface\npublic interface SpanConsumer {\n  Call<Void> accept(List<Span> spans);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/SpanStore.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.DependencyLinker;\n\n/**\n * Queries data derived from {@link SpanConsumer}.\n *\n * <p>Note: This is not considered a user-level Api, rather an Spi that can be used to bind\n * user-level abstractions such as futures or observables.\n */\npublic interface SpanStore {\n\n  /**\n   * Retrieves spans grouped by trace ID from the storage system with no ordering expectation.\n   *\n   * <p>When strict trace ID is disabled, spans are grouped by the right-most 16 characters of the\n   * trace ID.\n   */\n  Call<List<List<Span>>> getTraces(QueryRequest request);\n\n  /**\n   * Retrieves spans that share a 128-bit trace id with no ordering expectation or empty if none are\n   * found.\n   *\n   * <p>When strict trace ID is disabled, spans with the same right-most 16 characters are returned\n   * even if the characters to the left are not.\n   *\n   * <p>Implementations should use {@link Span#normalizeTraceId(String)} to ensure consistency.\n   *\n   * @param traceId the {@link Span#traceId() trace ID}\n   * @deprecated use {@link Traces#getTrace(String)}\n   */\n  @Deprecated Call<List<Span>> getTrace(String traceId);\n\n  /**\n   * Retrieves all {@link Span#localEndpoint() local} and {@link Span#remoteEndpoint() remote}\n   * {@link Endpoint#serviceName() service names}, sorted lexicographically.\n   *\n   * @deprecated use {@link ServiceAndSpanNames#getServiceNames()}\n   */\n  @Deprecated Call<List<String>> getServiceNames();\n\n  /**\n   * Retrieves all {@link Span#name() span names} recorded by a {@link Span#localEndpoint()\n   * service}, sorted lexicographically.\n   *\n   * @deprecated use {@link ServiceAndSpanNames#getSpanNames(String)}\n   */\n  @Deprecated Call<List<String>> getSpanNames(String serviceName);\n\n  /**\n   * Returns dependency links derived from spans in an interval contained by (endTs - lookback) or\n   * empty if none are found.\n   *\n   * <p>Implementations may bucket aggregated data, for example daily. When this is the case, endTs\n   * may be floored to align with that bucket, for example midnight if daily. lookback applies to\n   * the original endTs, even when bucketed. Using the daily example, if endTs was 11pm and lookback\n   * was 25 hours, the implementation would query against 2 buckets.\n   *\n   * <p>Some implementations parse spans from storage and call {@link\n   * DependencyLinker} to aggregate links. The reason is certain graph logic, such as skipping up\n   * the tree is difficult to implement as a storage query.\n   *\n   * <p>Spans are grouped by the right-most 16 characters of the trace ID. This ensures call counts\n   * are not incremented twice due to one hop downgrading from 128 to 64-bit trace IDs.\n   *\n   * @param endTs only return links from spans where {@link Span#timestamp()} are at or before this\n   * time in epoch milliseconds.\n   * @param lookback only return links from spans where {@link Span#timestamp()} are at or after\n   * (endTs - lookback) in milliseconds.\n   */\n  Call<List<DependencyLink>> getDependencies(long endTs, long lookback);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/StorageComponent.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.logging.Logger;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.internal.TracesAdapter;\n\n/**\n * A component that provides storage interfaces used for spans and aggregations. Implementations are\n * free to provide other interfaces, but the ones declared here must be supported.\n *\n * @see InMemoryStorage\n */\npublic abstract class StorageComponent extends Component {\n\n  public Traces traces() {\n    return new TracesAdapter(spanStore()); // delegates to deprecated methods.\n  }\n\n  public abstract SpanStore spanStore();\n\n  public AutocompleteTags autocompleteTags() { // returns default to not break compat\n    return new AutocompleteTags() {\n      @Override public Call<List<String>> getKeys() {\n        return Call.emptyList();\n      }\n\n      @Override public Call<List<String>> getValues(String key) {\n        return Call.emptyList();\n      }\n\n      @Override public String toString() {\n        return \"EmptyAutocompleteTags{}\";\n      }\n    };\n  }\n\n  public ServiceAndSpanNames serviceAndSpanNames() { // delegates to deprecated methods.\n    final SpanStore delegate = spanStore();\n    return new ServiceAndSpanNames() {\n      @Override public Call<List<String>> getServiceNames() {\n        return delegate.getServiceNames();\n      }\n\n      @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {\n        return Call.emptyList(); // incorrect for not yet ported 3rd party storage components.\n      }\n\n      @Override public Call<List<String>> getSpanNames(String serviceName) {\n        return delegate.getSpanNames(serviceName);\n      }\n\n      @Override public String toString() {\n        return \"ServiceAndSpanNames{\" + delegate + \"}\";\n      }\n    };\n  }\n\n  public abstract SpanConsumer spanConsumer();\n\n  /**\n   * A storage request failed and was dropped due to a limit, resource unavailability, or a timeout.\n   * Implementations of throttling can use this signal to differentiate between failures, for\n   * example to reduce traffic.\n   *\n   * <p>Callers of this method will submit an exception raised by {@link Call#execute()} or on the\n   * error callback of {@link Call#enqueue(Callback)}.\n   *\n   * <p>By default, this returns true if the input is a {@link RejectedExecutionException}. When\n   * originating exceptions, use this type to indicate a load related failure.\n   *\n   * <p>It is generally preferred to specialize this method to handle relevant exceptions for the\n   * particular storage rather than wrapping them in {@link RejectedExecutionException} at call\n   * sites. Extra wrapping can make errors harder to read, for example, by making it harder to\n   * \"google\" a solution for a well known error message for the storage client, instead thinking the\n   * error is in Zipkin code itself.\n   *\n   * <h3>See also</h3>\n   * <p>While implementation is flexible, one known use is <a href=\"https://github.com/Netflix/concurrency-limits\">Netflix\n   * concurrency limits</a>\n   */\n  public boolean isOverCapacity(Throwable e) {\n    return e instanceof RejectedExecutionException;\n  }\n\n  public static abstract class Builder {\n\n    /**\n     * Zipkin supports 64 and 128-bit trace identifiers, typically serialized as 16 or 32 character\n     * hex strings. When false, this setting only considers the low 64-bits (right-most 16\n     * characters) of a trace ID when grouping or retrieving traces. This should be set to false\n     * while some applications issue 128-bit trace IDs and while other truncate them to 64-bit. If\n     * 128-bit trace IDs are not in use, this setting is not required.\n     *\n     * <h3>Details</h3>\n     *\n     * <p>Zipkin historically had 64-bit {@link Span#traceId() trace IDs}, but it now supports 128-\n     * bit trace IDs via 32-character hex representation. While instrumentation update to propagate\n     * 128-bit IDs, it can be ambiguous whether a 64-bit trace ID was sent intentionally, or as an\n     * accident of truncation. This setting allows Zipkin to be usable until application\n     * instrumentation are upgraded to support 128-bit trace IDs.\n     *\n     * <p>Here are a few trace IDs the help explain this setting.\n     *\n     * <pre><ul>\n     *   <li>Trace ID A: 463ac35c9f6413ad48485a3953bb6124</li>\n     *   <li>Trace ID B: 48485a3953bb6124</li>\n     *   <li>Trace ID C: 463ac35c9f6413adf1a48a8cff464e0e</li>\n     *   <li>Trace ID D: 463ac35c9f6413ad</li>\n     * </ul></pre>\n     *\n     * <p>In the above example, Trace ID A and Trace ID B might mean they are in the same trace,\n     * since the lower-64 bits of the IDs are the same. This could happen if a server A created the\n     * trace and propagated it to server B which ran an older tracing library. Server B could have\n     * truncated the trace ID to lower-64 bits. When {@code strictTraceId == false}, spans matching\n     * either trace ID A or B would be returned in the same trace when searching by ID A or B. Spans\n     * with trace ID C or D wouldn't be when searching by ID A or B because trace IDs C and D don't\n     * share lower 64-bits (right-most 16 characters) with trace IDs A or B.\n     *\n     * <p>It is also possible that all servers are capable of handling 128-bit trace identifiers,\n     * but are configured to only send 64-bit ones. In this case, if {@code strictTraceId == false}\n     * trace ID A and B would clash and be put into the same trace, causing confusion. Moreover,\n     * there is overhead associated with indexing spans both by 64 and 128-bit trace IDs. When a\n     * site has finished upgrading to 128-bit trace IDs, they should enable this setting.\n     *\n     * <p>See https://github.com/openzipkin/b3-propagation/issues/6 for the status of\n     * known open source libraries on 128-bit trace identifiers.\n     */\n    public abstract Builder strictTraceId(boolean strictTraceId);\n\n    /**\n     * False is an attempt to disable indexing, leaving only {@link StorageComponent#traces()}\n     * supported. For example, query requests will be disabled.\n     * <p>\n     * The use case is typically to support 100% sampled data, or when traces are searched using\n     * alternative means such as a logging index.\n     *\n     * <p>Refer to implementation docs for the impact of this parameter. Operations that use\n     * indexes should return empty as opposed to throwing an exception.\n     */\n    public abstract Builder searchEnabled(boolean searchEnabled);\n\n    /**\n     * Autocomplete is used by the UI to suggest getValues for site-specific tags, such as\n     * environment names. The getKeys here would appear in {@link Span#tags() span tags}. Good\n     * choices for autocomplete are limited in cardinality for the same reasons as service and span\n     * names.\n     * <p>\n     * For example, \"http.url\" would be a bad choice for autocomplete, not just because it isn't\n     * site-specific (such as environment would be), but also as there are unlimited getValues due\n     * to factors such as unique ids in the path.\n     *\n     * @param keys controls the span values stored for auto-complete.\n     */\n    public Builder autocompleteKeys(List<String> keys) { // not abstract as added later\n      Logger.getLogger(getClass().getName()).info(\"autocompleteKeys not yet supported\");\n      return this;\n    }\n\n    /** How long in milliseconds to suppress calls to write the same autocomplete key/value pair. */\n    public Builder autocompleteTtl(int autocompleteTtl) { // not abstract as added later\n      Logger.getLogger(getClass().getName()).info(\"autocompleteTtl not yet supported\");\n      return this;\n    }\n\n    /** How many autocomplete key/value pairs to suppress at a time. */\n    public Builder autocompleteCardinality(\n      int autocompleteCardinality) { // not abstract as added later\n      Logger.getLogger(getClass().getName()).info(\"autocompleteCardinality not yet supported\");\n      return this;\n    }\n\n    public abstract StorageComponent build();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/StrictTraceId.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.Iterator;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport zipkin2.Call.Mapper;\nimport zipkin2.Span;\nimport zipkin2.internal.FilterTraces;\n\n/**\n * Storage implementation often need to re-check query results when {@link\n * StorageComponent.Builder#strictTraceId(boolean) strict trace ID} is disabled.\n */\npublic final class StrictTraceId {\n\n  public static Mapper<List<Span>, List<Span>> filterSpans(String traceId) {\n    return new FilterSpans(traceId);\n  }\n\n  /**\n   * Filters the mutable input client-side when there's a clash on lower 64-bits of a trace ID.\n   *\n   * @see FilterTraces\n   */\n  public static Mapper<List<List<Span>>, List<List<Span>>> filterTraces(QueryRequest request) {\n    return new FilterTracesIfClashOnLowerTraceId(request);\n  }\n\n  static final class FilterTracesIfClashOnLowerTraceId\n    implements Mapper<List<List<Span>>, List<List<Span>>> {\n    final QueryRequest request;\n\n    FilterTracesIfClashOnLowerTraceId(QueryRequest request) {\n      this.request = request;\n    }\n\n    @Override public List<List<Span>> map(List<List<Span>> input) {\n      if (hasClashOnLowerTraceId(input)) {\n        return FilterTraces.create(request).map(input);\n      }\n      return input;\n    }\n\n    @Override public String toString() {\n      return \"FilterTracesIfClashOnLowerTraceId{request=\" + request + \"}\";\n    }\n  }\n\n  /** Returns true if any trace clashes on the right-most 16 characters of the trace ID */\n  // Concretely, Netflix have a special index template for a multi-tag, \"fit.sessionId\". If we\n  // blindly filtered without seeing if we had to, a match that works on the server side would\n  // fail client side. Normally, we wouldn't special case like this, but not filtering unless\n  // necessary is also more efficient.\n  static boolean hasClashOnLowerTraceId(List<List<Span>> input) {\n    int traceCount = input.size();\n    if (traceCount <= 1) return false;\n\n    // NOTE: It is probably more efficient to do clever sorting and peeking here, but the call site\n    // is query side, which is not in the critical path of user code. A set is much easier to grok.\n    Set<String> traceIdLows = new LinkedHashSet<>();\n    boolean clash = false;\n    for (List<Span> spans : input) {\n      String traceId = lowerTraceId(spans.get(0).traceId());\n      if (!traceIdLows.add(traceId)) {\n        clash = true;\n        break;\n      }\n    }\n    return clash;\n  }\n\n  static String lowerTraceId(String traceId) {\n    return traceId.length() == 16 ? traceId : traceId.substring(16);\n  }\n\n  static final class FilterSpans implements Mapper<List<Span>, List<Span>> {\n    final String traceId;\n\n    FilterSpans(String traceId) {\n      this.traceId = traceId;\n    }\n\n    @Override public List<Span> map(List<Span> input) {\n      Iterator<Span> i = input.iterator();\n      while (i.hasNext()) {\n        Span next = i.next();\n        if (!next.traceId().equals(traceId)) i.remove();\n      }\n      return input;\n    }\n\n    @Override public String toString() {\n      return \"FilterSpans{traceId=\" + traceId + \"}\";\n    }\n  }\n\n  /**\n   * Returns a function that filters its mutable input when it contains a trace not matching the\n   * specified trace IDs.\n   *\n   * <p>Make sure the input IDs are unique and {@link Span#normalizeTraceId(String) normalized}.\n   */\n  public static Mapper<List<List<Span>>, List<List<Span>>> filterTraces(Iterable<String> traceIds) {\n    return new FilterTracesByIds(traceIds);\n  }\n\n  static final class FilterTracesByIds implements Mapper<List<List<Span>>, List<List<Span>>> {\n    final Set<String> traceIds;\n\n    FilterTracesByIds(Iterable<String> sanitizedIds) {\n      traceIds = new LinkedHashSet<>();\n      for (String traceId : sanitizedIds) {\n        traceIds.add(traceId);\n      }\n    }\n\n    @Override\n    public List<List<Span>> map(List<List<Span>> input) {\n      Iterator<List<Span>> i = input.iterator();\n      while (i.hasNext()) { // Not using removeIf as that's java 8+\n        List<Span> next = i.next();\n        if (!traceIds.contains(next.get(0).traceId())) {\n          i.remove();\n        }\n      }\n      return input;\n    }\n\n    @Override\n    public String toString() {\n      return \"FilterTracesByIds{traceIds=\" + traceIds + \"}\";\n    }\n  }\n\n  StrictTraceId() {\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/storage/Traces.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.Span;\n\n/**\n * Allows readback of traces by ID, as written by a {@link SpanConsumer}.\n *\n * <p>Specifically, this provides apis present when {@link StorageComponent.Builder#searchEnabled(boolean)\n * search is disabled}.\n *\n * <p>Note: This is not considered a user-level Api, rather an Spi that can be used to bind\n * user-level abstractions such as futures or observables.\n *\n * @since 2.17\n */\npublic interface Traces {\n  /**\n   * Retrieves spans that share a 128-bit trace id with no ordering expectation or empty if none are\n   * found.\n   *\n   * <p>When strict trace ID is disabled, spans with the same right-most 16 characters are returned\n   * even if the characters to the left are not.\n   *\n   * <p>Implementations should use {@link Span#normalizeTraceId(String)} to ensure consistency.\n   *\n   * @param traceId the {@link Span#traceId() trace ID}\n   */\n  Call<List<Span>> getTrace(String traceId);\n\n  /**\n   * Retrieves any traces with the specified IDs. Results return in any order, and can be empty.\n   *\n   * <p>When strict trace ID is disabled, spans with the same right-most 16 characters are returned\n   * even if the characters to the left are not.\n   *\n   * <p>Implementations should use {@link Span#normalizeTraceId(String)} on each input trace ID to\n   * ensure consistency.\n   *\n   * @param traceIds a list of unique {@link Span#traceId() trace IDs}.\n   * @return traces matching the supplied trace IDs, in any order\n   */\n  Call<List<List<Span>>> getTraces(Iterable<String> traceIds);\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/v1/V1Annotation.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport java.util.Objects;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\n\n/**\n * Like {@link zipkin2.Annotation}, except in v1 format the {@link Span#localEndpoint()} was\n * repeated for each annotation.\n *\n * @deprecated new code should use {@link Annotation}.\n */\n@Deprecated\npublic final class V1Annotation implements Comparable<V1Annotation> {\n\n  // exposed for conversion\n  public static V1Annotation create(long timestamp, String value, @Nullable Endpoint endpoint) {\n    return new V1Annotation(timestamp, value, endpoint);\n  }\n\n  /** Sets {@link Annotation#timestamp()} */\n  public long timestamp() {\n    return timestamp;\n  }\n\n  /** Sets {@link Annotation#value()} */\n  public String value() {\n    return value;\n  }\n\n  /**\n   * The host that reported this annotation or null if unknown.\n   *\n   * <p>In v2 format, this is analogous to {@link Span#localEndpoint()}.\n   */\n  @Nullable\n  public Endpoint endpoint() {\n    return endpoint;\n  }\n\n  final long timestamp;\n  final String value;\n  final Endpoint endpoint;\n\n  V1Annotation(long timestamp, String value, @Nullable Endpoint endpoint) {\n    this.timestamp = timestamp;\n    if (value == null) throw new NullPointerException(\"value == null\");\n    this.value = value;\n    this.endpoint = endpoint;\n  }\n\n  // hashCode and equals implemented as legacy cassandra uses it in a naming convention\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof V1Annotation)) return false;\n    V1Annotation that = (V1Annotation) o;\n    return timestamp == that.timestamp\n        && value.equals(that.value)\n        && Objects.equals(endpoint, that.endpoint);\n  }\n\n  @Override\n  public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (int) (h ^ ((timestamp >>> 32) ^ timestamp));\n    h *= 1000003;\n    h ^= value.hashCode();\n    h *= 1000003;\n    h ^= endpoint == null ? 0 : endpoint.hashCode();\n    return h;\n  }\n\n  /** Compares by {@link #timestamp()}, then {@link #value()}. */\n  @Override\n  public int compareTo(V1Annotation that) {\n    if (this == that) return 0;\n    int byTimestamp = timestamp < that.timestamp ? -1 : timestamp == that.timestamp ? 0 : 1;\n    if (byTimestamp != 0) return byTimestamp;\n    return value.compareTo(that.value);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/v1/V1BinaryAnnotation.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport java.util.Objects;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\n\n/**\n * This only supports binary annotations that map to {@link Span v2 span} data. Namely, this\n * supports {@link Span#tags()}, {@link Span#localEndpoint()} and {@link Span#remoteEndpoint()}.\n *\n * <p>Specifically, this maps String and Boolean binary annotations, ignoring others.\n *\n * @deprecated new code should use {@link Span#tags()}.\n */\n@Deprecated\npublic final class V1BinaryAnnotation implements Comparable<V1BinaryAnnotation> {\n  /** The defined in zipkin's thrift definition */\n  public static final int TYPE_BOOLEAN = 0;\n  /** The type defined in zipkin's thrift definition */\n  public static final int TYPE_STRING = 6;\n\n  /** Creates an address annotation, which is the same as {@link Span#remoteEndpoint()} */\n  public static V1BinaryAnnotation createAddress(String address, Endpoint endpoint) {\n    if (endpoint == null) throw new NullPointerException(\"endpoint == null\");\n    return new V1BinaryAnnotation(address, null, endpoint);\n  }\n\n  /**\n   * Creates a tag annotation, which is the same as {@link Span#tags()} except duplicating the\n   * endpoint.\n   *\n   * <p>A special case is when the key is \"lc\" and value is empty: This substitutes for the {@link\n   * Span#localEndpoint()}.\n   */\n  public static V1BinaryAnnotation createString(String key, String value, Endpoint endpoint) {\n    if (value == null) throw new NullPointerException(\"value == null\");\n    return new V1BinaryAnnotation(key, value, endpoint);\n  }\n\n  /** The same as the key of a {@link Span#tags()} v2 span tag} */\n  public String key() {\n    return key;\n  }\n\n  /**\n   * The thrift type for the value defined in Zipkin's thrift definition. Note this is not the\n   * TBinaryProtocol field type!\n   */\n  public int type() {\n    return type;\n  }\n\n  /** The same as the value of a {@link Span#tags()} v2 span tag} or null if this is an address */\n  @Nullable\n  public String stringValue() {\n    return stringValue;\n  }\n\n  /**\n   * When {@link #stringValue()} is present, this is the same as the {@link Span#localEndpoint()}\n   * Otherwise, it is the same as the {@link Span#remoteEndpoint()}.\n   */\n  public Endpoint endpoint() {\n    return endpoint;\n  }\n\n  final String key, stringValue;\n  final int type;\n  final Endpoint endpoint;\n\n  V1BinaryAnnotation(String key, String stringValue, Endpoint endpoint) {\n    if (key == null) throw new NullPointerException(\"key == null\");\n    this.key = key;\n    this.stringValue = stringValue;\n    this.type = stringValue != null ? TYPE_STRING : TYPE_BOOLEAN;\n    this.endpoint = endpoint;\n  }\n\n  // hashCode and equals implemented as legacy cassandra uses it in a naming convention\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof V1BinaryAnnotation)) return false;\n    V1BinaryAnnotation that = (V1BinaryAnnotation) o;\n    return key.equals(that.key)\n        && Objects.equals(stringValue, that.stringValue)\n        && Objects.equals(endpoint, that.endpoint);\n  }\n\n  @Override\n  public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= key.hashCode();\n    h *= 1000003;\n    h ^= stringValue == null ? 0 : stringValue.hashCode();\n    h *= 1000003;\n    h ^= endpoint == null ? 0 : endpoint.hashCode();\n    return h;\n  }\n\n  /** Provides consistent iteration by {@link #key} */\n  @Override\n  public int compareTo(V1BinaryAnnotation that) {\n    if (this == that) return 0;\n    return key.compareTo(that.key);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/v1/V1Span.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\n\nimport static java.util.Collections.unmodifiableList;\nimport static zipkin2.internal.HexCodec.lowerHexToUnsignedLong;\n\n/**\n * V1 spans are different from v2 especially as annotations repeat. Support is available to help\n * migrate old code or allow for parsing older data formats.\n *\n * @deprecated new code should use {@link Span}.\n */\n@Deprecated\npublic final class V1Span {\n  static final Endpoint EMPTY_ENDPOINT = Endpoint.newBuilder().build();\n\n  /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */\n  public long traceIdHigh() {\n    return traceIdHigh;\n  }\n\n  /** lower 64-bits of the {@link Span#traceId()} */\n  public long traceId() {\n    return traceId;\n  }\n\n  /** Same as {@link zipkin2.Span#id()} except packed into a long. Zero means root span. */\n  public long id() {\n    return id;\n  }\n\n  /** Same as {@link zipkin2.Span#name()} */\n  public String name() {\n    return name;\n  }\n\n  /** The parent's {@link #id()} or zero if this the root span in a trace. */\n  public long parentId() {\n    return parentId;\n  }\n\n  /** Same as {@link Span#timestampAsLong()} */\n  public long timestamp() {\n    return timestamp;\n  }\n\n  /** Same as {@link Span#durationAsLong()} */\n  public long duration() {\n    return duration;\n  }\n\n  /**\n   * Same as {@link Span#annotations()}, except each may be associated with {@link\n   * Span#localEndpoint()}\n   */\n  public List<V1Annotation> annotations() {\n    return annotations;\n  }\n\n  /**\n   * {@link Span#tags()} are allocated to binary annotations with a {@link\n   * V1BinaryAnnotation#stringValue()}. {@link Span#remoteEndpoint()} to those without.\n   */\n  public List<V1BinaryAnnotation> binaryAnnotations() {\n    return binaryAnnotations;\n  }\n\n  /** Same as {@link Span#debug()} */\n  public Boolean debug() {\n    return debug;\n  }\n\n  final long traceIdHigh, traceId, id;\n  final String name;\n  final long parentId, timestamp, duration;\n  final List<V1Annotation> annotations;\n  final List<V1BinaryAnnotation> binaryAnnotations;\n  final Boolean debug;\n\n  V1Span(Builder builder) {\n    if (builder.traceId == 0L) throw new IllegalArgumentException(\"traceId == 0\");\n    if (builder.id == 0L) throw new IllegalArgumentException(\"id == 0\");\n    this.traceId = builder.traceId;\n    this.traceIdHigh = builder.traceIdHigh;\n    this.name = builder.name;\n    this.id = builder.id;\n    this.parentId = builder.parentId;\n    this.timestamp = builder.timestamp;\n    this.duration = builder.duration;\n    this.annotations = sortedList(builder.annotations);\n    this.binaryAnnotations = sortedList(builder.binaryAnnotations);\n    this.debug = builder.debug;\n  }\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder {\n    // ID accessors are here to help organize builders by their identifiers\n\n    /** Sets {@link V1Span#traceIdHigh()} */\n    public long traceIdHigh() {\n      return traceIdHigh;\n    }\n\n    /** Sets {@link V1Span#traceId()} */\n    public long traceId() {\n      return traceId;\n    }\n\n    /** Sets {@link V1Span#id()} */\n    public long id() {\n      return id;\n    }\n\n    long traceIdHigh, traceId, parentId, id;\n    String name;\n    long timestamp, duration;\n    ArrayList<V1Annotation> annotations;\n    ArrayList<V1BinaryAnnotation> binaryAnnotations;\n    Boolean debug;\n\n    Builder() {\n    }\n\n    public Builder clear() {\n      traceId = traceIdHigh = id = 0;\n      name = null;\n      parentId = timestamp = duration = 0;\n      if (annotations != null) annotations.clear();\n      if (binaryAnnotations != null) binaryAnnotations.clear();\n      debug = null;\n      return this;\n    }\n\n    /** Same as {@link Span.Builder#traceId(String)} */\n    public Builder traceId(String traceId) {\n      if (traceId == null) throw new NullPointerException(\"traceId == null\");\n      if (traceId.length() == 32) {\n        traceIdHigh = lowerHexToUnsignedLong(traceId, 0);\n      }\n      this.traceId = lowerHexToUnsignedLong(traceId);\n      return this;\n    }\n\n    /** Sets {@link V1Span#traceId()} */\n    public Builder traceId(long traceId) {\n      this.traceId = traceId;\n      return this;\n    }\n\n    /** Sets {@link V1Span#traceIdHigh()} */\n    public Builder traceIdHigh(long traceIdHigh) {\n      this.traceIdHigh = traceIdHigh;\n      return this;\n    }\n\n    /** Sets {@link V1Span#id()} */\n    public Builder id(long id) {\n      this.id = id;\n      return this;\n    }\n\n    /** Same as {@link Span.Builder#id(String)} */\n    public Builder id(String id) {\n      if (id == null) throw new NullPointerException(\"id == null\");\n      this.id = lowerHexToUnsignedLong(id);\n      return this;\n    }\n\n    /** Same as {@link Span.Builder#parentId(String)} */\n    public Builder parentId(String parentId) {\n      this.parentId = parentId != null ? lowerHexToUnsignedLong(parentId) : 0L;\n      return this;\n    }\n\n    /** Sets {@link V1Span#parentId()} */\n    public Builder parentId(long parentId) {\n      this.parentId = parentId;\n      return this;\n    }\n\n    /** Sets {@link V1Span#name()} */\n    public Builder name(String name) {\n      this.name = name == null || name.isEmpty() ? null : name.toLowerCase(Locale.ROOT);\n      return this;\n    }\n\n    /** Sets {@link V1Span#timestamp()} */\n    public Builder timestamp(long timestamp) {\n      this.timestamp = timestamp;\n      return this;\n    }\n\n    /** Sets {@link V1Span#duration()} */\n    public Builder duration(long duration) {\n      this.duration = duration;\n      return this;\n    }\n\n    /** Sets {@link V1Span#annotations()} */\n    public Builder addAnnotation(long timestamp, String value, @Nullable Endpoint endpoint) {\n      if (annotations == null) annotations = new ArrayList<>(4);\n      if (EMPTY_ENDPOINT.equals(endpoint)) endpoint = null;\n      annotations.add(new V1Annotation(timestamp, value, endpoint));\n      return this;\n    }\n\n    /** Creates an address annotation, which is the same as {@link Span#remoteEndpoint()} */\n    public Builder addBinaryAnnotation(String address, Endpoint endpoint) {\n      // Ignore empty endpoints rather than crashing v1 parsers on bad address data\n      if (endpoint == null || EMPTY_ENDPOINT.equals(endpoint)) return this;\n\n      if (binaryAnnotations == null) binaryAnnotations = new ArrayList<>(4);\n      binaryAnnotations.add(new V1BinaryAnnotation(address, null, endpoint));\n      return this;\n    }\n\n    /**\n     * Creates a tag annotation, which is the same as {@link Span#tags()} except duplicating the\n     * endpoint.\n     *\n     * <p>A key of \"lc\" and empty value substitutes for {@link Span#localEndpoint()}.\n     */\n    public Builder addBinaryAnnotation(String key, String value, Endpoint endpoint) {\n      if (value == null) throw new NullPointerException(\"value == null\");\n      if (EMPTY_ENDPOINT.equals(endpoint)) endpoint = null;\n      if (binaryAnnotations == null) binaryAnnotations = new ArrayList<>(4);\n      binaryAnnotations.add(new V1BinaryAnnotation(key, value, endpoint));\n      return this;\n    }\n\n    /** Sets {@link V1Span#debug()} */\n    public Builder debug(@Nullable Boolean debug) {\n      this.debug = debug;\n      return this;\n    }\n\n    public V1Span build() {\n      return new V1Span(this);\n    }\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof V1Span)) return false;\n    V1Span that = (V1Span) o;\n    return traceIdHigh == that.traceIdHigh\n        && traceId == that.traceId\n        && Objects.equals(name, that.name)\n        && id == that.id\n        && parentId == that.parentId\n        && timestamp == that.timestamp\n        && duration == that.duration\n        && annotations.equals(that.annotations)\n        && binaryAnnotations.equals(that.binaryAnnotations)\n        && Objects.equals(debug, that.debug);\n  }\n\n  @Override\n  public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (int) (h ^ ((traceIdHigh >>> 32) ^ traceIdHigh));\n    h *= 1000003;\n    h ^= (int) (h ^ ((traceId >>> 32) ^ traceId));\n    h *= 1000003;\n    h ^= name == null ? 0 : name.hashCode();\n    h *= 1000003;\n    h ^= (int) (h ^ ((id >>> 32) ^ id));\n    h *= 1000003;\n    h ^= (int) (h ^ ((parentId >>> 32) ^ parentId));\n    h *= 1000003;\n    h ^= (int) (h ^ ((timestamp >>> 32) ^ timestamp));\n    h *= 1000003;\n    h ^= (int) (h ^ ((duration >>> 32) ^ duration));\n    h *= 1000003;\n    h ^= annotations.hashCode();\n    h *= 1000003;\n    h ^= binaryAnnotations.hashCode();\n    h *= 1000003;\n    h ^= debug == null ? 0 : debug.hashCode();\n    return h;\n  }\n\n  static <T extends Comparable<T>> List<T> sortedList(List<T> input) {\n    if (input == null) return Collections.emptyList();\n    Collections.sort(input);\n    return unmodifiableList(new ArrayList<>(input));\n  }\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/v1/V1SpanConverter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Objects;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\nimport zipkin2.internal.Nullable;\n\n/**\n * Allows you to split a v1 span when necessary. This can be the case when reading merged\n * client+server spans from storage or parsing old data formats.\n *\n * <p>This type isn't thread-safe: it re-uses state to avoid re-allocations in conversion loops.\n */\npublic final class V1SpanConverter {\n  public static V1SpanConverter create() {\n    return new V1SpanConverter();\n  }\n\n  final Span.Builder first = Span.newBuilder();\n  final List<Span.Builder> spans = new ArrayList<>();\n  V1Annotation cs, sr, ss, cr, ms, mr, ws, wr;\n\n  public List<Span> convert(V1Span source) {\n    List<Span> out = new ArrayList<>();\n    convert(source, out);\n    return out;\n  }\n\n  public void convert(V1Span source, Collection<Span> sink) {\n    start(source);\n    // add annotations unless they are \"core\"\n    processAnnotations(source);\n    // convert binary annotations to tags and addresses\n    processBinaryAnnotations(source);\n    finish(sink);\n  }\n\n  void start(V1Span source) {\n    first.clear();\n    spans.clear();\n    cs = sr = ss = cr = ms = mr = ws = wr = null;\n    newBuilder(first, source);\n  }\n\n  void processAnnotations(V1Span source) {\n    for (int i = 0, length = source.annotations.size(); i < length; i++) {\n      V1Annotation a = source.annotations.get(i);\n      Span.Builder currentSpan = forEndpoint(source, a.endpoint);\n      // core annotations require an endpoint. Don't give special treatment when that's missing\n      if (a.value.length() == 2 && a.endpoint != null) {\n        if (a.value.equals(\"cs\")) {\n          currentSpan.kind(Kind.CLIENT);\n          cs = a;\n        } else if (a.value.equals(\"sr\")) {\n          currentSpan.kind(Kind.SERVER);\n          sr = a;\n        } else if (a.value.equals(\"ss\")) {\n          currentSpan.kind(Kind.SERVER);\n          ss = a;\n        } else if (a.value.equals(\"cr\")) {\n          currentSpan.kind(Kind.CLIENT);\n          cr = a;\n        } else if (a.value.equals(\"ms\")) {\n          currentSpan.kind(Kind.PRODUCER);\n          ms = a;\n        } else if (a.value.equals(\"mr\")) {\n          currentSpan.kind(Kind.CONSUMER);\n          mr = a;\n        } else if (a.value.equals(\"ws\")) {\n          ws = a;\n        } else if (a.value.equals(\"wr\")) {\n          wr = a;\n        } else {\n          currentSpan.addAnnotation(a.timestamp, a.value);\n        }\n      } else {\n        currentSpan.addAnnotation(a.timestamp, a.value);\n      }\n    }\n\n    // When bridging between event and span model, you can end up missing a start annotation\n    if (cs == null && endTimestampReflectsSpanDuration(cr, source)) {\n      cs = V1Annotation.create(source.timestamp, \"cs\", cr.endpoint);\n    }\n    if (sr == null && endTimestampReflectsSpanDuration(ss, source)) {\n      sr = V1Annotation.create(source.timestamp, \"sr\", ss.endpoint);\n    }\n\n    if (cs != null && sr != null) {\n      // in a shared span, the client side owns span duration by annotations or explicit timestamp\n      maybeTimestampDuration(source, cs, cr);\n\n      // special-case loopback: We need to make sure on loopback there are two span2s\n      Span.Builder client = forEndpoint(source, cs.endpoint);\n      Span.Builder server;\n      if (hasSameServiceName(cs.endpoint, sr.endpoint)) {\n        client.kind(Kind.CLIENT);\n        // fork a new span for the server side\n        server = newSpanBuilder(source, sr.endpoint).kind(Kind.SERVER);\n      } else {\n        server = forEndpoint(source, sr.endpoint);\n      }\n\n      // the server side is smaller than that, we have to read annotations to find out\n      server.shared(true).timestamp(sr.timestamp);\n      if (ss != null) server.duration(ss.timestamp - sr.timestamp);\n      if (cr == null && source.duration == 0) client.duration(null); // one-way has no duration\n    } else if (cs != null && cr != null) {\n      maybeTimestampDuration(source, cs, cr);\n    } else if (sr != null && ss != null) {\n      maybeTimestampDuration(source, sr, ss);\n    } else { // otherwise, the span is incomplete. revert special-casing\n      handleIncompleteRpc(source);\n    }\n\n    // Span v1 format did not have a shared flag. By convention, span.timestamp being absent\n    // implied shared. When we only see the server-side, carry this signal over.\n    if (cs == null && sr != null &&\n      // We use a signal of either the authoritative timestamp being unset, or the duration is unset\n      // eventhough we have the server send result. The latter clarifies an edge case in MySQL\n      // where a span row is shared between client and server. The presence of timestamp in this\n      // case could be due to the client-side of that RPC.\n      (source.timestamp == 0 || (ss != null && source.duration == 0))) {\n      forEndpoint(source, sr.endpoint).shared(true);\n    }\n\n    // ms and mr are not supposed to be in the same span, but in case they are..\n    if (ms != null && mr != null) {\n      // special-case loopback: We need to make sure on loopback there are two span2s\n      Span.Builder producer = forEndpoint(source, ms.endpoint);\n      Span.Builder consumer;\n      if (hasSameServiceName(ms.endpoint, mr.endpoint)) {\n        producer.kind(Kind.PRODUCER);\n        // fork a new span for the consumer side\n        consumer = newSpanBuilder(source, mr.endpoint).kind(Kind.CONSUMER);\n      } else {\n        consumer = forEndpoint(source, mr.endpoint);\n      }\n\n      consumer.shared(true);\n      if (wr != null) {\n        consumer.timestamp(wr.timestamp).duration(mr.timestamp - wr.timestamp);\n      } else {\n        consumer.timestamp(mr.timestamp);\n      }\n\n      producer.timestamp(ms.timestamp).duration(ws != null ? ws.timestamp - ms.timestamp : null);\n    } else if (ms != null) {\n      maybeTimestampDuration(source, ms, ws);\n    } else if (mr != null) {\n      if (wr != null) {\n        maybeTimestampDuration(source, wr, mr);\n      } else {\n        maybeTimestampDuration(source, mr, null);\n      }\n    } else {\n      if (ws != null) forEndpoint(source, ws.endpoint).addAnnotation(ws.timestamp, ws.value);\n      if (wr != null) forEndpoint(source, wr.endpoint).addAnnotation(wr.timestamp, wr.value);\n    }\n  }\n\n  void handleIncompleteRpc(V1Span source) {\n    handleIncompleteRpc(first);\n    for (Span.Builder span : spans) {\n      handleIncompleteRpc(span);\n    }\n    if (source.timestamp != 0) {\n      first.timestamp(source.timestamp).duration(source.duration);\n    }\n  }\n\n  void handleIncompleteRpc(Span.Builder next) {\n    if (Kind.CLIENT.equals(next.kind())) {\n      if (cs != null) next.timestamp(cs.timestamp);\n      if (cr != null) next.addAnnotation(cr.timestamp, cr.value);\n    } else if (Kind.SERVER.equals(next.kind())) {\n      if (sr != null) next.timestamp(sr.timestamp);\n      if (ss != null) next.addAnnotation(ss.timestamp, ss.value);\n    }\n  }\n\n  static boolean endTimestampReflectsSpanDuration(V1Annotation end, V1Span source) {\n    return end != null\n        && source.timestamp != 0\n        && source.duration != 0\n        && source.timestamp + source.duration == end.timestamp;\n  }\n\n  void maybeTimestampDuration(V1Span source, V1Annotation begin, @Nullable V1Annotation end) {\n    Span.Builder span2 = forEndpoint(source, begin.endpoint);\n    if (source.timestamp != 0 && source.duration != 0) {\n      span2.timestamp(source.timestamp).duration(source.duration);\n    } else {\n      span2.timestamp(begin.timestamp);\n      if (end != null) span2.duration(end.timestamp - begin.timestamp);\n    }\n  }\n\n  void processBinaryAnnotations(V1Span source) {\n    zipkin2.Endpoint ca = null, sa = null, ma = null;\n    for (int i = 0, length = source.binaryAnnotations.size(); i < length; i++) {\n      V1BinaryAnnotation b = source.binaryAnnotations.get(i);\n\n      // Peek to see if this is an address annotation. Strictly speaking, address annotations should\n      // have a value of true (not \"true\" or \"1\"). However, there are versions of zipkin-ruby in the\n      // wild that create \"1\" and misinterpreting confuses the question of what is the local\n      // endpoint. Hence, we leniently parse.\n      if (\"ca\".equals(b.key)) {\n        ca = b.endpoint;\n        continue;\n      } else if (\"sa\".equals(b.key)) {\n        sa = b.endpoint;\n        continue;\n      } else if (\"ma\".equals(b.key)) {\n        ma = b.endpoint;\n        continue;\n      }\n\n      Span.Builder currentSpan = forEndpoint(source, b.endpoint);\n\n      // don't add marker \"lc\" tags\n      if (\"lc\".equals(b.key) && b.stringValue.isEmpty()) continue;\n      currentSpan.putTag(b.key, b.stringValue);\n    }\n\n    boolean noCoreAnnotations = cs == null && cr == null && ss == null && sr == null;\n    // special-case when we are missing core annotations, but we have both address annotations\n    if (noCoreAnnotations && (ca != null || sa != null)) {\n      if (ca != null && sa != null) {\n        forEndpoint(source, ca).remoteEndpoint(sa);\n      } else if (sa != null) {\n        // \"sa\" is a default for a remote address, don't make it a client span\n        forEndpoint(source, null).remoteEndpoint(sa);\n      } else { // ca != null: treat it like a server\n        forEndpoint(source, null).kind(Kind.SERVER).remoteEndpoint(ca);\n      }\n      return;\n    }\n\n    V1Annotation server = sr != null ? sr : ss;\n    if (ca != null && server != null && !ca.equals(server.endpoint)) {\n      // Finagle adds a \"ca\" annotation on server spans for the client-port on the socket, but with\n      // the same service name as \"sa\". Removing the service name prevents creating loopback links.\n      if (hasSameServiceName(ca, server.endpoint)) {\n        ca = ca.toBuilder().serviceName(null).build();\n      }\n      forEndpoint(source, server.endpoint).remoteEndpoint(ca);\n    }\n    if (sa != null) { // client span\n      if (cs != null) {\n        forEndpoint(source, cs.endpoint).remoteEndpoint(sa);\n      } else if (cr != null) {\n        forEndpoint(source, cr.endpoint).remoteEndpoint(sa);\n      }\n    }\n    if (ma != null) { // messaging span\n      // Intentionally process messaging endpoints separately in case someone accidentally shared\n      // a messaging span. This will ensure both sides have the address of the broker.\n      if (ms != null) forEndpoint(source, ms.endpoint).remoteEndpoint(ma);\n      if (mr != null) forEndpoint(source, mr.endpoint).remoteEndpoint(ma);\n    }\n  }\n\n  Span.Builder forEndpoint(V1Span source, @Nullable zipkin2.Endpoint e) {\n    if (e == null) return first; // allocate missing endpoint data to first span\n    if (closeEnoughEndpoint(first, e)) return first;\n    for (Span.Builder next : spans) {\n      if (closeEnoughEndpoint(next, e)) return next;\n    }\n    return newSpanBuilder(source, e);\n  }\n\n  static boolean closeEnoughEndpoint(Span.Builder builder, Endpoint e) {\n    Endpoint localEndpoint = builder.localEndpoint();\n    if (localEndpoint == null) {\n      builder.localEndpoint(e);\n      return true;\n    }\n    return hasSameServiceName(localEndpoint, e);\n  }\n\n  Span.Builder newSpanBuilder(V1Span source, Endpoint e) {\n    Span.Builder result = newBuilder(Span.newBuilder(), source).localEndpoint(e);\n    spans.add(result);\n    return result;\n  }\n\n  void finish(Collection<Span> sink) {\n    sink.add(first.build());\n    for (Span.Builder span : spans) {\n      sink.add(span.build());\n    }\n  }\n\n  static boolean hasSameServiceName(Endpoint left, @Nullable Endpoint right) {\n    return Objects.equals(left.serviceName(), right.serviceName());\n  }\n\n  static Span.Builder newBuilder(Span.Builder builder, V1Span source) {\n    return builder\n        .traceId(source.traceIdHigh, source.traceId)\n        .parentId(source.parentId)\n        .id(source.id)\n        .name(source.name)\n        .debug(source.debug);\n  }\n\n  V1SpanConverter() {}\n}\n"
  },
  {
    "path": "zipkin/src/main/java/zipkin2/v1/V2SpanConverter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport java.util.Map;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\n/**\n * Allows you convert a v2 span into a v1 span. This is helpful for legacy storage which still use\n * annotations. This shouldn't be used by new code.\n *\n * <p>This type isn't thread-safe: it re-uses state to avoid re-allocations in conversion loops.\n */\npublic final class V2SpanConverter {\n\n  public static V2SpanConverter create() {\n    return new V2SpanConverter();\n  }\n\n  final V1Span.Builder result = V1Span.newBuilder();\n  final V1SpanMetadata md = new V1SpanMetadata();\n\n  public V1Span convert(Span value) {\n    md.parse(value);\n    result\n        .clear()\n        .traceId(value.traceId())\n        .parentId(value.parentId())\n        .id(value.id())\n        .name(value.name())\n        .debug(value.debug());\n\n    // Don't report timestamp and duration on shared spans (should be server, but not necessarily)\n    if (!Boolean.TRUE.equals(value.shared())) {\n      result.timestamp(value.timestampAsLong());\n      result.duration(value.durationAsLong());\n    }\n\n    boolean beginAnnotation = md.startTs != 0L && md.begin != null;\n    boolean endAnnotation = md.endTs != 0L && md.end != null;\n    Endpoint ep = value.localEndpoint();\n    int annotationCount = value.annotations().size();\n    if (beginAnnotation) {\n      annotationCount++;\n      result.addAnnotation(md.startTs, md.begin, ep);\n    }\n    for (int i = 0, length = value.annotations().size(); i < length; i++) {\n      Annotation a = value.annotations().get(i);\n      if (beginAnnotation && a.value().equals(md.begin)) continue;\n      if (endAnnotation && a.value().equals(md.end)) continue;\n      result.addAnnotation(a.timestamp(), a.value(), ep);\n    }\n    if (endAnnotation) {\n      annotationCount++;\n      result.addAnnotation(md.endTs, md.end, ep);\n    }\n\n    for (Map.Entry<String, String> b : value.tags().entrySet()) {\n      result.addBinaryAnnotation(b.getKey(), b.getValue(), ep);\n    }\n\n    boolean writeLocalComponent = annotationCount == 0 && ep != null && value.tags().isEmpty();\n    boolean hasRemoteEndpoint = md.addr != null && value.remoteEndpoint() != null;\n\n    // write an empty \"lc\" annotation to avoid missing the localEndpoint in an in-process span\n    if (writeLocalComponent) result.addBinaryAnnotation(\"lc\", \"\", ep);\n    if (hasRemoteEndpoint) result.addBinaryAnnotation(md.addr, value.remoteEndpoint());\n    return result.build();\n  }\n\n  static final class V1SpanMetadata {\n    long startTs, endTs, msTs, wsTs, wrTs, mrTs;\n    String begin, end, addr;\n\n    void parse(Span in) {\n      endTs = msTs = wsTs = wrTs = mrTs = 0L;\n      begin = end = addr = null;\n\n      startTs = in.timestampAsLong();\n      endTs = startTs != 0L && in.durationAsLong() != 0L ? startTs + in.durationAsLong() : 0L;\n\n      Span.Kind kind = in.kind();\n\n      // scan annotations in case there are better timestamps, or inferred kind\n      for (int i = 0, length = in.annotations().size(); i < length; i++) {\n        Annotation a = in.annotations().get(i);\n        String value = a.value();\n        if (value.length() != 2) continue;\n\n        if (value.equals(\"cs\")) {\n          kind = Span.Kind.CLIENT;\n          if (a.timestamp() < startTs) startTs = a.timestamp();\n        } else if (value.equals(\"sr\")) {\n          kind = Span.Kind.SERVER;\n          if (a.timestamp() < startTs) startTs = a.timestamp();\n        } else if (value.equals(\"ss\")) {\n          kind = Span.Kind.SERVER;\n          if (a.timestamp() > endTs) endTs = a.timestamp();\n        } else if (value.equals(\"cr\")) {\n          kind = Span.Kind.CLIENT;\n          if (a.timestamp() > endTs) endTs = a.timestamp();\n        } else if (value.equals(\"ms\")) {\n          kind = Span.Kind.PRODUCER;\n          msTs = a.timestamp();\n        } else if (value.equals(\"mr\")) {\n          kind = Span.Kind.CONSUMER;\n          mrTs = a.timestamp();\n        } else if (value.equals(\"ws\")) {\n          wsTs = a.timestamp();\n        } else if (value.equals(\"wr\")) {\n          wrTs = a.timestamp();\n        }\n      }\n\n      if (in.remoteEndpoint() != null) addr = \"sa\"; // default value\n\n      if (kind == null) return;\n\n      switch (kind) {\n        case CLIENT:\n          addr = \"sa\";\n          begin = \"cs\";\n          end = \"cr\";\n          break;\n        case SERVER:\n          addr = \"ca\";\n          begin = \"sr\";\n          end = \"ss\";\n          break;\n        case PRODUCER:\n          addr = \"ma\";\n          begin = \"ms\";\n          end = \"ws\";\n          if (startTs == 0L || (msTs != 0 && msTs < startTs)) {\n            startTs = msTs;\n          }\n          if (endTs == 0L || (wsTs != 0 && wsTs > endTs)) {\n            endTs = wsTs;\n          }\n          break;\n        case CONSUMER:\n          addr = \"ma\";\n          if (startTs == 0L || (wrTs != 0 && wrTs < startTs)) {\n            startTs = wrTs;\n          }\n          if (endTs == 0L || (mrTs != 0 && mrTs > endTs)) {\n            endTs = mrTs;\n          }\n          if (endTs != 0L || wrTs != 0) {\n            begin = \"wr\";\n            end = \"mr\";\n          } else {\n            begin = \"mr\";\n          }\n          break;\n        default:\n          throw new AssertionError(\"update kind mapping\");\n      }\n\n      // If we didn't find a span kind, directly or indirectly, unset the addr\n      if (in.remoteEndpoint() == null) addr = null;\n    }\n  }\n\n  V2SpanConverter() {}\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/AnnotationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nclass AnnotationTest {\n\n  @Test void messageWhenMissingValue() {\n    Throwable exception = assertThrows(NullPointerException.class, () -> {\n\n      Annotation.create(1L, null);\n    });\n    assertThat(exception.getMessage()).contains(\"value\");\n  }\n\n  @Test void toString_isNice() {\n    assertThat(Annotation.create(1L, \"foo\"))\n      .hasToString(\"Annotation{timestamp=1, value=foo}\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/CallTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.NoSuchElementException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport zipkin2.internal.Nullable;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.isNull;\nimport static org.mockito.Mockito.verify;\n\n@ExtendWith(MockitoExtension.class)\nclass CallTest {\n\n  @Mock Callback callback;\n\n  @Test void constant_execute() throws Exception {\n    Call<String> call = Call.create(\"foo\");\n\n    assertThat(call.execute())\n      .isEqualTo(\"foo\");\n  }\n\n  @Test void constant_submit() {\n    Call<String> call = Call.create(\"foo\");\n\n    call.enqueue(callback);\n\n    verify(callback).onSuccess(\"foo\");\n  }\n\n  @Test void constant_execute_null() throws Exception {\n    Call<Void> call = Call.create(null);\n\n    assertThat(call.execute()).isNull();\n  }\n\n  @Test void constant_submit_null() {\n    Call<Void> call = Call.create(null);\n\n    call.enqueue(callback);\n\n    verify(callback).onSuccess(isNull());\n  }\n\n  @Test void constant_submit_cancel() {\n    Call<Void> call = Call.create(null);\n    call.cancel();\n\n    assertThat(call.isCanceled()).isTrue();\n\n    call.enqueue(callback);\n\n    verify(callback).onError(isA(IOException.class));\n  }\n\n  @Test void executesOnce() throws Exception {\n    Call<Void> call = Call.create(null);\n    call.execute();\n\n    assertThatThrownBy(() -> call.execute())\n      .isInstanceOf(IllegalStateException.class);\n\n    assertThatThrownBy(() -> call.enqueue(callback))\n      .isInstanceOf(IllegalStateException.class);\n  }\n\n  @Test void enqueuesOnce() {\n    Call<Void> call = Call.create(null);\n    call.enqueue(callback);\n\n    assertThatThrownBy(() -> call.enqueue(callback))\n      .isInstanceOf(IllegalStateException.class);\n\n    assertThatThrownBy(() -> call.execute())\n      .isInstanceOf(IllegalStateException.class);\n  }\n\n  @Test\n  @Timeout(1000L)\n  void concurrent_executesOrSubmitsOnce() throws InterruptedException {\n    Call<Void> call = Call.create(null);\n\n    int tryCount = 100;\n\n    AtomicInteger executeOrSubmit = new AtomicInteger();\n\n    callback = new Callback() {\n      @Override public void onSuccess(@Nullable Object value) {\n        executeOrSubmit.incrementAndGet();\n      }\n\n      @Override public void onError(Throwable t) {\n      }\n    };\n\n    ExecutorService exec = Executors.newFixedThreadPool(10);\n    List<Runnable> tries = new ArrayList<>(tryCount);\n    for (int i = 0; i < tryCount; i++) {\n      tries.add(i % 2 == 0 ? () -> {\n        try {\n          call.execute();\n          executeOrSubmit.incrementAndGet();\n        } catch (Exception e) {\n        }\n      } : () -> call.enqueue(callback));\n    }\n\n    tries.forEach(exec::execute);\n    exec.shutdown();\n    exec.awaitTermination(1, TimeUnit.SECONDS);\n\n    assertThat(executeOrSubmit.get()).isEqualTo(1);\n  }\n\n  @Test void constantEqualsConstant() {\n    assertThat(Call.create(null))\n      .isEqualTo(Call.create(null));\n  }\n\n  @Test void emptyList() throws Exception {\n    Call<List<String>> call = Call.emptyList();\n\n    assertThat(call.execute()).isEmpty();\n  }\n\n  @Test void emptyList_independentInstances() {\n    assertThat(Call.emptyList())\n      .isNotSameAs(Call.emptyList());\n  }\n\n  @Test void map_execute() throws Exception {\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> fooBarCall = fooCall.map(foo -> {\n      assertThat(foo).isEqualTo(\"foo\");\n      return \"bar\";\n    });\n\n    assertThat(fooBarCall.execute())\n      .isEqualTo(\"bar\");\n  }\n\n  @Test void map_enqueue() {\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> fooBarCall = fooCall.map(foo -> \"bar\");\n\n    fooBarCall.enqueue(callback);\n\n    verify(callback).onSuccess(\"bar\");\n  }\n\n  @Test void map_enqueue_mappingException() {\n    IllegalArgumentException error = new IllegalArgumentException();\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> fooBarCall = fooCall.map(foo -> {\n      throw error;\n    });\n\n    fooBarCall.enqueue(callback);\n\n    verify(callback).onError(error);\n  }\n\n  @Test void flatMap_execute() throws Exception {\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> barCall = Call.create(\"bar\");\n    Call<String> fooBarCall = fooCall.flatMap(foo -> {\n      assertThat(foo).isEqualTo(\"foo\");\n      return barCall;\n    });\n\n    assertThat(fooBarCall.execute())\n      .isEqualTo(\"bar\");\n  }\n\n  @Test void flatMap_enqueue() {\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> barCall = Call.create(\"bar\");\n    Call<String> fooBarCall = fooCall.flatMap(foo -> barCall);\n\n    fooBarCall.enqueue(callback);\n\n    verify(callback).onSuccess(\"bar\");\n  }\n\n  @Test void flatMap_enqueue_mappingException() {\n    IllegalArgumentException error = new IllegalArgumentException();\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> fooBarCall = fooCall.flatMap(foo -> {\n      assertThat(foo).isEqualTo(\"foo\");\n      throw error;\n    });\n\n    fooBarCall.enqueue(callback);\n\n    verify(callback).onError(error);\n  }\n\n  @Test void flatMap_enqueue_callException() {\n    IllegalArgumentException error = new IllegalArgumentException();\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> exceptionCall = errorCall(error);\n\n    Call<String> fooBarCall = fooCall.flatMap(foo -> exceptionCall);\n\n    fooBarCall.enqueue(callback);\n\n    verify(callback).onError(error);\n  }\n\n  @Test void flatMap_cancelPropagates() throws Exception {\n    Call<String> fooCall = Call.create(\"foo\");\n    Call<String> barCall = Call.create(\"bar\");\n    Call<String> fooBarCall = fooCall.flatMap(foo -> barCall);\n\n    fooBarCall.execute(); // to instantiate the chain.\n    fooBarCall.cancel();\n\n    assertThat(fooBarCall.isCanceled()).isTrue();\n    assertThat(fooCall.isCanceled()).isTrue();\n    assertThat(barCall.isCanceled()).isTrue();\n  }\n\n  @Test void onErrorReturn_execute_onError() throws Exception {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      IllegalArgumentException exception = new IllegalArgumentException();\n      Call<String> errorCall = errorCall(exception);\n\n      Call<String> resolvedCall = errorCall.handleError(\n        (error, callback) -> callback.onError(error)\n      );\n\n      resolvedCall.execute();\n    });\n  }\n\n  @Test void onErrorReturn_execute_onSuccess() throws Exception {\n    IllegalArgumentException exception = new IllegalArgumentException();\n    Call<String> errorCall = errorCall(exception);\n\n    Call<String> resolvedCall = errorCall.handleError(\n      (error, callback) -> callback.onSuccess(\"foo\")\n    );\n\n    assertThat(resolvedCall.execute())\n      .isEqualTo(\"foo\");\n  }\n\n  @Test void onErrorReturn_execute_onSuccess_null() throws Exception {\n    IllegalArgumentException exception = new IllegalArgumentException();\n    Call<String> errorCall = errorCall(exception);\n\n    Call<String> resolvedCall = errorCall.handleError(\n      (error, callback) -> callback.onSuccess(null)\n    );\n\n    assertThat(resolvedCall.execute())\n      .isNull();\n  }\n\n  @Test void onErrorReturn_enqueue_onError() {\n    IllegalArgumentException exception = new IllegalArgumentException();\n    Call<String> errorCall = errorCall(exception);\n\n    Call<String> resolvedCall = errorCall.handleError(\n      (error, callback) -> callback.onError(error)\n    );\n\n    resolvedCall.enqueue(callback);\n\n    verify(callback).onError(exception);\n  }\n\n  @Test void onErrorReturn_enqueue_onSuccess() {\n    IllegalArgumentException exception = new IllegalArgumentException();\n    Call<String> errorCall = errorCall(exception);\n\n    Call<String> resolvedCall = errorCall.handleError(\n      (error, callback) -> callback.onSuccess(\"foo\")\n    );\n\n    resolvedCall.enqueue(callback);\n\n    verify(callback).onSuccess(\"foo\");\n  }\n\n  @Test void onErrorReturn_enqueue_onSuccess_null() {\n    NoSuchElementException exception = new NoSuchElementException();\n    Call<List<String>> call = errorCall(exception);\n\n    Call<List<String>> resolvedCall = call.handleError((error, callback) -> {\n        if (error instanceof NoSuchElementException) {\n          callback.onSuccess(List.of());\n        } else {\n          callback.onError(error);\n        }\n      }\n    );\n\n    resolvedCall.enqueue(callback);\n\n    verify(callback).onSuccess(List.of());\n  }\n\n  static <T> Call<T> errorCall(RuntimeException error) {\n    return new Call.Base<T>() {\n      @Override protected T doExecute() {\n        throw error;\n      }\n\n      @Override protected void doEnqueue(Callback<T> callback) {\n        callback.onError(error);\n      }\n\n      @Override public Call<T> clone() {\n        throw new AssertionError();\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/EndpointTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.net.Inet4Address;\nimport java.net.Inet6Address;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nclass EndpointTest {\n\n  @Test void missingIpv4IsNull() {\n    assertThat(Endpoint.newBuilder().build().ipv4())\n      .isNull();\n  }\n\n  /** Many getPort operations return -1 by default. Leniently coerse to null. */\n  @Test void newBuilderWithPort_NegativeCoercesToNull() {\n    assertThat(Endpoint.newBuilder().port(-1).build().port())\n      .isNull();\n  }\n\n  @Test void newBuilderWithPort_0CoercesToNull() {\n    assertThat(Endpoint.newBuilder().port(0).build().port())\n      .isNull();\n  }\n\n  @Test void newBuilderWithPort_highest() {\n    assertThat(Endpoint.newBuilder().port(65535).build().port())\n      .isEqualTo(65535);\n  }\n\n  @Test void ip_addr_ipv4() throws Exception {\n    Endpoint.Builder newBuilder = Endpoint.newBuilder();\n    assertThat(newBuilder.parseIp(Inet4Address.getByName(\"43.0.192.2\"))).isTrue();\n    Endpoint endpoint = newBuilder.build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_bytes_ipv4() throws Exception {\n    Endpoint.Builder newBuilder = Endpoint.newBuilder();\n    assertThat(newBuilder.parseIp(Inet4Address.getByName(\"43.0.192.2\").getAddress())).isTrue();\n    Endpoint endpoint = newBuilder.build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_string_ipv4() {\n    Endpoint.Builder newBuilder = Endpoint.newBuilder();\n    assertThat(newBuilder.parseIp(\"43.0.192.2\")).isTrue();\n    Endpoint endpoint = newBuilder.build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_ipv6() throws Exception {\n    String ipv6 = \"2001:db8::c001\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv4Bytes())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isEqualTo(ipv6);\n    assertThat(endpoint.ipv6Bytes())\n      .containsExactly(Inet6Address.getByName(ipv6).getAddress());\n  }\n\n  @Test void ip_ipv6_addr() throws Exception {\n    String ipv6 = \"2001:db8::c001\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(Inet6Address.getByName(ipv6)).build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv4Bytes())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isEqualTo(ipv6);\n    assertThat(endpoint.ipv6Bytes())\n      .containsExactly(Inet6Address.getByName(ipv6).getAddress());\n  }\n\n  @Test void parseIp_ipv6_bytes() throws Exception {\n    String ipv6 = \"2001:db8::c001\";\n\n    Endpoint.Builder newBuilder = Endpoint.newBuilder();\n    assertThat(newBuilder.parseIp(Inet6Address.getByName(ipv6))).isTrue();\n    Endpoint endpoint = newBuilder.build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv4Bytes())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isEqualTo(ipv6);\n    assertThat(endpoint.ipv6Bytes())\n      .containsExactly(Inet6Address.getByName(ipv6).getAddress());\n  }\n\n  @Test void ip_ipv6_mappedIpv4() {\n    String ipv6 = \"::FFFF:43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_ipv6_addr_mappedIpv4() throws Exception {\n    String ipv6 = \"::FFFF:43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(Inet6Address.getByName(ipv6)).build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_ipv6_compatIpv4() {\n    String ipv6 = \"::0000:43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ip_ipv6_addr_compatIpv4() throws Exception {\n    String ipv6 = \"::0000:43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(Inet6Address.getByName(ipv6)).build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  @Test void ipv6_notMappedIpv4() {\n    String ipv6 = \"::ffef:43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv4Bytes())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isNull();\n    assertThat(endpoint.ipv6Bytes())\n      .isNull();\n  }\n\n  @Test void ipv6_downcases() {\n    Endpoint endpoint = Endpoint.newBuilder().ip(\"2001:DB8::C001\").build();\n\n    assertThat(endpoint.ipv6())\n      .isEqualTo(\"2001:db8::c001\");\n  }\n\n  @Test void ip_ipv6_compatIpv4_compressed() {\n    String ipv6 = \"::43.0.192.2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertExpectedIpv4(endpoint);\n  }\n\n  /** This ensures we don't mistake IPv6 localhost for a mapped IPv4 0.0.0.1 */\n  @Test void ipv6_localhost() {\n    String ipv6 = \"::1\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv4Bytes())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isEqualTo(ipv6);\n    assertThat(endpoint.ipv6Bytes())\n      .containsExactly(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);\n  }\n\n  /** This is an unusable compat Ipv4 of 0.0.0.2. This makes sure it isn't mistaken for localhost */\n  @Test void ipv6_notLocalhost() {\n    String ipv6 = \"::2\";\n    Endpoint endpoint = Endpoint.newBuilder().ip(ipv6).build();\n\n    assertThat(endpoint.ipv4())\n      .isNull();\n    assertThat(endpoint.ipv6())\n      .isEqualTo(ipv6);\n  }\n\n  /** The integer arg of port should be a whole number */\n  @Test void newBuilderWithPort_tooLargeIsInvalid() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n      assertThat(Endpoint.newBuilder().port(65536).build().port()).isNull();\n    });\n    assertThat(exception.getMessage()).contains(\"invalid port 65536\");\n  }\n\n  /**\n   * The integer arg of port should fit in a 16bit unsigned value\n   */\n  @Test void newBuilderWithPort_tooHighIsInvalid() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n      Endpoint.newBuilder().port(65536).build();\n    });\n    assertThat(exception.getMessage()).contains(\"invalid port 65536\");\n  }\n\n  /** Catches common error when zero is passed instead of null for a port */\n  @Test void coercesZeroPortToNull() {\n    Endpoint endpoint = Endpoint.newBuilder().port(0).build();\n\n    assertThat(endpoint.port())\n      .isNull();\n  }\n\n  @Test void lowercasesServiceName() {\n    assertThat(Endpoint.newBuilder().serviceName(\"fFf\").ip(\"127.0.0.1\").build().serviceName())\n      .isEqualTo(\"fff\");\n  }\n\n  static void assertExpectedIpv4(Endpoint endpoint) {\n    assertThat(endpoint.ipv4())\n      .isEqualTo(\"43.0.192.2\");\n    assertThat(endpoint.ipv4Bytes())\n      .containsExactly(43, 0, 192, 2);\n    assertThat(endpoint.ipv6())\n      .isNull();\n    assertThat(endpoint.ipv6Bytes())\n      .isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/SpanBytesDecoderDetectorTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.codec.SpanBytesEncoder;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static zipkin2.TestObjects.FRONTEND;\n\nclass SpanBytesDecoderDetectorTest {\n  Span span1 =\n      Span.newBuilder()\n          .traceId(\"a\")\n          .id(\"b\")\n          .name(\"get\")\n          .timestamp(10)\n          .duration(30)\n          .kind(Span.Kind.SERVER)\n          .shared(true)\n          .putTag(\"http.method\", \"GET\")\n          .localEndpoint(FRONTEND)\n          .build();\n  Span span2 =\n      Span.newBuilder()\n          .traceId(\"a\")\n          .parentId(\"b\")\n          .id(\"c\")\n          .name(\"get\")\n          .timestamp(15)\n          .duration(10)\n          .localEndpoint(FRONTEND)\n          .build();\n\n  @Test void decoderForMessage_json_v1() {\n    byte[] message = SpanBytesEncoder.JSON_V1.encode(span1);\n    assertThat(SpanBytesDecoderDetector.decoderForMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V1);\n  }\n\n  @Test void decoderForMessage_json_v1_list() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.JSON_V1.encodeList(List.of(span1, span2));\n      SpanBytesDecoderDetector.decoderForMessage(message);\n    });\n  }\n\n  @Test void decoderForListMessage_json_v1() {\n    byte[] message = SpanBytesEncoder.JSON_V1.encodeList(List.of(span1, span2));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V1);\n  }\n\n  @Test void decoderForListMessage_json_v1_singleItem() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.JSON_V1.encode(span1);\n      SpanBytesDecoderDetector.decoderForListMessage(message);\n    });\n  }\n\n  /** Single-element reads were for legacy non-list encoding. Don't add new code that does this */\n  @Test void decoderForMessage_json_v2() {\n    assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.JSON_V2.encode(span1);\n      assertThat(SpanBytesDecoderDetector.decoderForMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V2);\n    });\n  }\n\n  @Test void decoderForMessage_json_v2_list() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.JSON_V2.encodeList(List.of(span1, span2));\n      SpanBytesDecoderDetector.decoderForMessage(message);\n    });\n  }\n\n  @Test void decoderForListMessage_json_v2() {\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(List.of(span1, span2));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V2);\n  }\n\n  @Test void decoderForListMessage_json_v2_partial_localEndpoint() {\n    Span span =\n        Span.newBuilder()\n            .traceId(\"a\")\n            .id(\"b\")\n            .localEndpoint(Endpoint.newBuilder().serviceName(\"foo\").build())\n            .build();\n\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(List.of(span));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V2);\n  }\n\n  @Test void decoderForListMessage_json_v2_partial_remoteEndpoint() {\n    Span span =\n        Span.newBuilder()\n            .traceId(\"a\")\n            .id(\"b\")\n            .kind(Span.Kind.CLIENT)\n            .remoteEndpoint(Endpoint.newBuilder().serviceName(\"foo\").build())\n            .build();\n\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(List.of(span));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V2);\n  }\n\n  @Test void decoderForListMessage_json_v2_partial_tag() {\n    Span span = Span.newBuilder().traceId(\"a\").id(\"b\").putTag(\"foo\", \"bar\").build();\n\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(List.of(span));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.JSON_V2);\n  }\n\n  @Test void decoderForListMessage_json_v2_singleItem() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.JSON_V2.encode(span1);\n      SpanBytesDecoderDetector.decoderForListMessage(message);\n    });\n  }\n\n  @Test void decoderForMessage_thrift() {\n    byte[] message = SpanBytesEncoder.THRIFT.encode(span1);\n    assertThat(SpanBytesDecoderDetector.decoderForMessage(message))\n        .isEqualTo(SpanBytesDecoder.THRIFT);\n  }\n\n  @Test void decoderForMessage_thrift_list() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.THRIFT.encodeList(List.of(span1, span2));\n      SpanBytesDecoderDetector.decoderForMessage(message);\n    });\n  }\n\n  @Test void decoderForListMessage_thrift() {\n    byte[] message = SpanBytesEncoder.THRIFT.encodeList(List.of(span1, span2));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.THRIFT);\n  }\n\n  /**\n   * We encoded incorrectly for years, so we have to read this data eventhough it is wrong.\n   *\n   * <p>See openzipkin/zipkin-reporter-java#133\n   */\n  @Test void decoderForListMessage_thrift_incorrectFirstByte() {\n    byte[] message = SpanBytesEncoder.THRIFT.encodeList(List.of(span1, span2));\n    message[0] = 11; // We made a typo.. it should have been 12\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n      .isEqualTo(SpanBytesDecoder.THRIFT);\n  }\n\n  @Test void decoderForListMessage_thrift_singleItem() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.THRIFT.encode(span1);\n      SpanBytesDecoderDetector.decoderForListMessage(message);\n    });\n  }\n\n  /**\n   * Single-element reads were for legacy non-list encoding. Don't add new code that does this\n   */\n  @Test void decoderForMessage_proto3() {\n    assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.PROTO3.encode(span1);\n      assertThat(SpanBytesDecoderDetector.decoderForMessage(message))\n        .isEqualTo(SpanBytesDecoder.PROTO3);\n    });\n  }\n\n  @Test void decoderForMessage_proto3_list() {\n    assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> {\n      byte[] message = SpanBytesEncoder.PROTO3.encodeList(List.of(span1, span2));\n      SpanBytesDecoderDetector.decoderForMessage(message);\n    });\n  }\n\n  @Test void decoderForListMessage_proto3() {\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(List.of(span1, span2));\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.PROTO3);\n  }\n\n  /** There is no difference between a list of size one and a single element in proto3 */\n  @Test void decoderForListMessage_proto3_singleItem() {\n    byte[] message = SpanBytesEncoder.PROTO3.encode(span1);\n    assertThat(SpanBytesDecoderDetector.decoderForListMessage(message))\n        .isEqualTo(SpanBytesDecoder.PROTO3);\n  }\n\n  @Test void decoderForMessage_unknown() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      SpanBytesDecoderDetector.decoderForMessage(new byte[]{'h'});\n    });\n  }\n\n  @Test void decoderForListMessage_unknown() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      SpanBytesDecoderDetector.decoderForListMessage(new byte[]{'h'});\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/SpanTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.util.UUID;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static org.assertj.core.data.MapEntry.entry;\nimport static zipkin2.Span.normalizeTraceId;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.FRONTEND;\n\nclass SpanTest {\n  Span base = Span.newBuilder().traceId(\"1\").id(\"1\").localEndpoint(FRONTEND).build();\n  Span oneOfEach = Span.newBuilder()\n    .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n    .parentId(\"1\")\n    .id(\"2\")\n    .name(\"get\")\n    .kind(Span.Kind.SERVER)\n    .localEndpoint(BACKEND)\n    .remoteEndpoint(FRONTEND)\n    .timestamp(1)\n    .duration(3)\n    .addAnnotation(2, \"foo\")\n    .putTag(\"http.path\", \"/api\")\n    .shared(true)\n    .debug(true)\n    .build();\n\n  @Test void traceIdString() {\n    Span with128BitId = base.toBuilder()\n      .traceId(\"463ac35c9f6413ad48485a3953bb6124\")\n      .name(\"foo\").build();\n\n    assertThat(with128BitId.traceId())\n      .isEqualTo(\"463ac35c9f6413ad48485a3953bb6124\");\n  }\n\n  @Test void localEndpoint_emptyToNull() {\n    assertThat(base.toBuilder().localEndpoint(Endpoint.newBuilder().build()).localEndpoint)\n      .isNull();\n  }\n\n  @Test void remoteEndpoint_emptyToNull() {\n    assertThat(base.toBuilder().remoteEndpoint(Endpoint.newBuilder().build()).remoteEndpoint)\n      .isNull();\n  }\n\n  @Test void localServiceName() {\n    assertThat(base.toBuilder().localEndpoint(null).build().localServiceName())\n      .isNull();\n    assertThat(base.toBuilder().localEndpoint(FRONTEND).build().localServiceName())\n      .isEqualTo(FRONTEND.serviceName);\n  }\n\n  @Test void remoteServiceName() {\n    assertThat(base.toBuilder().remoteEndpoint(null).build().remoteServiceName())\n      .isNull();\n    assertThat(base.toBuilder().remoteEndpoint(BACKEND).build().remoteServiceName())\n      .isEqualTo(BACKEND.serviceName);\n  }\n\n  @Test void spanNamesLowercase() {\n    assertThat(base.toBuilder().name(\"GET\").build().name())\n      .isEqualTo(\"get\");\n  }\n\n  @Test void annotationsSortByTimestamp() {\n    Span span = base.toBuilder()\n      .addAnnotation(2L, \"foo\")\n      .addAnnotation(1L, \"foo\")\n      .build();\n\n    // note: annotations don't also have endpoints, as it is implicit to Span.localEndpoint\n    assertThat(span.annotations()).containsExactly(\n      Annotation.create(1L, \"foo\"),\n      Annotation.create(2L, \"foo\")\n    );\n  }\n\n  @Test void annotationsDedupe() {\n    Span span = base.toBuilder()\n      .addAnnotation(2L, \"foo\")\n      .addAnnotation(2L, \"foo\")\n      .addAnnotation(1L, \"foo\")\n      .addAnnotation(2L, \"foo\")\n      .addAnnotation(3L, \"foo\")\n      .build();\n\n    assertThat(span.annotations()).containsExactly(\n      Annotation.create(1L, \"foo\"),\n      Annotation.create(2L, \"foo\"),\n      Annotation.create(3L, \"foo\")\n    );\n  }\n\n  @Test void putTagOverwritesValue() {\n    Span span = base.toBuilder()\n      .putTag(\"foo\", \"bar\")\n      .putTag(\"foo\", \"qux\")\n      .build();\n\n    assertThat(span.tags()).containsExactly(\n      entry(\"foo\", \"qux\")\n    );\n  }\n\n  @Test void builder_canUnsetParent() {\n    Span withParent = base.toBuilder().parentId(\"3\").build();\n\n    assertThat(withParent.toBuilder().parentId(null).build().parentId())\n      .isNull();\n  }\n\n  @Test void clone_differentCollections() {\n    Span.Builder builder = base.toBuilder()\n      .addAnnotation(1L, \"foo\")\n      .putTag(\"foo\", \"qux\");\n\n    Span.Builder builder2 = builder.clone()\n      .addAnnotation(2L, \"foo\")\n      .putTag(\"foo\", \"bar\");\n\n    assertThat(builder.build()).isEqualTo(base.toBuilder()\n      .addAnnotation(1L, \"foo\")\n      .putTag(\"foo\", \"qux\")\n      .build()\n    );\n\n    assertThat(builder2.build()).isEqualTo(base.toBuilder()\n      .addAnnotation(1L, \"foo\")\n      .addAnnotation(2L, \"foo\")\n      .putTag(\"foo\", \"bar\")\n      .build()\n    );\n  }\n\n  /** Catches common error when zero is passed instead of null for a timestamp */\n  @Test void coercesZeroTimestampsToNull() {\n    Span span = base.toBuilder()\n      .timestamp(0L)\n      .duration(0L)\n      .build();\n\n    assertThat(span.timestamp())\n      .isNull();\n    assertThat(span.duration())\n      .isNull();\n  }\n\n  @Test void canUsePrimitiveOverloads() {\n    Span primitives = base.toBuilder()\n      .timestamp(1L)\n      .duration(1L)\n      .shared(true)\n      .debug(true)\n      .build();\n\n    Span objects = base.toBuilder()\n      .timestamp(Long.valueOf(1L))\n      .duration(Long.valueOf(1L))\n      .shared(Boolean.TRUE)\n      .debug(Boolean.TRUE)\n      .build();\n\n    assertThat(primitives)\n      .isEqualToComparingFieldByField(objects);\n  }\n\n  @Test void debug_canUnset() {\n    assertThat(base.toBuilder().debug(true).debug(null).build().debug())\n      .isNull();\n  }\n\n  @Test void debug_canDisable() {\n    assertThat(base.toBuilder().debug(true).debug(false).build().debug())\n      .isFalse();\n  }\n\n  @Test void shared_canUnset() {\n    assertThat(base.toBuilder().shared(true).shared(null).build().shared())\n      .isNull();\n  }\n\n  @Test void shared_canDisable() {\n    assertThat(base.toBuilder().shared(true).shared(false).build().shared())\n      .isFalse();\n  }\n\n  @Test void nullToZeroOrFalse() {\n    Span nulls = base.toBuilder()\n      .timestamp(null)\n      .duration(null)\n      .build();\n\n    Span zeros = base.toBuilder()\n      .timestamp(0L)\n      .duration(0L)\n      .build();\n\n    assertThat(nulls)\n      .isEqualToComparingFieldByField(zeros);\n  }\n\n  @Test void builder_clear() {\n    assertThat(oneOfEach.toBuilder().clear().traceId(\"a\").id(\"a\").build())\n      .isEqualToComparingFieldByField(Span.newBuilder().traceId(\"a\").id(\"a\").build());\n  }\n\n  @Test void builder_clone() {\n    Span.Builder builder = oneOfEach.toBuilder();\n    assertThat(builder.clone())\n      .isNotSameAs(builder)\n      .isEqualToComparingFieldByField(builder);\n  }\n\n  @Test void builder_merge_redundant() {\n    Span merged = oneOfEach.toBuilder().merge(oneOfEach).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(oneOfEach);\n  }\n\n  @Test void builder_merge_flags() {\n    assertThat(Span.newBuilder().shared(true).merge(base.toBuilder().debug(true).build()).build())\n      .isEqualToComparingFieldByField(base.toBuilder().shared(true).debug(true).build());\n  }\n\n  @Test void builder_merge_annotations() {\n    Span merged = Span.newBuilder().merge(oneOfEach).build();\n\n    assertThat(merged.annotations).containsExactlyElementsOf(oneOfEach.annotations);\n  }\n\n  @Test void builder_merge_annotations_concat() {\n    Span merged = Span.newBuilder().addAnnotation(1, \"a\").addAnnotation(1, \"b\")\n      .merge(base.toBuilder().addAnnotation(1, \"b\").addAnnotation(1, \"c\").build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().addAnnotation(1, \"a\").addAnnotation(1, \"b\").addAnnotation(1, \"c\").build()\n    );\n  }\n\n  @Test void builder_merge_tags() {\n    Span merged = Span.newBuilder().merge(oneOfEach).build();\n\n    assertThat(merged.tags).containsAllEntriesOf(oneOfEach.tags);\n  }\n\n  @Test void builder_merge_tags_concat() {\n    Span merged = Span.newBuilder().putTag(\"1\", \"a\").putTag(\"2\", \"a\")\n      .merge(base.toBuilder().putTag(\"2\", \"a\").putTag(\"3\", \"a\").build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().putTag(\"1\", \"a\").putTag(\"2\", \"a\").putTag(\"3\", \"a\").build()\n    );\n  }\n\n  @Test void builder_merge_localEndpoint() {\n    Span merged = Span.newBuilder()\n      .merge(base.toBuilder().localEndpoint(FRONTEND).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().localEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_localEndpoint_redundant() {\n    Span merged = Span.newBuilder().localEndpoint(FRONTEND)\n      .merge(base.toBuilder().localEndpoint(FRONTEND).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().localEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_localEndpoint_merge() {\n    Span merged = Span.newBuilder().localEndpoint(Endpoint.newBuilder().serviceName(\"a\").build())\n      .merge(\n        base.toBuilder().localEndpoint(Endpoint.newBuilder().ip(\"192.168.99.101\").build()).build())\n      .merge(\n        base.toBuilder().localEndpoint(Endpoint.newBuilder().ip(\"2001:db8::c001\").build()).build())\n      .merge(\n        base.toBuilder().localEndpoint(Endpoint.newBuilder().port(80).build()).build())\n      .build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().localEndpoint(Endpoint.newBuilder()\n        .serviceName(\"a\")\n        .ip(\"192.168.99.101\")\n        .ip(\"2001:db8::c001\")\n        .port(80)\n        .build()\n      ).build()\n    );\n  }\n\n  @Test void builder_merge_localEndpoint_null() {\n    Span merged = Span.newBuilder().localEndpoint(FRONTEND)\n      .merge(Span.newBuilder().traceId(base.traceId()).id(base.id()).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      Span.newBuilder().traceId(base.traceId()).id(base.id()).localEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_remoteEndpoint_null() {\n    Span merged = Span.newBuilder().remoteEndpoint(FRONTEND)\n      .merge(Span.newBuilder().traceId(base.traceId()).id(base.id()).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      Span.newBuilder().traceId(base.traceId()).id(base.id()).remoteEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_remoteEndpoint() {\n    Span merged = Span.newBuilder()\n      .merge(base.toBuilder().remoteEndpoint(FRONTEND).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().remoteEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_remoteEndpoint_redundant() {\n    Span merged = Span.newBuilder().remoteEndpoint(FRONTEND)\n      .merge(base.toBuilder().remoteEndpoint(FRONTEND).build()).build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().remoteEndpoint(FRONTEND).build()\n    );\n  }\n\n  @Test void builder_merge_remoteEndpoint_merge() {\n    Span merged = Span.newBuilder().remoteEndpoint(Endpoint.newBuilder().serviceName(\"a\").build())\n      .merge(\n        base.toBuilder().remoteEndpoint(Endpoint.newBuilder().ip(\"192.168.99.101\").build()).build())\n      .merge(\n        base.toBuilder().remoteEndpoint(Endpoint.newBuilder().ip(\"2001:db8::c001\").build()).build())\n      .merge(\n        base.toBuilder().remoteEndpoint(Endpoint.newBuilder().port(80).build()).build())\n      .build();\n\n    assertThat(merged).isEqualToComparingFieldByField(\n      base.toBuilder().remoteEndpoint(Endpoint.newBuilder()\n        .serviceName(\"a\")\n        .ip(\"192.168.99.101\")\n        .ip(\"2001:db8::c001\")\n        .port(80)\n        .build()\n      ).build()\n    );\n  }\n\n  @Test void toString_isJson() {\n    assertThat(base.toString()).hasToString(\n      \"{\\\"traceId\\\":\\\"0000000000000001\\\",\\\"id\\\":\\\"0000000000000001\\\",\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}\"\n    );\n  }\n\n  /** Test serializable as used in spark jobs. Careful to include all non-standard fields */\n  @Test void serialization() throws Exception {\n    ByteArrayOutputStream buffer = new ByteArrayOutputStream();\n\n    Span span = base.toBuilder()\n      .addAnnotation(1L, \"foo\")\n      .build();\n\n    new ObjectOutputStream(buffer).writeObject(span);\n\n    assertThat(new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject())\n      .isEqualTo(span);\n  }\n\n  @Test void traceIdFromLong() {\n    assertThat(base.toBuilder().traceId(0L, 12345678L).build().traceId())\n      .isEqualTo(\"0000000000bc614e\");\n  }\n\n  @Test void traceIdFromLong_128() {\n    assertThat(base.toBuilder().traceId(1234L, 5678L).build().traceId())\n      .isEqualTo(\"00000000000004d2000000000000162e\");\n  }\n\n  /** Some tools like rsocket redundantly pass high bits as zero. */\n  @Test void normalizeTraceId_truncates64BitZeroPrefix() {\n    assertThat(normalizeTraceId(\"0000000000000000000000000000162e\"))\n      .isEqualTo(\"000000000000162e\");\n  }\n\n  @Test void normalizeTraceId_padsTo64() {\n    assertThat(normalizeTraceId(\"162e\"))\n      .isEqualTo(\"000000000000162e\");\n  }\n\n  @Test void normalizeTraceId_padsTo128() {\n    assertThat(normalizeTraceId(\"4d2000000000000162e\"))\n      .isEqualTo(\"00000000000004d2000000000000162e\");\n  }\n\n  @Test void normalizeTraceId_badCharacters() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      normalizeTraceId(\"000-0000000004d20000000ss000162e\");\n    });\n  }\n\n  @Test void traceIdFromLong_invalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().traceId(0, 0);\n    });\n  }\n\n  @Test void parentIdFromLong() {\n    assertThat(base.toBuilder().parentId(3405691582L).build().parentId())\n      .isEqualTo(\"00000000cafebabe\");\n  }\n\n  @Test void parentIdFromLong_zeroSameAsNull() {\n    assertThat(base.toBuilder().parentId(0L).build().parentId())\n      .isNull();\n    assertThat(base.toBuilder().parentId(\"0\").build().parentId())\n      .isNull();\n  }\n\n  /** Prevents processing tools from looping */\n  @Test void parentId_sameAsIdCoerseToNull() {\n    assertThat(base.toBuilder().parentId(base.id).build().parentId())\n      .isNull();\n  }\n\n  @Test void removesSharedFlagFromClientSpans() {\n    assertThat(base.toBuilder().kind(Span.Kind.CLIENT).build().shared())\n      .isNull();\n  }\n\n  @Test void idFromLong() {\n    assertThat(base.toBuilder().id(3405691582L).build().id())\n      .isEqualTo(\"00000000cafebabe\");\n  }\n\n  @Test void idFromLong_minValue() {\n    assertThat(base.toBuilder().id(Long.MAX_VALUE).build().id())\n      .isEqualTo(\"7fffffffffffffff\");\n  }\n\n  @Test void idFromLong_invalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().id(0);\n    });\n  }\n\n  @Test void id_emptyInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().id(\"\");\n    });\n  }\n\n  @Test void id_zerosInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().id(\"0000000000000000\");\n    });\n  }\n\n  @Test void parentId_emptyInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().parentId(\"\");\n    });\n  }\n\n  @Test void traceId_emptyInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().traceId(\"\");\n    });\n  }\n\n  @Test void traceId_zerosInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().traceId(\"0000000000000000\");\n    });\n  }\n\n  @Test void traceId_uuidInvalid() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      base.toBuilder().traceId(UUID.randomUUID().toString());\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/TestObjects.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.util.Calendar;\nimport java.util.List;\nimport java.util.TimeZone;\nimport java.util.concurrent.TimeUnit;\nimport zipkin2.Span.Kind;\nimport zipkin2.storage.QueryRequest;\n\nimport static java.util.Arrays.asList;\n\npublic final class TestObjects {\n  /** Notably, the cassandra implementation has day granularity */\n  public static final long DAY = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS);\n\n  static final TimeZone UTC = TimeZone.getTimeZone(\"UTC\");\n\n  // Use real time, as most span-stores have TTL logic which looks back several days.\n  public static final long TODAY = midnightUTC(System.currentTimeMillis());\n\n  public static final Endpoint FRONTEND =\n    Endpoint.newBuilder().serviceName(\"frontend\").ip(\"127.0.0.1\").build();\n  public static final Endpoint BACKEND =\n    Endpoint.newBuilder().serviceName(\"backend\").ip(\"192.168.99.101\").port(9000).build();\n  public static final Endpoint DB =\n    Endpoint.newBuilder().serviceName(\"db\").ip(\"2001:db8::c001\").port(3036).build();\n\n  /** For bucketed data floored to the day. For example, dependency links. */\n  public static long midnightUTC(long epochMillis) {\n    Calendar day = Calendar.getInstance(UTC);\n    day.setTimeInMillis(epochMillis);\n    day.set(Calendar.MILLISECOND, 0);\n    day.set(Calendar.SECOND, 0);\n    day.set(Calendar.MINUTE, 0);\n    day.set(Calendar.HOUR_OF_DAY, 0);\n    return day.getTimeInMillis();\n  }\n\n  /** Only for unit tests, not integration tests. Integration tests should use random trace IDs. */\n  public static final Span CLIENT_SPAN = Span.newBuilder()\n    .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n    .parentId(\"1\")\n    .id(\"2\")\n    .name(\"get\")\n    .kind(Kind.CLIENT)\n    .localEndpoint(FRONTEND)\n    .remoteEndpoint(BACKEND)\n    .timestamp((TODAY + 50L) * 1000L)\n    .duration(200 * 1000L)\n    .addAnnotation((TODAY + 100) * 1000L, \"foo\")\n    .putTag(\"http.path\", \"/api\")\n    .putTag(\"clnt/finagle.version\", \"6.45.0\")\n    .build();\n\n  /** Only for unit tests, not integration tests. Integration tests should use random trace IDs. */\n  public static final List<Span> TRACE = newTrace(CLIENT_SPAN.traceId());\n\n  static final Span.Builder SPAN_BUILDER = newSpanBuilder();\n\n  /** Reuse a builder as it is significantly slows tests to create 100000 of these! */\n  static Span.Builder newSpanBuilder() {\n    return Span.newBuilder().name(\"get\")\n      .timestamp(TODAY * 1000L + 100L)\n      .duration(200 * 1000L)\n      .localEndpoint(FRONTEND)\n      .putTag(\"environment\", \"test\");\n  }\n\n  public static Span span(long traceId) {\n    return SPAN_BUILDER.traceId(0L, traceId).id(traceId).build();\n  }\n\n  public static String appendSuffix(String serviceName, String serviceNameSuffix) {\n    if (serviceNameSuffix == null) throw new NullPointerException(\"serviceNameSuffix == null\");\n    if (serviceNameSuffix.isEmpty()) return serviceName;\n    return serviceName + \"_\" + serviceNameSuffix;\n  }\n\n  public static Endpoint suffixServiceName(Endpoint endpoint, String serviceNameSuffix) {\n    String prefixed = appendSuffix(endpoint.serviceName(), serviceNameSuffix);\n    if (endpoint.serviceName().equals(prefixed)) return endpoint;\n    return endpoint.toBuilder().serviceName(prefixed).build();\n  }\n\n  static List<Span> newTrace(String traceId) {\n    Endpoint frontend = suffixServiceName(FRONTEND, \"\");\n    Endpoint backend = suffixServiceName(BACKEND, \"\");\n    Endpoint db = suffixServiceName(DB, \"\");\n\n    return asList(\n      Span.newBuilder().traceId(traceId).id(\"1\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(frontend)\n        .timestamp(TODAY * 1000L)\n        .duration(350 * 1000L)\n        .build(),\n      CLIENT_SPAN.toBuilder()\n        .traceId(traceId)\n        .localEndpoint(frontend)\n        .remoteEndpoint(backend)\n        .build(),\n      Span.newBuilder().traceId(traceId)\n        .parentId(CLIENT_SPAN.parentId()).id(CLIENT_SPAN.id()).shared(true)\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .timestamp((TODAY + 100L) * 1000L)\n        .duration(150 * 1000L)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"2\").id(\"3\")\n        .name(\"query\")\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .remoteEndpoint(db)\n        .timestamp((TODAY + 150L) * 1000L)\n        .duration(50 * 1000L)\n        .addAnnotation((TODAY + 190) * 1000L, \"⻩\")\n        .putTag(\"error\", \"\\uD83D\\uDCA9\")\n        .build()\n    );\n  }\n\n  public static QueryRequest.Builder requestBuilder() {\n    return QueryRequest.newBuilder().endTs(TODAY + DAY).lookback(DAY * 2).limit(100);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/codec/EncodingTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass EncodingTest {\n\n  @Test void emptyList_json() {\n    List<byte[]> encoded = List.of();\n    assertThat(Encoding.JSON.listSizeInBytes(encoded))\n      .isEqualTo(2 /* [] */);\n  }\n\n  @Test void singletonList_json() {\n    List<byte[]> encoded = List.of(new byte[10]);\n\n    assertThat(Encoding.JSON.listSizeInBytes(encoded.get(0).length))\n      .isEqualTo(2 /* [] */ + 10);\n    assertThat(Encoding.JSON.listSizeInBytes(encoded))\n      .isEqualTo(2 /* [] */ + 10);\n  }\n\n  @Test void multiItemList_json() {\n    List<byte[]> encoded = List.of(new byte[3], new byte[4], new byte[128]);\n    assertThat(Encoding.JSON.listSizeInBytes(encoded))\n      .isEqualTo(2 /* [] */ + 3 + 1 /* , */ + 4 + 1  /* , */ + 128);\n  }\n\n  @Test void emptyList_proto3() {\n    List<byte[]> encoded = List.of();\n    assertThat(Encoding.PROTO3.listSizeInBytes(encoded))\n      .isEqualTo(0);\n  }\n\n  // an entry in a list is a repeated field\n  @Test void singletonList_proto3() {\n    List<byte[]> encoded = List.of(new byte[10]);\n\n    assertThat(Encoding.PROTO3.listSizeInBytes(encoded.get(0).length))\n      .isEqualTo(10);\n    assertThat(Encoding.PROTO3.listSizeInBytes(encoded))\n      .isEqualTo(10);\n  }\n\n  // per ListOfSpans in zipkin2.proto\n  @Test void multiItemList_proto3() {\n    List<byte[]> encoded = List.of(new byte[3], new byte[4], new byte[128]);\n    assertThat(Encoding.PROTO3.listSizeInBytes(encoded))\n      .isEqualTo(3 + 4 + 128);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/codec/KryoTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport com.esotericsoftware.kryo.Kryo;\nimport com.esotericsoftware.kryo.Serializer;\nimport com.esotericsoftware.kryo.io.Input;\nimport com.esotericsoftware.kryo.io.Output;\nimport com.esotericsoftware.kryo.serializers.JavaSerializer;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Annotation;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass KryoTest {\n\n  @Test void kryoJavaSerialization_annotation() {\n    Kryo kryo = new Kryo();\n    kryo.register(Annotation.class, new JavaSerializer());\n\n    Output output = new Output(4096);\n    kryo.writeObject(output, Annotation.create(1L, \"foo\"));\n    output.flush();\n    byte[] serialized = output.getBuffer();\n\n    assertThat(kryo.readObject(new Input(serialized), Annotation.class))\n      .isEqualTo(Annotation.create(1L, \"foo\"));\n  }\n\n  @Test void kryoJavaSerialization_endpoint() {\n    Kryo kryo = new Kryo();\n    kryo.register(Endpoint.class, new JavaSerializer());\n\n    Output output = new Output(4096);\n    kryo.writeObject(output, TestObjects.BACKEND);\n    output.flush();\n    byte[] serialized = output.getBuffer();\n\n    assertThat(kryo.readObject(new Input(serialized), Endpoint.class))\n      .isEqualTo(TestObjects.BACKEND);\n  }\n\n  @Test void kryoJavaSerialization_span() {\n    Kryo kryo = new Kryo();\n    kryo.register(Span.class, new JavaSerializer());\n\n    Output output = new Output(4096);\n    kryo.writeObject(output, TestObjects.CLIENT_SPAN);\n    output.flush();\n    byte[] serialized = output.getBuffer();\n\n    assertThat(kryo.readObject(new Input(serialized), Span.class))\n      .isEqualTo(TestObjects.CLIENT_SPAN);\n  }\n\n  @Test void kryoJavaSerialization_dependencyLink() {\n    Kryo kryo = new Kryo();\n    kryo.register(DependencyLink.class, new JavaSerializer());\n\n    DependencyLink link = DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L)\n      .errorCount(23L).build();\n\n    Output output = new Output(4096);\n    kryo.writeObject(output, link);\n    output.flush();\n    byte[] serialized = output.getBuffer();\n\n    assertThat(kryo.readObject(new Input(serialized), DependencyLink.class))\n      .isEqualTo(link);\n  }\n\n  /** Example test for how to use Kryo and reuse our encoders */\n  public static class JsonV2SpanSerializer extends Serializer<Span> {\n    @Override public void write(Kryo kryo, Output output, Span span) {\n      byte[] json = SpanBytesEncoder.JSON_V2.encode(span);\n      output.writeInt(json.length);\n      output.write(json);\n    }\n\n    @Override public Span read(Kryo kryo, Input input, Class<? extends Span> type) {\n      int length = input.readInt();\n      byte[] json = input.readBytes(length);\n      return SpanBytesDecoder.JSON_V2.decodeOne(json);\n    }\n  }\n\n  @Test void kryoJson2() {\n    Kryo kryo = new Kryo();\n    kryo.register(Span.class, new JsonV2SpanSerializer());\n\n    Output output = new Output(4096);\n    kryo.writeObject(output, TestObjects.CLIENT_SPAN);\n    output.flush();\n    byte[] serialized = output.getBuffer();\n\n    assertThat(kryo.readObject(new Input(serialized), Span.class))\n      .isEqualTo(TestObjects.CLIENT_SPAN);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/codec/SpanBytesDecoderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.TRACE;\nimport static zipkin2.codec.SpanBytesEncoderTest.ERROR_SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.LOCAL_SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.NO_ANNOTATIONS_ROOT_SERVER_SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.UTF8_SPAN;\n\nclass SpanBytesDecoderTest {\n  Span span = SPAN;\n\n  @Test void niceErrorOnTruncatedSpans_PROTO3() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      byte[] encoded = SpanBytesEncoder.PROTO3.encodeList(TRACE);\n      SpanBytesDecoder.PROTO3.decodeList(Arrays.copyOfRange(encoded, 0, 10));\n    });\n    assertThat(exception.getMessage()).contains(\n      \"Truncated: length 66 > bytes available 8 reading List<Span> from proto3\");\n  }\n\n  @Test void niceErrorOnTruncatedSpan_PROTO3() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      byte[] encoded = SpanBytesEncoder.PROTO3.encode(SPAN);\n      SpanBytesDecoder.PROTO3.decodeOne(Arrays.copyOfRange(encoded, 0, 10));\n    });\n    assertThat(exception.getMessage()).contains(\n      \"Truncated: length 179 > bytes available 7 reading Span from proto3\");\n  }\n\n  @Test void emptyListOk_JSON_V1() {\n    assertThat(SpanBytesDecoder.JSON_V1.decodeList(new byte[0]))\n      .isEmpty(); // instead of throwing an exception\n    assertThat(SpanBytesDecoder.JSON_V1.decodeList(new byte[] {'[', ']'}))\n      .isEmpty(); // instead of throwing an exception\n  }\n\n  @Test void emptyListOk_JSON_V2() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeList(new byte[0]))\n      .isEmpty(); // instead of throwing an exception\n    assertThat(SpanBytesDecoder.JSON_V2.decodeList(new byte[] {'[', ']'}))\n      .isEmpty(); // instead of throwing an exception\n  }\n\n  @Test void emptyListOk_PROTO3() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(new byte[0]))\n      .isEmpty(); // instead of throwing an exception\n  }\n\n  @Test void spanRoundTrip_JSON_V2() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_PROTO3() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void localSpanRoundTrip_JSON_V2() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(LOCAL_SPAN)))\n      .isEqualTo(LOCAL_SPAN);\n  }\n\n  @Test void localSpanRoundTrip_PROTO3() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(LOCAL_SPAN)))\n      .isEqualTo(LOCAL_SPAN);\n  }\n\n  @Test void errorSpanRoundTrip_JSON_V2() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(ERROR_SPAN)))\n      .isEqualTo(ERROR_SPAN);\n  }\n\n  @Test void errorSpanRoundTrip_PROTO3() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(ERROR_SPAN)))\n      .isEqualTo(ERROR_SPAN);\n  }\n\n  @Test void spanRoundTrip_64bitTraceId_JSON_V2() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_64bitTraceId_PROTO3() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_shared_JSON_V2() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_shared_PROTO3() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  /**\n   * This isn't a test of what we \"should\" accept as a span, rather that characters that trip-up\n   * json don't fail in codec.\n   */\n  @Test void specialCharsInJson_JSON_V2() {\n    assertThat(\n      SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(UTF8_SPAN)))\n      .isEqualTo(UTF8_SPAN);\n  }\n\n  @Test void specialCharsInJson_PROTO3() {\n    assertThat(\n      SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(UTF8_SPAN)))\n      .isEqualTo(UTF8_SPAN);\n  }\n\n  @Test void falseOnEmpty_inputSpans_JSON_V2() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeList(new byte[0], new ArrayList<>()))\n      .isFalse();\n  }\n\n  @Test void falseOnEmpty_inputSpans_PROTO3() {\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(new byte[0], new ArrayList<>()))\n      .isFalse();\n  }\n\n  /**\n   * Particular, thrift can mistake malformed content as a huge list. Let's not blow up.\n   */\n  @Test void niceErrorOnMalformed_inputSpans_JSON_V2() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      SpanBytesDecoder.JSON_V2.decodeList(new byte[] {'h', 'e', 'l', 'l', 'o'});\n    });\n    assertThat(exception.getMessage()).contains(\"Malformed reading List<Span> from \");\n  }\n\n  @Test void niceErrorOnMalformed_inputSpans_PROTO3() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      SpanBytesDecoder.PROTO3.decodeList(new byte[] {'h', 'e', 'l', 'l', 'o'});\n    });\n    assertThat(exception.getMessage()).contains(\n      \"Truncated: length 101 > bytes available 3 reading List<Span> from proto3\");\n  }\n\n  @Test void traceRoundTrip_JSON_V2() {\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(TRACE);\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeList(message)).isEqualTo(TRACE);\n  }\n\n  @Test void traceRoundTrip_PROTO3() {\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(TRACE);\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(message)).isEqualTo(TRACE);\n  }\n\n  @Test void traceRoundTrip_PROTO3_directBuffer() {\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(TRACE);\n    ByteBuffer buf = ByteBuffer.allocateDirect(message.length);\n    buf.put(message);\n    buf.flip();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(buf)).isEqualTo(TRACE);\n  }\n\n  @Test void traceRoundTrip_PROTO3_heapBuffer() {\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(TRACE);\n    ByteBuffer buf = ByteBuffer.wrap(message);\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(buf)).isEqualTo(TRACE);\n  }\n\n  @Test void traceRoundTrip_PROTO3_heapBufferOffset() {\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(TRACE);\n    byte[] array = new byte[message.length + 4 + 5];\n    System.arraycopy(message, 0, array, 4, message.length);\n    ByteBuffer buf = ByteBuffer.wrap(array, 4, message.length);\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(buf)).isEqualTo(TRACE);\n  }\n\n  @Test void spansRoundTrip_JSON_V2() {\n    List<Span> tenClientSpans = Collections.nCopies(10, span);\n\n    byte[] message = SpanBytesEncoder.JSON_V2.encodeList(tenClientSpans);\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeList(message))\n      .isEqualTo(tenClientSpans);\n  }\n\n  @Test void spansRoundTrip_PROTO3() {\n    List<Span> tenClientSpans = Collections.nCopies(10, span);\n\n    byte[] message = SpanBytesEncoder.PROTO3.encodeList(tenClientSpans);\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(message))\n      .isEqualTo(tenClientSpans);\n  }\n\n  @Test void spanRoundTrip_noRemoteServiceName_JSON_V2() {\n    span = span.toBuilder()\n      .remoteEndpoint(BACKEND.toBuilder().serviceName(null).build())\n      .build();\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noRemoteServiceName_PROTO3() {\n    span = span.toBuilder()\n      .remoteEndpoint(BACKEND.toBuilder().serviceName(null).build())\n      .build();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_JSON_V2() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_PROTO3() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_incomplete_JSON_V2() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_incomplete_PROTO3() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_shared_JSON_V2() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(SpanBytesEncoder.JSON_V2.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_shared_PROTO3() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeOne(SpanBytesEncoder.PROTO3.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void niceErrorOnUppercase_traceId_JSON_V2() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"48485A3953BB6124\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\n      \"48485A3953BB6124 should be lower-hex encoded with no prefix\");\n  }\n\n  @Test void readsTraceIdHighFromTraceIdField() {\n    byte[] with128BitTraceId = (\"\"\"\n      {\n        \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\"\n      }\n      \"\"\").getBytes(UTF_8);\n    byte[] withLower64bitsTraceId = (\"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\"\n      }\n      \"\"\").getBytes(UTF_8);\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(with128BitTraceId))\n      .isEqualTo(SpanBytesDecoder.JSON_V2.decodeOne(withLower64bitsTraceId).toBuilder()\n        .traceId(\"48485a3953bb61246b221d5bc9e6496c\").build());\n  }\n\n  @Test void ignoresNull_topLevelFields() {\n    String json = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"parentId\": null,\n        \"id\": \"6b221d5bc9e6496c\",\n        \"name\": null,\n        \"timestamp\": null,\n        \"duration\": null,\n        \"localEndpoint\": null,\n        \"remoteEndpoint\": null,\n        \"annotations\": null,\n        \"tags\": null,\n        \"debug\": null,\n        \"shared\": null\n      }\n      \"\"\";\n\n    SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n  }\n\n  @Test void ignoresNull_endpoint_topLevelFields() {\n    String json = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"serviceName\": null,\n          \"ipv4\": \"127.0.0.1\",\n          \"ipv6\": null,\n          \"port\": null\n        }\n      }\n      \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8)).localEndpoint())\n      .isEqualTo(Endpoint.newBuilder().ip(\"127.0.0.1\").build());\n  }\n\n  @Test void skipsIncompleteEndpoint() {\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne((\"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"serviceName\": null,\n          \"ipv4\": null,\n          \"ipv6\": null,\n          \"port\": null\n        }\n      }\n      \"\"\")\n      .getBytes(UTF_8)).localEndpoint()).isNull();\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne((\"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n        }\n      }\n      \"\"\")\n      .getBytes(UTF_8)).localEndpoint()).isNull();\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne((\"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"remoteEndpoint\": {\n          \"serviceName\": null,\n          \"ipv4\": null,\n          \"ipv6\": null,\n          \"port\": null\n        }\n      }\n      \"\"\")\n      .getBytes(UTF_8)).remoteEndpoint()).isNull();\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne((\"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"remoteEndpoint\": {\n        }\n      }\n      \"\"\")\n      .getBytes(UTF_8)).remoteEndpoint()).isNull();\n  }\n\n  @Test void niceErrorOnIncomplete_annotation() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": 1472470996199000}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\n      \"Incomplete annotation at $.annotations[0].timestamp\");\n  }\n\n  @Test void niceErrorOnNull_traceId() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": null,\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"Expected a string but was NULL\");\n  }\n\n  @Test void niceErrorOnNull_id() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": null\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"Expected a string but was NULL\");\n  }\n\n  @Test void niceErrorOnNull_tagValue() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"tags\": {\n            \"foo\": NULL\n          }\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"No value at $.tags.foo\");\n  }\n\n  @Test void niceErrorOnNull_annotationValue() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": 1472470996199000, \"value\": NULL}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"$.annotations[0].value\");\n  }\n\n  @Test void niceErrorOnNull_annotationTimestamp() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json = \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": NULL, \"value\": \"foo\"}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"$.annotations[0].timestamp\");\n  }\n\n  @Test void niceErrorOnNonStringTagValue() {\n    String json = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"tags\": {\n          \"error\": true\n        }\n      }\n      \"\"\";\n\n    assertThatThrownBy(() -> SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8)))\n      .hasMessageContaining(\"Expected a string but was BOOLEAN\");\n  }\n\n  @Test void readSpan_localEndpoint_noServiceName() {\n    String json = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"ipv4\": \"127.0.0.1\"\n        }\n      }\n      \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8)).localServiceName())\n      .isNull();\n  }\n\n  @Test void readSpan_remoteEndpoint_noServiceName() {\n    String json = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"remoteEndpoint\": {\n          \"ipv4\": \"127.0.0.1\"\n        }\n      }\n      \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(json.getBytes(UTF_8)).remoteServiceName())\n      .isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/codec/SpanBytesEncoderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.internal.Proto3SpanWriterTest;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.FRONTEND;\n\n/**\n * This test is intentionally sensitive to ensure our custom encoders do not break in subtle ways.\n *\n * <p>Test for {@link SpanBytesEncoder#PROTO3} are sanity-check only as the corresponding tests are\n * in {@link Proto3SpanWriterTest}.\n */\npublic class SpanBytesEncoderTest {\n  /**\n   * Similar to {@link TestObjects#CLIENT_SPAN} except with fixed timestamps to ensure easy testing\n   * of json literals.\n   */\n  public static final Span SPAN =\n      Span.newBuilder()\n          .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n          .parentId(\"6b221d5bc9e6496c\")\n          .id(\"5b4185666d50f68b\")\n          .name(\"get\")\n          .kind(Span.Kind.CLIENT)\n          .localEndpoint(FRONTEND)\n          .remoteEndpoint(BACKEND)\n          .timestamp(1472470996199000L)\n          .duration(207000L)\n          .addAnnotation(1472470996238000L, \"foo\")\n          .addAnnotation(1472470996403000L, \"bar\")\n          .putTag(\"http.path\", \"/api\")\n          .putTag(\"clnt/finagle.version\", \"6.45.0\")\n          .build();\n\n  // service name is surrounded by control characters\n  public static final Span UTF8_SPAN =\n      Span.newBuilder()\n          .traceId(\"1\")\n          .id(\"1\")\n          // name is terrible\n          .name(new String(new char[] {'\"', '\\\\', '\\t', '\\b', '\\n', '\\r', '\\f'}))\n          // annotation value includes some json newline characters\n          .addAnnotation(1L, \"\\u2028 and \\u2029\")\n          // tag key includes a quote and value newlines\n          .putTag(\n              \"\\\"foo\", \"Database error: ORA-00942:\\u2028 and \\u2029 table or view does not exist\\n\")\n          .build();\n\n  public static final Span NO_ANNOTATIONS_ROOT_SERVER_SPAN =\n      Span.newBuilder()\n          .traceId(\"dc955a1d4768875d\")\n          .id(\"dc955a1d4768875d\")\n          .name(\"get\")\n          .timestamp(1510256710021866L)\n          .duration(1117L)\n          .kind(Span.Kind.SERVER)\n          .localEndpoint(Endpoint.newBuilder().serviceName(\"isao01\").ip(\"10.23.14.72\").build())\n          .putTag(\"http.path\", \"/rs/A\")\n          .putTag(\"location\", \"T67792\")\n          .putTag(\"other\", \"A\")\n          .build();\n\n  public static final Span LOCAL_SPAN =\n      Span.newBuilder()\n          .traceId(\"dc955a1d4768875d\")\n          .id(\"dc955a1d4768875d\")\n          .name(\"encode\")\n          .timestamp(1510256710021866L)\n          .duration(1117L)\n          .localEndpoint(Endpoint.newBuilder().serviceName(\"isao01\").ip(\"10.23.14.72\").build())\n          .build();\n\n  /** the following skeletal span is used in dependency linking */\n  public static final Span ERROR_SPAN =\n    Span.newBuilder()\n      .traceId(\"dc955a1d4768875d\")\n      .id(\"dc955a1d4768875d\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"isao01\").build())\n      .kind(Span.Kind.CLIENT)\n      .putTag(\"error\", \"\")\n      .build();\n\n  Span span = SPAN;\n\n  @Test void span_JSON_V1() {\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"annotations\\\":[{\\\"timestamp\\\":1472470996199000,\\\"value\\\":\\\"cs\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996406000,\\\"value\\\":\\\"cr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"clnt/finagle.version\\\",\\\"value\\\":\\\"6.45.0\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/api\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"sa\\\",\\\"value\\\":true,\\\"endpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000}}]}\");\n  }\n\n  @Test void span_JSON_V2() {\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"kind\\\":\\\"CLIENT\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"},\\\"remoteEndpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000},\\\"annotations\\\":[{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\"},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\"}],\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"}}\");\n  }\n\n  @Test void span_PROTO3() {\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(182);\n  }\n\n  @Test void localSpan_JSON_V1() {\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(LOCAL_SPAN), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"name\\\":\\\"encode\\\",\\\"timestamp\\\":1510256710021866,\\\"duration\\\":1117,\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"lc\\\",\\\"value\\\":\\\"\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}]}\");\n  }\n\n  @Test void localSpan_JSON_V2() {\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(LOCAL_SPAN), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"name\\\":\\\"encode\\\",\\\"timestamp\\\":1510256710021866,\\\"duration\\\":1117,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}\");\n  }\n\n  @Test void localSpan_PROTO3() {\n    assertThat(SpanBytesEncoder.PROTO3.encode(LOCAL_SPAN)).hasSize(58);\n  }\n\n  @Test void errorSpan_JSON_V2() {\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(ERROR_SPAN), UTF_8))\n      .isEqualTo(\n        \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"kind\\\":\\\"CLIENT\\\",\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\"},\\\"tags\\\":{\\\"error\\\":\\\"\\\"}}\");\n  }\n\n  @Test void errorSpan_PROTO3() {\n    assertThat(SpanBytesEncoder.PROTO3.encode(ERROR_SPAN)).hasSize(45);\n  }\n\n  @Test void span_64bitTraceId_JSON_V1() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"annotations\\\":[{\\\"timestamp\\\":1472470996199000,\\\"value\\\":\\\"cs\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996406000,\\\"value\\\":\\\"cr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"clnt/finagle.version\\\",\\\"value\\\":\\\"6.45.0\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/api\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"sa\\\",\\\"value\\\":true,\\\"endpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000}}]}\");\n  }\n\n  @Test void span_64bitTraceId_JSON_V2() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"kind\\\":\\\"CLIENT\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"},\\\"remoteEndpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000},\\\"annotations\\\":[{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\"},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\"}],\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"}}\");\n  }\n\n  @Test void span_64bitTraceId_PROTO3() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(174);\n  }\n\n  @Test void span_shared_JSON_V1() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"get\\\",\\\"annotations\\\":[{\\\"timestamp\\\":1472470996199000,\\\"value\\\":\\\"sr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996406000,\\\"value\\\":\\\"ss\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"ca\\\",\\\"value\\\":true,\\\"endpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000}},{\\\"key\\\":\\\"clnt/finagle.version\\\",\\\"value\\\":\\\"6.45.0\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/api\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}]}\");\n  }\n\n  @Test void span_shared_JSON_V2() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"kind\\\":\\\"SERVER\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"},\\\"remoteEndpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000},\\\"annotations\\\":[{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\"},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\"}],\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"},\\\"shared\\\":true}\");\n  }\n\n  @Test void span_shared_PROTO3() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(184);\n  }\n\n  @Test void specialCharsInJson_JSON_V1() {\n    span = UTF8_SPAN;\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"0000000000000001\\\",\\\"id\\\":\\\"0000000000000001\\\",\\\"name\\\":\\\"\\\\\\\"\\\\\\\\\\\\t\\\\b\\\\n\\\\r\\\\f\\\",\\\"annotations\\\":[{\\\"timestamp\\\":1,\\\"value\\\":\\\"\\\\u2028 and \\\\u2029\\\"}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"\\\\\\\"foo\\\",\\\"value\\\":\\\"Database error: ORA-00942:\\\\u2028 and \\\\u2029 table or view does not exist\\\\n\\\"}]}\");\n  }\n\n  @Test void specialCharsInJson_JSON_V2() {\n    span = UTF8_SPAN;\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"0000000000000001\\\",\\\"id\\\":\\\"0000000000000001\\\",\\\"name\\\":\\\"\\\\\\\"\\\\\\\\\\\\t\\\\b\\\\n\\\\r\\\\f\\\",\\\"annotations\\\":[{\\\"timestamp\\\":1,\\\"value\\\":\\\"\\\\u2028 and \\\\u2029\\\"}],\\\"tags\\\":{\\\"\\\\\\\"foo\\\":\\\"Database error: ORA-00942:\\\\u2028 and \\\\u2029 table or view does not exist\\\\n\\\"}}\");\n  }\n\n  @Test void specialCharsInJson_PROTO3() {\n    span = UTF8_SPAN;\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(133);\n  }\n\n  @Test void span_minimum_JSON_V1() {\n    span =\n        Span.newBuilder()\n            .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n            .id(\"5b4185666d50f68b\")\n            .build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"\\\"}\");\n  }\n\n  @Test void span_minimum_JSON_V2() {\n    span =\n        Span.newBuilder()\n            .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n            .id(\"5b4185666d50f68b\")\n            .build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\"}\");\n  }\n\n  @Test void span_minimum_PROTO3() {\n    span =\n        Span.newBuilder()\n            .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n            .id(\"5b4185666d50f68b\")\n            .build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(30);\n  }\n\n  @Test void span_noLocalServiceName_JSON_V1() {\n    span = span.toBuilder().localEndpoint(FRONTEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"annotations\\\":[{\\\"timestamp\\\":1472470996199000,\\\"value\\\":\\\"cs\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996406000,\\\"value\\\":\\\"cr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"clnt/finagle.version\\\",\\\"value\\\":\\\"6.45.0\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/api\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"sa\\\",\\\"value\\\":true,\\\"endpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000}}]}\");\n  }\n\n  @Test void span_noLocalServiceName_JSON_V2() {\n    span = span.toBuilder().localEndpoint(FRONTEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"kind\\\":\\\"CLIENT\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"localEndpoint\\\":{\\\"ipv4\\\":\\\"127.0.0.1\\\"},\\\"remoteEndpoint\\\":{\\\"serviceName\\\":\\\"backend\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000},\\\"annotations\\\":[{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\"},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\"}],\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"}}\");\n  }\n\n  @Test void span_noLocalServiceName_PROTO3() {\n    span = span.toBuilder().localEndpoint(FRONTEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(172);\n  }\n\n  @Test void span_noRemoteServiceName_JSON_V1() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"annotations\\\":[{\\\"timestamp\\\":1472470996199000,\\\"value\\\":\\\"cs\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"timestamp\\\":1472470996406000,\\\"value\\\":\\\"cr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"clnt/finagle.version\\\",\\\"value\\\":\\\"6.45.0\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/api\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}},{\\\"key\\\":\\\"sa\\\",\\\"value\\\":true,\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000}}]}\");\n  }\n\n  @Test void span_noRemoteServiceName_JSON_V2() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"7180c278b62e8f6a216a2aea45d08fc9\\\",\\\"parentId\\\":\\\"6b221d5bc9e6496c\\\",\\\"id\\\":\\\"5b4185666d50f68b\\\",\\\"kind\\\":\\\"CLIENT\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1472470996199000,\\\"duration\\\":207000,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"},\\\"remoteEndpoint\\\":{\\\"ipv4\\\":\\\"192.168.99.101\\\",\\\"port\\\":9000},\\\"annotations\\\":[{\\\"timestamp\\\":1472470996238000,\\\"value\\\":\\\"foo\\\"},{\\\"timestamp\\\":1472470996403000,\\\"value\\\":\\\"bar\\\"}],\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"}}\");\n  }\n\n  @Test void span_noRemoteServiceName_PROTO3() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(173);\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V1() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1510256710021866,\\\"duration\\\":1117,\\\"annotations\\\":[{\\\"timestamp\\\":1510256710021866,\\\"value\\\":\\\"sr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"timestamp\\\":1510256710022983,\\\"value\\\":\\\"ss\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/rs/A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"location\\\",\\\"value\\\":\\\"T67792\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"other\\\",\\\"value\\\":\\\"A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}]}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V2() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"kind\\\":\\\"SERVER\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1510256710021866,\\\"duration\\\":1117,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"},\\\"tags\\\":{\\\"http.path\\\":\\\"/rs/A\\\",\\\"location\\\":\\\"T67792\\\",\\\"other\\\":\\\"A\\\"}}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_PROTO3() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(109);\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V1_incomplete() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1510256710021866,\\\"annotations\\\":[{\\\"timestamp\\\":1510256710021866,\\\"value\\\":\\\"sr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/rs/A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"location\\\",\\\"value\\\":\\\"T67792\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"other\\\",\\\"value\\\":\\\"A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}]}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V2_incomplete() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"kind\\\":\\\"SERVER\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1510256710021866,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"},\\\"tags\\\":{\\\"http.path\\\":\\\"/rs/A\\\",\\\"location\\\":\\\"T67792\\\",\\\"other\\\":\\\"A\\\"}}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_PROTO3_incomplete() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(106);\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V1_shared() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V1.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"name\\\":\\\"get\\\",\\\"annotations\\\":[{\\\"timestamp\\\":1510256710021866,\\\"value\\\":\\\"sr\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"timestamp\\\":1510256710022983,\\\"value\\\":\\\"ss\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}],\\\"binaryAnnotations\\\":[{\\\"key\\\":\\\"http.path\\\",\\\"value\\\":\\\"/rs/A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"location\\\",\\\"value\\\":\\\"T67792\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}},{\\\"key\\\":\\\"other\\\",\\\"value\\\":\\\"A\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"}}]}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_JSON_V2_shared() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8))\n        .isEqualTo(\n            \"{\\\"traceId\\\":\\\"dc955a1d4768875d\\\",\\\"id\\\":\\\"dc955a1d4768875d\\\",\\\"kind\\\":\\\"SERVER\\\",\\\"name\\\":\\\"get\\\",\\\"timestamp\\\":1510256710021866,\\\"duration\\\":1117,\\\"localEndpoint\\\":{\\\"serviceName\\\":\\\"isao01\\\",\\\"ipv4\\\":\\\"10.23.14.72\\\"},\\\"tags\\\":{\\\"http.path\\\":\\\"/rs/A\\\",\\\"location\\\":\\\"T67792\\\",\\\"other\\\":\\\"A\\\"},\\\"shared\\\":true}\");\n  }\n\n  @Test void noAnnotations_rootServerSpan_PROTO3_shared() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesEncoder.PROTO3.encode(span)).hasSize(111);\n  }\n\n  @Test void span_THRIFT() {\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(503);\n  }\n\n  @Test void localSpan_THRIFT() {\n    assertThat(SpanBytesEncoder.THRIFT.encode(LOCAL_SPAN)).hasSize(127);\n  }\n\n  @Test void span_64bitTraceId_THRIFT() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(492);\n  }\n\n  @Test void span_shared_THRIFT() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(481);\n  }\n\n  @Test void specialCharsInJson_THRIFT() {\n    span = UTF8_SPAN;\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(176);\n  }\n\n  @Test void span_minimum_THRIFT() {\n    span =\n        Span.newBuilder()\n            .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n            .id(\"5b4185666d50f68b\")\n            .build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(57);\n  }\n\n  @Test void span_noLocalServiceName_THRIFT() {\n    span = span.toBuilder().localEndpoint(FRONTEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(455);\n  }\n\n  @Test void span_noRemoteServiceName_THRIFT() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(496);\n  }\n\n  @Test void noAnnotations_rootServerSpan_THRIFT() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(358);\n  }\n\n  @Test void noAnnotations_rootServerSpan_THRIFT_incomplete() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(297);\n  }\n\n  @Test void noAnnotations_rootServerSpan_THRIFT_shared() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesEncoder.THRIFT.encode(span)).hasSize(336);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/codec/V1SpanBytesDecoderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.codec;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.TRACE;\nimport static zipkin2.codec.SpanBytesEncoderTest.LOCAL_SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.NO_ANNOTATIONS_ROOT_SERVER_SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.SPAN;\nimport static zipkin2.codec.SpanBytesEncoderTest.UTF8_SPAN;\n\n/** V1 tests for {@link SpanBytesDecoderTest} */\nclass V1SpanBytesDecoderTest {\n  Span span = SPAN;\n\n  @Test void niceErrorOnTruncatedSpans_THRIFT() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      byte[] encoded = SpanBytesEncoder.THRIFT.encodeList(TRACE);\n      SpanBytesDecoder.THRIFT.decodeList(Arrays.copyOfRange(encoded, 0, 10));\n    });\n    assertThat(exception.getMessage()).contains(\"Truncated: length 8 > bytes available 2 reading List<Span> from TBinary\");\n  }\n\n  @Test void niceErrorOnTruncatedSpan_THRIFT() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      byte[] encoded = SpanBytesEncoder.THRIFT.encode(SPAN);\n      SpanBytesDecoder.THRIFT.decodeOne(Arrays.copyOfRange(encoded, 0, 10));\n    });\n    assertThat(exception.getMessage()).contains(\"Truncated: length 8 > bytes available 7 reading Span from TBinary\");\n  }\n\n  @Test void emptyListOk_THRIFT() {\n    assertThat(SpanBytesDecoder.THRIFT.decodeList(new byte[0]))\n      .isEmpty(); // instead of throwing an exception\n\n    byte[] emptyListLiteral = {12 /* TYPE_STRUCT */, 0, 0, 0, 0 /* zero length */};\n    assertThat(SpanBytesDecoder.THRIFT.decodeList(emptyListLiteral))\n      .isEmpty(); // instead of throwing an exception\n  }\n\n  @Test void spanRoundTrip_JSON_V1() {\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n        .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_THRIFT() {\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n        .isEqualTo(span);\n  }\n\n  @Test void localSpanRoundTrip_JSON_V1() {\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(LOCAL_SPAN)))\n        .isEqualTo(LOCAL_SPAN);\n  }\n\n  @Test void localSpanRoundTrip_THRIFT() {\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(LOCAL_SPAN)))\n        .isEqualTo(LOCAL_SPAN);\n  }\n\n  @Test void spanRoundTrip_64bitTraceId_JSON_V1() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n        .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_64bitTraceId_THRIFT() {\n    span = span.toBuilder().traceId(span.traceId().substring(16)).build();\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n        .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_shared_JSON_V1() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n        .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_shared_THRIFT() {\n    span = span.toBuilder().kind(Span.Kind.SERVER).shared(true).build();\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n        .isEqualTo(span);\n  }\n\n  /**\n   * This isn't a test of what we \"should\" accept as a span, rather that characters that trip-up\n   * json don't fail in codec.\n   */\n  @Test void specialCharsInJson_JSON_V1() {\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(UTF8_SPAN)))\n        .isEqualTo(UTF8_SPAN);\n  }\n\n  @Test void specialCharsInJson_THRIFT() {\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(UTF8_SPAN)))\n        .isEqualTo(UTF8_SPAN);\n  }\n\n  @Test void falseOnEmpty_inputSpans_JSON_V1() {\n    assertThat(SpanBytesDecoder.JSON_V1.decodeList(new byte[0], new ArrayList<>())).isFalse();\n  }\n\n  @Test void falseOnEmpty_inputSpans_THRIFT() {\n    assertThat(SpanBytesDecoder.THRIFT.decodeList(new byte[0], new ArrayList<>())).isFalse();\n  }\n\n  /**\n   * Particular, thrift can mistake malformed content as a huge list. Let's not blow up.\n   */\n  @Test void niceErrorOnMalformed_inputSpans_JSON_V1() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      SpanBytesDecoder.JSON_V1.decodeList(new byte[] {'h', 'e', 'l', 'l', 'o'});\n    });\n    assertThat(exception.getMessage()).contains(\"Malformed reading List<Span> from \");\n  }\n\n  @Test void niceErrorOnMalformed_inputSpans_THRIFT() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      SpanBytesDecoder.THRIFT.decodeList(new byte[] {'h', 'e', 'l', 'l', 'o'});\n    });\n    assertThat(exception.getMessage()).contains(\"Truncated: length 1 > bytes available 0 reading List<Span> from TBinary\");\n  }\n\n  @Test void traceRoundTrip_JSON_V1() {\n    byte[] message = SpanBytesEncoder.JSON_V1.encodeList(TRACE);\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeList(message)).isEqualTo(TRACE);\n  }\n\n  @Test void traceRoundTrip_THRIFT() {\n    byte[] message = SpanBytesEncoder.THRIFT.encodeList(TRACE);\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeList(message)).isEqualTo(TRACE);\n  }\n\n  @Test void spansRoundTrip_JSON_V1() {\n    List<Span> tenClientSpans = Collections.nCopies(10, span);\n\n    byte[] message = SpanBytesEncoder.JSON_V1.encodeList(tenClientSpans);\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeList(message)).isEqualTo(tenClientSpans);\n  }\n\n  @Test void spansRoundTrip_THRIFT() {\n    List<Span> tenClientSpans = Collections.nCopies(10, span);\n\n    byte[] message = SpanBytesEncoder.THRIFT.encodeList(tenClientSpans);\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeList(message)).isEqualTo(tenClientSpans);\n  }\n\n  @Test void spanRoundTrip_noRemoteServiceName_JSON_V1() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noRemoteServiceName_THRIFT() {\n    span = span.toBuilder().remoteEndpoint(BACKEND.toBuilder().serviceName(null).build()).build();\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_JSON_V1() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_THRIFT() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN;\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_incomplete_JSON_V1() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_incomplete_THRIFT() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().duration(null).build();\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_shared_JSON_V1() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(SpanBytesEncoder.JSON_V1.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test void spanRoundTrip_noAnnotations_rootServerSpan_shared_THRIFT() {\n    span = NO_ANNOTATIONS_ROOT_SERVER_SPAN.toBuilder().shared(true).build();\n\n    assertThat(SpanBytesDecoder.THRIFT.decodeOne(SpanBytesEncoder.THRIFT.encode(span)))\n      .isEqualTo(span);\n  }\n\n  @Test\n  @Disabled\n  void niceErrorOnUppercase_traceId_JSON_V1() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": \"48485A3953BB6124\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"48485A3953BB6124 should be lower-hex encoded with no prefix\");\n  }\n\n  @Test void readsTraceIdHighFromTraceIdField() {\n    byte[] with128BitTraceId =\n      (\"\"\"\n        {\n          \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\")\n        .getBytes(UTF_8);\n    byte[] withLower64bitsTraceId =\n      (\"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\")\n        .getBytes(UTF_8);\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(with128BitTraceId))\n      .isEqualTo(\n        SpanBytesDecoder.JSON_V1\n          .decodeOne(withLower64bitsTraceId)\n          .toBuilder()\n          .traceId(\"48485a3953bb61246b221d5bc9e6496c\")\n          .build());\n  }\n\n  @Test void ignoresNull_topLevelFields() {\n    String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"parentId\": null,\n          \"id\": \"6b221d5bc9e6496c\",\n          \"name\": null,\n          \"timestamp\": null,\n          \"duration\": null,\n          \"annotations\": null,\n          \"binaryAnnotations\": null,\n          \"debug\": null,\n          \"shared\": null\n        }\n        \"\"\";\n\n    SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n  }\n\n  @Test void ignoresNull_endpoint_topLevelFields() {\n    String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"binaryAnnotations\": [\n            {\n              \"key\": \"lc\",\n              \"value\": \"\",\n              \"endpoint\": {\n                \"serviceName\": null,\n            \"ipv4\": \"127.0.0.1\",\n                \"ipv6\": null,\n                \"port\": null\n              }\n            }\n          ]\n        }\n        \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8)).localEndpoint())\n        .isEqualTo(Endpoint.newBuilder().ip(\"127.0.0.1\").build());\n  }\n\n  @Test void skipsIncompleteEndpoint() {\n    String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"binaryAnnotations\": [\n            {\n              \"key\": \"lc\",\n              \"value\": \"\",\n              \"endpoint\": {\n                \"serviceName\": null,\n                \"ipv4\": null,\n                \"ipv6\": null,\n                \"port\": null\n              }\n            }\n          ]\n        }\n        \"\"\";\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8)).localEndpoint()).isNull();\n    json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"binaryAnnotations\": [\n          {\n            \"key\": \"lc\",\n            \"value\": \"\",\n            \"endpoint\": {\n            }\n          }\n        ]\n      }\n      \"\"\";\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8)).localEndpoint()).isNull();\n  }\n\n  @Test void ignoresNonAddressBooleanBinaryAnnotations() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"binaryAnnotations\": [\n          {\n            \"key\": \"aa\",\n            \"value\": true,\n            \"endpoint\": {\n              \"serviceName\": \"foo\"\n            }\n          }\n        ]\n      }\n      \"\"\";\n\n    Span decoded = SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    assertThat(decoded.tags()).isEmpty();\n    assertThat(decoded.localEndpoint()).isNull();\n    assertThat(decoded.remoteEndpoint()).isNull();\n  }\n\n  @Test void niceErrorOnIncomplete_annotation() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": 1472470996199000}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"Incomplete annotation at $.annotations[0].timestamp\");\n  }\n\n  @Test void niceErrorOnNull_traceId() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": null,\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"Expected a string but was NULL\");\n  }\n\n  @Test void niceErrorOnNull_id() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": null\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"Expected a string but was NULL\");\n  }\n\n  @Test void niceErrorOnNull_annotationValue() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": 1472470996199000, \"value\": NULL}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"$.annotations[0].value\");\n  }\n\n  @Test void niceErrorOnNull_annotationTimestamp() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"annotations\": [\n            { \"timestamp\": NULL, \"value\": \"foo\"}\n          ]\n        }\n        \"\"\";\n\n      SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8));\n    });\n    assertThat(exception.getMessage()).contains(\"$.annotations[0].timestamp\");\n  }\n\n  @Test void readSpan_localEndpoint_noServiceName() {\n    String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"localEndpoint\": {\n            \"ipv4\": \"127.0.0.1\"\n          }\n        }\n        \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8)).localServiceName())\n        .isNull();\n  }\n\n  @Test void readSpan_remoteEndpoint_noServiceName() {\n    String json =\n        \"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\",\n          \"remoteEndpoint\": {\n            \"ipv4\": \"127.0.0.1\"\n          }\n        }\n        \"\"\";\n\n    assertThat(SpanBytesDecoder.JSON_V1.decodeOne(json.getBytes(UTF_8)).remoteServiceName())\n        .isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/AggregateCallTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.DependencyLink;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass AggregateCallTest {\n\n  @Mock Call<Void> call1, call2;\n  @Mock Callback<Void> callback;\n\n  @Test void newVoidCall_emptyNotAllowed() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      AggregateCall.newVoidCall(List.of());\n    });\n  }\n\n  @Test void newVoidCall_singletonReturnsOnlyElement() {\n    assertThat(AggregateCall.newVoidCall(List.of(call1)))\n      .isEqualTo(call1);\n  }\n\n  @Test void newVoidCall_joinsMultipleCalls() {\n    assertThat(AggregateCall.newVoidCall(List.of(call1, call2)))\n      .isInstanceOf(AggregateCall.AggregateVoidCall.class)\n      .extracting(\"delegate\")\n      .isEqualTo(List.of(call1, call2));\n  }\n\n  @Test void execute() throws Exception {\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n\n    assertThat(call.execute())\n      .isNull();\n\n    verify(call1).execute();\n    verify(call2).execute();\n    verifyNoMoreInteractions(call1, call2);\n  }\n\n  @Test void enqueue() {\n    successCallback(call1);\n    successCallback(call2);\n\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n\n    call.enqueue(callback);\n\n    verify(callback).onSuccess(null);\n\n    verify(call1).enqueue(any(Callback.class));\n    verify(call2).enqueue(any(Callback.class));\n    verifyNoMoreInteractions(call1, call2);\n  }\n\n  @Test void enqueue_cancel() {\n    call1 = Call.create(null);\n    call2 = Call.create(null);\n\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n    call.cancel();\n\n    assertThat(call.isCanceled()).isTrue();\n    assertThat(call1.isCanceled()).isTrue();\n    assertThat(call2.isCanceled()).isTrue();\n\n    call.enqueue(callback);\n\n    verify(callback).onError(isA(IOException.class));\n  }\n\n  @Test void executesOnce() throws Exception {\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n    call.execute();\n\n    assertThatThrownBy(call::execute)\n      .isInstanceOf(IllegalStateException.class);\n\n    assertThatThrownBy(() -> call.enqueue(callback))\n      .isInstanceOf(IllegalStateException.class);\n  }\n\n  @Test void enqueuesOnce() {\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n    call.enqueue(callback);\n\n    assertThatThrownBy(() -> call.enqueue(callback))\n      .isInstanceOf(IllegalStateException.class);\n\n    assertThatThrownBy(call::execute)\n      .isInstanceOf(IllegalStateException.class);\n  }\n\n  @Test void execute_errorDoesntStopOtherCalls() throws Exception {\n    Exception e = new IllegalArgumentException();\n    when(call1.execute()).thenThrow(e);\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n\n    try {\n      call.execute();\n      failBecauseExceptionWasNotThrown(e.getClass());\n    } catch (IllegalArgumentException ex) {\n    }\n\n    verify(call1).execute();\n    verify(call2).execute();\n    verifyNoMoreInteractions(call1, call2);\n  }\n\n  /** This shows that regardless of success or error, we block on completion */\n  @Test @Timeout(1000L) void enqueue_blocksOnCompletion() throws InterruptedException {\n    CountDownLatch callsLatch = new CountDownLatch(10);\n    ExecutorService exec = Executors.newFixedThreadPool(10);\n\n    class LatchCall extends Call.Base<Void> {\n      final boolean fail;\n\n      LatchCall(boolean fail) {\n        this.fail = fail;\n      }\n\n      @Override protected Void doExecute() {\n        throw new UnsupportedOperationException();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        exec.submit(() -> {\n          try {\n            callsLatch.await();\n          } catch (InterruptedException e) {\n            callback.onError(e);\n          }\n          if (fail) {\n            callback.onError(new IOException());\n          } else {\n            callback.onSuccess(null);\n          }\n        });\n      }\n\n      @Override public Call<Void> clone() {\n        return new LatchCall(fail);\n      }\n    }\n\n    List<Call<Void>> calls = new ArrayList<>();\n    for (int i = 0; i < 10; i++) calls.add(new LatchCall(i % 2 == 0));\n    Call<Void> call = AggregateCall.newVoidCall(calls);\n\n    AtomicReference<Object> result = new AtomicReference<>();\n    call.enqueue(new Callback<Void>() {\n      @Override public void onSuccess(Void value) {\n        result.set(\"foo\");\n      }\n\n      @Override public void onError(Throwable t) {\n        result.set(t);\n      }\n    });\n\n    assertThat(result).hasValue(null);\n    callsLatch.countDown(); // one down\n    assertThat(result).hasValue(null);\n    callsLatch.countDown(); // two down\n    assertThat(result).hasValue(null);\n    for (int i = 0; i < 8; i++) callsLatch.countDown();\n\n    // wait for threads to finish\n    exec.shutdown();\n    exec.awaitTermination(1, TimeUnit.SECONDS);\n\n    assertThat(result.get()).isNotNull();\n  }\n\n  @Test void enqueue_errorDoesntStopOtherCalls() {\n    Exception e = new IllegalArgumentException();\n    errorCallback(call1, e);\n    successCallback(call2);\n\n    Call<Void> call = AggregateCall.newVoidCall(List.of(call1, call2));\n\n    call.enqueue(callback);\n\n    verify(callback).onError(e);\n\n    verify(call1).enqueue(any(Callback.class));\n    verify(call2).enqueue(any(Callback.class));\n    verifyNoMoreInteractions(call1, call2);\n  }\n\n  static void successCallback(Call<Void> call) {\n    doAnswer(a -> {\n      ((Callback<Void>) a.getArgument(0)).onSuccess(null);\n      return a;\n    }).when(call).enqueue(any(Callback.class));\n  }\n\n  static void errorCallback(Call<Void> call, Exception e) {\n    doAnswer(a -> {\n      ((Callback<Void>) a.getArgument(0)).onError(e);\n      return a;\n    }).when(call).enqueue(any(Callback.class));\n  }\n\n  @Test void execute_finish() throws Exception {\n    Call<List<DependencyLink>> call = new AggregateDependencyLinks(List.of(\n      Call.create(List.of(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(1).build())),\n      Call.create(List.of(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(3).build()))\n    ));\n\n    assertThat(call.execute())\n      .containsExactly(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(4).build());\n  }\n\n  @Test void enqueue_finish() {\n    Call<List<DependencyLink>> call = new AggregateDependencyLinks(List.of(\n      Call.create(List.of(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(1).build())),\n      Call.create(List.of(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(3).build()))\n    ));\n\n    Callback<List<DependencyLink>> callback = mock(Callback.class);\n\n    call.enqueue(callback);\n\n    verify(callback)\n      .onSuccess(List.of(DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(4).build()));\n  }\n\n  static final class AggregateDependencyLinks\n    extends AggregateCall<List<DependencyLink>, List<DependencyLink>> {\n    AggregateDependencyLinks(List<Call<List<DependencyLink>>> calls) {\n      super(calls);\n    }\n\n    @Override protected List<DependencyLink> newOutput() {\n      return new ArrayList<>();\n    }\n\n    @Override protected void append(List<DependencyLink> input, List<DependencyLink> output) {\n      output.addAll(input);\n    }\n\n    // this is the part we are testing, that we can peform a finish step\n    @Override protected List<DependencyLink> finish(List<DependencyLink> done) {\n      return DependencyLinker.merge(done);\n    }\n\n    @Override public AggregateDependencyLinks clone() {\n      return new AggregateDependencyLinks(cloneCalls());\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/DateUtilTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.TimeZone;\nimport org.junit.jupiter.api.Test;\n\nimport static java.util.concurrent.TimeUnit.DAYS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.internal.DateUtil.midnightUTC;\n\nclass DateUtilTest {\n\n  @Test void midnightUTCTest() throws ParseException {\n\n    DateFormat iso8601 = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    iso8601.setTimeZone(TimeZone.getTimeZone(\"UTC\"));\n\n    Date date = iso8601.parse(\"2011-04-15T20:08:18Z\");\n\n    long midnight = midnightUTC(date.getTime());\n\n    assertThat(iso8601.format(new Date(midnight))).isEqualTo(\"2011-04-15T00:00:00Z\");\n  }\n\n  @Test void getDays() {\n    assertThat(DateUtil.epochDays(DAYS.toMillis(2), DAYS.toMillis(1)))\n        .containsExactly(DAYS.toMillis(1), DAYS.toMillis(2));\n  }\n\n  /** Looking back earlier than 1970 is likely a bug */\n  @Test void getDays_doesntLookEarlierThan1970() {\n    assertThat(DateUtil.epochDays(DAYS.toMillis(2), DAYS.toMillis(3)))\n        .containsExactly(0L, DAYS.toMillis(1), DAYS.toMillis(2));\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/DelayLimiterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.stream.LongStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport zipkin2.internal.DelayLimiter.SuppressionFactory;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass DelayLimiterTest {\n  static final long NANOS_PER_SECOND = SECONDS.toNanos(1L);\n  long nanoTime;\n  SuppressionFactory suppressionFactory = new SuppressionFactory(NANOS_PER_SECOND) {\n    @Override long nanoTime() {\n      return nanoTime;\n    }\n  };\n  DelayLimiter<Long> delayLimiter = new DelayLimiter<>(suppressionFactory, 1000);\n\n  @Test void mutesDuringDelayPeriod() {\n    nanoTime = NANOS_PER_SECOND;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n\n    nanoTime += NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isFalse();\n\n    nanoTime += NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n  }\n\n  @Test void contextsAreIndependent() {\n    nanoTime = NANOS_PER_SECOND;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n\n    nanoTime += NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isFalse();\n    assertThat(delayLimiter.shouldInvoke(1L)).isTrue();\n\n    nanoTime += NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n    assertThat(delayLimiter.shouldInvoke(1L)).isFalse();\n  }\n\n  @Test void worksOnRollover() {\n    nanoTime = -NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n\n    nanoTime = 0L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isFalse();\n\n    nanoTime = NANOS_PER_SECOND / 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n  }\n\n  @Test void worksOnSameNanos() {\n    nanoTime = NANOS_PER_SECOND;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n\n    nanoTime = NANOS_PER_SECOND * 2L;\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue();\n    assertThat(delayLimiter.shouldInvoke(0L)).isFalse();\n  }\n\n  @Test @Timeout(1000L) void cardinality() {\n    long count = delayLimiter.cardinality * 10L;\n    for (long i = 0L; i < count; i++, nanoTime++) {\n      assertThat(delayLimiter.shouldInvoke(i)).isTrue();\n    }\n    assertThat(delayLimiter.shouldInvoke(0L)).isTrue(); // eldest evicted\n    assertThat(delayLimiter.shouldInvoke(count - 1L)).isFalse(); // youngest not evicted\n\n    // verify internal state\n    assertThat(delayLimiter.cache)\n      .hasSameSizeAs(delayLimiter.suppressions)\n      .hasSize(delayLimiter.cardinality);\n  }\n\n  @Test @Timeout(2000L) void cardinality_parallel() throws InterruptedException {\n    AtomicLong trueCount = new AtomicLong();\n    ExecutorService exec = Executors.newFixedThreadPool(4);\n\n    long count = delayLimiter.cardinality * 10L;\n    LongStream.range(0L, count).forEach(i -> exec.execute(() -> {\n      if (delayLimiter.shouldInvoke(i)) trueCount.incrementAndGet();\n    }));\n\n    exec.shutdown();\n    assertThat(exec.awaitTermination(1L, SECONDS)).isTrue();\n\n    assertThat(trueCount).hasValue(count);\n\n    // verify internal state\n    assertThat(delayLimiter.cache)\n      .hasSameSizeAs(delayLimiter.suppressions)\n      .hasSize(delayLimiter.cardinality);\n  }\n\n  @Test void ttl_cantBeNegative() {\n    DelayLimiter.Builder builder = DelayLimiter.newBuilder().ttl(-1, SECONDS);\n\n    assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class);\n  }\n\n  @Test void ttl_cantBeZero() {\n    DelayLimiter.Builder builder = DelayLimiter.newBuilder().ttl(0, SECONDS);\n\n    assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class);\n  }\n\n  @Test void cardinality_cantBeNegative() {\n    DelayLimiter.Builder builder = DelayLimiter.newBuilder().ttl(1L, SECONDS).cardinality(-1);\n\n    assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class);\n  }\n\n  @Test void cardinality_cantBeZero() {\n    DelayLimiter.Builder builder = DelayLimiter.newBuilder().ttl(1L, SECONDS).cardinality(0);\n\n    assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/DependenciesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.nio.ByteBuffer;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.DependencyLink;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nfinal class DependenciesTest {\n  @Test void dependenciesRoundTrip() {\n    DependencyLink ab = DependencyLink.newBuilder().parent(\"a\").child(\"b\").callCount(2L).build();\n    DependencyLink cd = DependencyLink.newBuilder().parent(\"c\").child(\"d\").errorCount(2L).build();\n\n    Dependencies dependencies = Dependencies.create(1L, 2L, List.of(ab, cd));\n\n    ByteBuffer bytes = dependencies.toThrift();\n    assertThat(Dependencies.fromThrift(bytes)).isEqualTo(dependencies);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/DependencyLinkerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass DependencyLinkerTest {\n  // in reverse order as reporting is more likely to occur this way\n  static final List<Span> TRACE = List.of(\n    span(\"a\", \"b\", \"c\", Kind.CLIENT, \"app\", \"db\", true),\n    span(\"a\", \"a\", \"b\", Kind.SERVER, \"app\", \"web\", false)\n      .toBuilder().shared(true).build(),\n    span(\"a\", \"a\", \"b\", Kind.CLIENT, \"web\", \"app\", false),\n    span(\"a\", null, \"a\", Kind.SERVER, \"web\", null, false)\n  );\n\n  List<String> messages = new ArrayList<>();\n\n  Logger logger = new Logger(\"\", null) {\n    {\n      setLevel(Level.ALL);\n    }\n\n    @Override public void log(Level level, String msg) {\n      assertThat(level).isEqualTo(Level.FINE);\n      messages.add(msg);\n    }\n  };\n\n  @Test void baseCase() {\n    assertThat(new DependencyLinker().link()).isEmpty();\n  }\n\n  @Test void linksSpans() {\n    assertThat(new DependencyLinker().putTrace(TRACE).link()).containsExactly(\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"app\").child(\"db\").callCount(1L).errorCount(1L).build()\n    );\n  }\n\n  /**\n   * Trace id is not required to be a span id. For example, some instrumentation may create separate\n   * trace ids to help with collisions, or to encode information about the origin. This test makes\n   * sure we don't rely on the trace id = root span id convention.\n   */\n  @Test void traceIdIsOpaque() {\n    List<DependencyLink> links = new DependencyLinker().putTrace(TRACE).link();\n\n    List<Span> differentTraceId = TRACE.stream()\n      .map(s -> s.toBuilder().traceId(\"123\").build())\n      .collect(toList());\n\n    assertThat(new DependencyLinker().putTrace(differentTraceId).link())\n      .containsExactlyElementsOf(links);\n  }\n\n  /**\n   * Some don't propagate the server's parent ID which creates a race condition. Try to unwind it.\n   *\n   * <p>See https://github.com/openzipkin/zipkin/pull/1745\n   */\n  @Test void linksSpans_serverMissingParentId() {\n    List<Span> trace = Arrays.asList( // to allow sorting\n      span(\"a\", null, \"a\", Kind.SERVER, \"arn\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"arn\", \"link\", false),\n      // below the parent ID is null as it wasn't propagated\n      span(\"a\", null, \"b\", Kind.SERVER, \"link\", \"arn\", false)\n        .toBuilder().shared(true).build()\n    );\n\n    // trace is actually reported in reverse order\n    Collections.reverse(trace);\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsExactly(\n      DependencyLink.newBuilder().parent(\"arn\").child(\"link\").callCount(1L).build()\n    );\n  }\n\n  /** In case of a late error, we should know which trace ID is being processed */\n  @Test void logsTraceId() {\n    new DependencyLinker(logger).putTrace(TRACE);\n\n    assertThat(messages)\n      .contains(\"building trace tree: traceId=000000000000000a\");\n  }\n\n  /**\n   * This test shows that if a parent ID is stored late (ex because it wasn't propagated), the span\n   * can resolve once it is.\n   */\n  @Test void lateParentIdInSharedSpan() {\n    List<Span> withLateParent = new ArrayList<>(TRACE);\n    withLateParent.set(2, TRACE.get(2).toBuilder().parentId(null).build());\n\n    assertThat(new DependencyLinker().putTrace(withLateParent).link()).containsExactly(\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"app\").child(\"db\").callCount(1L).errorCount(1L).build()\n    );\n  }\n\n  /**\n   * This test shows that if a parent ID is stored late (ex because it wasn't propagated), the span\n   * can resolve even if the client side is never sent\n   */\n  @Test void lostChildAndNoParentIdInSharedSpan() {\n    List<Span> lostClientOrphan = new ArrayList<>(TRACE);\n    lostClientOrphan.set(2, TRACE.get(2).toBuilder().parentId(null).build());\n    lostClientOrphan.remove(1); // client span never sent\n\n    assertThat(new DependencyLinker().putTrace(lostClientOrphan).link()).containsExactly(\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"app\").child(\"db\").callCount(1L).errorCount(1L).build()\n    );\n  }\n\n  @Test void messagingSpansDontLinkWithoutBroker_consumer() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", \"kafka\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"kafka\").child(\"consumer\").callCount(1L).build()\n    );\n  }\n\n  @Test void messagingSpansDontLinkWithoutBroker_producer() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", \"kafka\", false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"producer\").child(\"kafka\").callCount(1L).build()\n    );\n  }\n\n  @Test void messagingWithBroker_both_sides_same() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", \"kafka\", false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", \"kafka\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"producer\").child(\"kafka\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"kafka\").child(\"consumer\").callCount(1L).build()\n    );\n  }\n\n  @Test void messagingWithBroker_different() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", \"kafka1\", false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", \"kafka2\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"producer\").child(\"kafka1\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"kafka2\").child(\"consumer\").callCount(1L).build()\n    );\n  }\n\n  /** Shows we don't assume there's a direct link between producer and consumer. */\n  @Test void messagingWithoutBroker_noLinks() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link())\n      .isEmpty();\n  }\n\n  /** When a server is the child of a producer span, make a link as it is really an RPC */\n  @Test void producerLinksToServer_childSpan() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"producer\").child(\"server\").callCount(1L).build()\n    );\n  }\n\n  /**\n   * Servers most often join a span vs create a child. Make sure this works when a producer is used\n   * instead of a client.\n   */\n  @Test void producerLinksToServer_sameSpan() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.PRODUCER, \"producer\", null, false),\n      span(\"a\", null, \"a\", Kind.SERVER, \"server\", null, false)\n        .toBuilder().shared(true).build()\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"producer\").child(\"server\").callCount(1L).build()\n    );\n  }\n\n  /**\n   * Client might be used for historical reasons instead of PRODUCER. Don't link as the server-side\n   * is authoritative.\n   */\n  @Test void clientDoesntLinkToConsumer_child() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CONSUMER, \"consumer\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link())\n      .isEmpty();\n  }\n\n  /**\n   * A root span can be a client-originated trace or a server receipt which knows its peer. In these\n   * cases, the peer is known and kind establishes the direction.\n   */\n  @Test void linksSpansDirectedByKind() {\n    List<Span> validRootSpans = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"server\", \"client\", false),\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", \"server\", false)\n        .toBuilder().shared(true).build()\n    );\n\n    for (Span span : validRootSpans) {\n      assertThat(new DependencyLinker().putTrace(List.of(span)).link()).containsOnly(\n        DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(1L).build()\n      );\n    }\n  }\n\n  @Test void callsAgainstTheSameLinkIncreasesCallCount_span() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, null, \"server\", false),\n      span(\"a\", \"a\", \"c\", Kind.CLIENT, null, \"server\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build()\n    );\n  }\n\n  @Test void callsAgainstTheSameLinkIncreasesCallCount_trace() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, null, \"server\", false)\n    );\n\n    assertThat(new DependencyLinker()\n      .putTrace(trace)\n      .putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build()\n    );\n  }\n\n  /**\n   * Spans don't always include both the client and server service. When you know the kind, you can\n   * link these without duplicating call count.\n   */\n  @Test void singleHostSpansResultInASingleCallCount() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(1L).build()\n    );\n  }\n\n  @Test void singleHostSpansResultInASingleErrorCount() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", null, true)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(\"client\")\n        .child(\"server\")\n        .callCount(1L)\n        .errorCount(1L)\n        .build()\n    );\n  }\n\n  @Test void singleHostSpansResultInASingleErrorCount_sameId() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, true),\n      span(\"a\", null, \"a\", Kind.SERVER, \"server\", null, true)\n        .toBuilder().shared(true).build()\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(\"client\")\n        .child(\"server\")\n        .callCount(1L)\n        .errorCount(1L)\n        .build()\n    );\n  }\n\n  @Test void singleHostSpansResultInASingleCallCount_defersNameToServer() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", \"server\", false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", null, false)\n    );\n\n    assertThat(new DependencyLinker(logger).putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(1L).build()\n    );\n  }\n\n  @Test void singleHostSpans_multipleChildren() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", \"client\", true),\n      span(\"a\", \"a\", \"c\", Kind.SERVER, \"server\", \"client\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(\"client\")\n        .child(\"server\")\n        .callCount(2L)\n        .errorCount(1L)\n        .build()\n    );\n  }\n\n  @Test void singleHostSpans_multipleChildren_defersNameToServer() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", \"server\", false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"server\", null, false),\n      span(\"a\", \"a\", \"c\", Kind.SERVER, \"server\", null, false)\n    );\n\n    assertThat(new DependencyLinker(logger).putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build()\n    );\n  }\n\n  /**\n   * Spans are sometimes intermediated by an unknown type of span. Prefer the nearest server when\n   * accounting for them.\n   */\n  @Test void intermediatedClientSpansMissingLocalServiceNameLinkToNearestServer() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", null, null, null, false),\n      // possibly a local fan-out span\n      span(\"a\", \"b\", \"c\", Kind.CLIENT, \"server\", null, false),\n      span(\"a\", \"b\", \"d\", Kind.CLIENT, \"server\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build()\n    );\n  }\n\n  @Test void errorsOnUninstrumentedLinks() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"client\", null, false),\n      span(\"a\", \"a\", \"b\", null, null, null, false),\n      // there's no remote here, so we shouldn't see any error count\n      span(\"a\", \"b\", \"c\", Kind.CLIENT, \"server\", null, true),\n      span(\"a\", \"b\", \"d\", Kind.CLIENT, \"server\", null, true)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build()\n    );\n  }\n\n  @Test void errorsOnInstrumentedLinks() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"foo\", null, false),\n      span(\"a\", \"a\", \"b\", null, null, null, false),\n      span(\"a\", \"b\", \"c\", Kind.CLIENT, \"bar\", \"baz\", true),\n      span(\"a\", \"b\", \"d\", Kind.CLIENT, \"bar\", \"baz\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(2L).build(),\n      DependencyLink.newBuilder().parent(\"bar\").child(\"baz\").callCount(2L).errorCount(1L).build()\n    );\n  }\n\n  @Test void linkWithErrorIsLogged() {\n    List<Span> trace = List.of(\n        span(\"a\", \"b\", \"c\", Kind.CLIENT, \"foo\", \"bar\", true)\n    );\n    new DependencyLinker(logger).putTrace(trace).link();\n\n    assertThat(messages).contains(\n      \"incrementing error link foo -> bar\"\n    );\n  }\n\n  /** Tag indicates a failed span, not an annotation */\n  @Test void annotationNamedErrorDoesntIncrementErrorCount() {\n    List<Span> trace = List.of(\n        span(\"a\", \"b\", \"c\", Kind.CLIENT, \"foo\", \"bar\", false)\n            .toBuilder().addAnnotation(1L, \"error\").build()\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(1L).build()\n    );\n  }\n\n  /** A loopback span is direction-agnostic, so can be linked properly regardless of kind. */\n  @Test void linksLoopbackSpans() {\n    List<Span> validRootSpans = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"service\", \"service\", false),\n      span(\"b\", null, \"b\", Kind.CLIENT, \"service\", \"service\", false)\n    );\n\n    for (Span span : validRootSpans) {\n      assertThat(new DependencyLinker().putTrace(List.of(span)).link()).containsOnly(\n        DependencyLink.newBuilder().parent(\"service\").child(\"service\").callCount(1L).build()\n      );\n    }\n  }\n\n  @Test void noSpanKindTreatedSameAsClient() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", null, \"some-client\", \"web\", false),\n      span(\"a\", \"a\", \"b\", null, \"web\", \"app\", false),\n      span(\"a\", \"b\", \"c\", null, \"app\", \"db\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"some-client\").child(\"web\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"app\").child(\"db\").callCount(1L).build()\n    );\n  }\n\n  @Test void noSpanKindWithError() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", null, \"some-client\", \"web\", false),\n      span(\"a\", \"a\", \"b\", null, \"web\", \"app\", true),\n      span(\"a\", \"b\", \"c\", null, \"app\", \"db\", false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"some-client\").child(\"web\").callCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).errorCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"app\").child(\"db\").callCount(1L).build()\n    );\n  }\n\n  /** A dependency link is between two services. We cannot link if we don't know both service names. */\n  @Test void cannotLinkSingleSpanWithoutBothServiceNames() {\n    List<Span> incompleteRootSpans = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, null, null, false),\n      span(\"a\", null, \"a\", Kind.SERVER, \"server\", null, false),\n      span(\"a\", null, \"a\", Kind.SERVER, null, \"client\", false),\n      span(\"a\", null, \"a\", Kind.CLIENT, null, null, false),\n      span(\"a\", null, \"a\", Kind.CLIENT, \"client\", null, false),\n      span(\"a\", null, \"a\", Kind.CLIENT, null, \"server\", false)\n    );\n\n    for (Span span : incompleteRootSpans) {\n      assertThat(new DependencyLinker(logger)\n        .putTrace(List.of(span)).link())\n        .isEmpty();\n    }\n  }\n\n  @Test void doesntLinkUnrelatedSpansWhenMissingRootSpan() {\n    String missingParentId = \"a\";\n    List<Span> trace = List.of(\n      span(\"a\", missingParentId, \"b\", Kind.SERVER, \"service1\", null, false),\n      span(\"a\", missingParentId, \"c\", Kind.SERVER, \"service2\", null, false)\n    );\n\n    assertThat(new DependencyLinker(logger)\n      .putTrace(trace).link())\n      .isEmpty();\n  }\n\n  @Test void linksRelatedSpansWhenMissingRootSpan() {\n    String missingParentId = \"a\";\n    List<Span> trace = List.of(\n      span(\"a\", missingParentId, \"b\", Kind.SERVER, \"service1\", null, false),\n      span(\"a\", \"b\", \"c\", Kind.SERVER, \"service2\", null, false)\n    );\n\n    assertThat(new DependencyLinker(logger).putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"service1\").child(\"service2\").callCount(1L).build()\n    );\n  }\n\n  /** Client+Server spans that don't share IDs are treated as server spans missing their peer */\n  @Test void linksSingleHostSpans() {\n    List<Span> singleHostSpans = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"web\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"app\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(singleHostSpans).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build()\n    );\n  }\n\n  @Test void linksSingleHostSpans_errorOnClient() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.CLIENT, \"web\", null, true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"app\", null, false)\n    );\n\n    assertThat(new DependencyLinker().putTrace(trace).link()).containsOnly(\n      DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).errorCount(1L).build()\n    );\n  }\n\n  /** Creates a link when there's a span missing, in this case 2L which is an RPC from web to app */\n  @Test void missingSpan() {\n    List<Span> singleHostSpans = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"web\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"app\", null, false)\n    );\n\n    assertThat(new DependencyLinker(logger).putTrace(singleHostSpans).link())\n      .containsOnly(DependencyLink.newBuilder().parent(\"web\").child(\"app\").callCount(1L).build());\n\n    assertThat(messages).contains(\n      \"detected missing link to client span\"\n    );\n  }\n\n  @Test void merge() {\n    List<DependencyLink> links = List.of(\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(2L).errorCount(1L).build(),\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(2L).errorCount(2L).build(),\n      DependencyLink.newBuilder().parent(\"foo\").child(\"foo\").callCount(1L).build()\n    );\n\n    assertThat(DependencyLinker.merge(links)).containsExactly(\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(4L).errorCount(3L).build(),\n      DependencyLink.newBuilder().parent(\"foo\").child(\"foo\").callCount(1L).build()\n    );\n  }\n\n  @Test void merge_error() {\n    List<DependencyLink> links = List.of(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build(),\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(2L).build(),\n      DependencyLink.newBuilder().parent(\"client\").child(\"client\").callCount(1L).build()\n    );\n\n    assertThat(DependencyLinker.merge(links)).containsExactly(\n      DependencyLink.newBuilder().parent(\"client\").child(\"server\").callCount(4L).build(),\n      DependencyLink.newBuilder().parent(\"client\").child(\"client\").callCount(1L).build()\n    );\n  }\n\n  static Span span(String traceId, @Nullable String parentId, String id, @Nullable Kind kind,\n    @Nullable String local, @Nullable String remote, boolean isError) {\n    Span.Builder result = Span.newBuilder().traceId(traceId).parentId(parentId).id(id).kind(kind);\n    if (local != null) result.localEndpoint(Endpoint.newBuilder().serviceName(local).build());\n    if (remote != null) result.remoteEndpoint(Endpoint.newBuilder().serviceName(remote).build());\n    if (isError) result.putTag(\"error\", \"\");\n    return result.build();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/FilterTracesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.storage.QueryRequest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.TODAY;\n\nclass FilterTracesTest {\n  QueryRequest request = QueryRequest.newBuilder().endTs(TODAY).lookback(1).limit(1).build();\n\n  @Test void returnsWhenValidlyMatches() {\n    List<List<Span>> input = new ArrayList<>(List.of(TestObjects.TRACE));\n\n    assertThat(FilterTraces.create(request).map(input)).isEqualTo(input);\n  }\n\n  @Test void doesntMutateInputWhenUnmatched() {\n    List<List<Span>> input = List.of(TestObjects.TRACE);\n\n    assertThat(FilterTraces.create(request.toBuilder().endTs(1).build()).map(input))\n      .isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/HexCodecTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static zipkin2.internal.HexCodec.lowerHexToUnsignedLong;\n\nclass HexCodecTest {\n\n  @Test void lowerHexToUnsignedLong_downgrades128bitIdsByDroppingHighBits() {\n    assertThat(lowerHexToUnsignedLong(\"463ac35c9f6413ad48485a3953bb6124\"))\n        .isEqualTo(lowerHexToUnsignedLong(\"48485a3953bb6124\"));\n  }\n\n  @Test void lowerHexToUnsignedLongTest() {\n    assertThat(lowerHexToUnsignedLong(\"ffffffffffffffff\")).isEqualTo(-1);\n    assertThat(lowerHexToUnsignedLong(\"0\")).isEqualTo(0);\n    assertThat(lowerHexToUnsignedLong(Long.toHexString(Long.MAX_VALUE))).isEqualTo(Long.MAX_VALUE);\n\n    try {\n      lowerHexToUnsignedLong(\"fffffffffffffffffffffffffffffffff\"); // too long\n      failBecauseExceptionWasNotThrown(NumberFormatException.class);\n    } catch (NumberFormatException e) {\n\n    }\n\n    try {\n      lowerHexToUnsignedLong(\"\"); // too short\n      failBecauseExceptionWasNotThrown(NumberFormatException.class);\n    } catch (NumberFormatException e) {\n\n    }\n\n    try {\n      lowerHexToUnsignedLong(\"rs\"); // bad charset\n      failBecauseExceptionWasNotThrown(NumberFormatException.class);\n    } catch (NumberFormatException e) {\n\n    }\n\n    try {\n      lowerHexToUnsignedLong(\"48485A3953BB6124\"); // uppercase\n      failBecauseExceptionWasNotThrown(NumberFormatException.class);\n    } catch (NumberFormatException e) {\n      assertThat(e)\n          .hasMessage(\n              \"48485A3953BB6124 should be a 1 to 32 character lower-hex string with no prefix\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/JsonCodecTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.io.IOException;\nimport org.junit.jupiter.api.Test;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.internal.JsonCodec.exceptionReading;\n\nclass JsonCodecTest {\n\n  @Test void doesntStackOverflowOnToBufferWriterBug_lessThanBytes() {\n    Throwable exception = assertThrows(AssertionError.class, () -> {\n\n      class FooWriter implements WriteBuffer.Writer {\n        @Override public int sizeInBytes(Object value) {\n          return 2;\n        }\n\n        @Override public void write(Object value, WriteBuffer buffer) {\n          buffer.writeByte('a');\n          throw new RuntimeException(\"buggy\");\n        }\n      }\n\n      class Foo {\n        @Override public String toString() {\n          return new String(JsonCodec.write(new FooWriter(), this), UTF_8);\n        }\n      }\n\n      new Foo().toString(); // cause the exception\n    });\n    assertThat(exception.getMessage()).contains(\"Bug found using FooWriter to write Foo as json. Wrote 1/2 bytes: a\"); // cause the exception\n  }\n\n  @Test void doesntStackOverflowOnToBufferWriterBug_Overflow() {\n    Throwable exception = assertThrows(AssertionError.class, () -> {\n\n      // pretend there was a bug calculating size, ex it calculated incorrectly as to small\n      class FooWriter implements WriteBuffer.Writer {\n        @Override public int sizeInBytes(Object value) {\n          return 2;\n        }\n\n        @Override public void write(Object value, WriteBuffer buffer) {\n          buffer.writeByte('a');\n          buffer.writeByte('b');\n          buffer.writeByte('c'); // wrote larger than size!\n        }\n      }\n\n      class Foo {\n        @Override public String toString() {\n          return new String(JsonCodec.write(new FooWriter(), this), UTF_8);\n        }\n      }\n\n      new Foo().toString(); // cause the exception\n    });\n    assertThat(exception.getMessage()).contains(\"Bug found using FooWriter to write Foo as json. Wrote 2/2 bytes: ab\"); // cause the exception\n  }\n\n  @Test void exceptionReading_malformedJsonWraps() {\n    // grab a real exception from the gson library\n    Exception error = null;\n    byte[] bytes = \"[\\\"='\".getBytes(UTF_8);\n    try {\n      new JsonCodec.JsonReader(ReadBuffer.wrap(bytes)).beginObject();\n      failBecauseExceptionWasNotThrown(IllegalStateException.class);\n    } catch (IOException | IllegalStateException e) {\n      error = e;\n    }\n\n    try {\n      exceptionReading(\"List<Span>\", error);\n      failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n    } catch (IllegalArgumentException e) {\n      assertThat(e).hasMessage(\"Malformed reading List<Span> from json\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/JsonEscaperTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.internal.JsonEscaper.jsonEscape;\nimport static zipkin2.internal.JsonEscaper.jsonEscapedSizeInBytes;\n\nclass JsonEscaperTest {\n\n  @Test void testJsonEscapedSizeInBytes() {\n    assertThat(jsonEscapedSizeInBytes(new String(new char[] {0, 'a', 1})))\n      .isEqualTo(13);\n    assertThat(jsonEscapedSizeInBytes(new String(new char[] {'\"', '\\\\', '\\t', '\\b'})))\n      .isEqualTo(8);\n    assertThat(jsonEscapedSizeInBytes(new String(new char[] {'\\n', '\\r', '\\f'})))\n      .isEqualTo(6);\n    assertThat(jsonEscapedSizeInBytes(\"\\u2028 and \\u2029\"))\n      .isEqualTo(17);\n    assertThat(jsonEscapedSizeInBytes(\"\\\"foo\"))\n      .isEqualTo(5);\n  }\n\n  @Test void testJsonEscape() {\n    assertThat(jsonEscape(new String(new char[] {0, 'a', 1})).toString())\n      .isEqualTo(\"\\\\u0000a\\\\u0001\");\n    assertThat(jsonEscape(new String(new char[] {'\"', '\\\\', '\\t', '\\b'})).toString())\n      .isEqualTo(\"\\\\\\\"\\\\\\\\\\\\t\\\\b\");\n    assertThat(jsonEscape(new String(new char[] {'\\n', '\\r', '\\f'})).toString())\n      .isEqualTo(\"\\\\n\\\\r\\\\f\");\n    assertThat(jsonEscape(\"\\u2028 and \\u2029\").toString())\n      .isEqualTo(\"\\\\u2028 and \\\\u2029\");\n    assertThat(jsonEscape(\"\\\"foo\").toString())\n      .isEqualTo(\"\\\\\\\"foo\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/Proto3FieldsTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.internal.Proto3Fields.BooleanField;\nimport zipkin2.internal.Proto3Fields.BytesField;\nimport zipkin2.internal.Proto3Fields.Fixed64Field;\nimport zipkin2.internal.Proto3Fields.Utf8Field;\nimport zipkin2.internal.Proto3Fields.VarintField;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static zipkin2.internal.Proto3Fields.Field;\nimport static zipkin2.internal.Proto3Fields.Fixed32Field;\nimport static zipkin2.internal.Proto3Fields.HexField;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_FIXED32;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_FIXED64;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_LENGTH_DELIMITED;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_VARINT;\n\nclass Proto3FieldsTest {\n  byte[] bytes = new byte[2048]; // bigger than needed to test sizeInBytes\n  WriteBuffer buf = WriteBuffer.wrap(bytes);\n\n  /** Shows we can reliably look at a byte zero to tell if we are decoding proto3 repeated fields. */\n  @Test void field_key_fieldOneLengthDelimited() {\n    Field field = new Field(1 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    assertThat(field.key)\n      .isEqualTo(0b00001010) // (field_number << 3) | wire_type = 1 << 3 | 2\n      .isEqualTo(10); // for sanity of those looking at debugger, 4th bit + 2nd bit = 10\n    assertThat(field.fieldNumber)\n      .isEqualTo(1);\n    assertThat(field.wireType)\n      .isEqualTo(WIRETYPE_LENGTH_DELIMITED);\n  }\n\n  @Test void varint_sizeInBytes() {\n    VarintField field = new VarintField(1 << 3 | WIRETYPE_VARINT);\n\n    assertThat(field.sizeInBytes(0))\n      .isZero();\n    assertThat(field.sizeInBytes(0xffffffff))\n      .isEqualTo(0\n        + 1 /* tag of varint field */ + 5 // max size of varint32\n      );\n\n    assertThat(field.sizeInBytes(0L))\n      .isZero();\n    assertThat(field.sizeInBytes(0xffffffffffffffffL))\n      .isEqualTo(0\n        + 1 /* tag of varint field */ + 10 // max size of varint64\n      );\n  }\n\n  @Test void boolean_sizeInBytes() {\n    BooleanField field = new BooleanField(1 << 3 | WIRETYPE_VARINT);\n\n    assertThat(field.sizeInBytes(false))\n      .isZero();\n    assertThat(field.sizeInBytes(true))\n      .isEqualTo(0\n        + 1 /* tag of varint field */ + 1 // size of 1\n      );\n  }\n\n  @Test void utf8_sizeInBytes() {\n    Utf8Field field = new Utf8Field(1 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    assertThat(field.sizeInBytes(\"12345678\"))\n      .isEqualTo(0\n        + 1 /* tag of string field */ + 1 /* len */ + 8 // 12345678\n      );\n  }\n\n  @Test void fixed64_sizeInBytes() {\n    Fixed64Field field = new Fixed64Field(1 << 3 | WIRETYPE_FIXED64);\n    assertThat(field.sizeInBytes(Long.MIN_VALUE))\n      .isEqualTo(9);\n  }\n\n  @Test void fixed32_sizeInBytes() {\n    Fixed32Field field = new Fixed32Field(1 << 3 | WIRETYPE_FIXED32);\n    assertThat(field.sizeInBytes(Integer.MIN_VALUE))\n      .isEqualTo(5);\n  }\n\n  @Test void supportedFields() {\n    for (Field field : List.of(\n      new VarintField(128 << 3 | WIRETYPE_VARINT),\n      new BooleanField(128 << 3 | WIRETYPE_VARINT),\n      new HexField(128 << 3 | WIRETYPE_LENGTH_DELIMITED),\n      new Utf8Field(128 << 3 | WIRETYPE_LENGTH_DELIMITED),\n      new BytesField(128 << 3 | WIRETYPE_LENGTH_DELIMITED),\n      new Fixed32Field(128 << 3 | WIRETYPE_FIXED32),\n      new Fixed64Field(128 << 3 | WIRETYPE_FIXED64)\n    )) {\n      assertThat(Field.fieldNumber(field.key, 1))\n        .isEqualTo(field.fieldNumber);\n      assertThat(Field.wireType(field.key, 1))\n        .isEqualTo(field.wireType);\n    }\n  }\n\n  @Test void fieldNumber_malformed() {\n    try {\n      Field.fieldNumber(0, 2);\n      failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n    } catch (IllegalArgumentException e) {\n      assertThat(e)\n        .hasMessage(\"Malformed: fieldNumber was zero at byte 2\");\n    }\n  }\n\n  @Test void wireType_unsupported() {\n    for (int unsupported : List.of(3, 4, 6)) {\n      try {\n        Field.wireType(1 << 3 | unsupported, 2);\n        failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n      } catch (IllegalArgumentException e) {\n        assertThat(e)\n          .hasMessage(\"Malformed: invalid wireType \" + unsupported + \" at byte 2\");\n      }\n    }\n  }\n\n  @Test void field_skipValue_VARINT() {\n    VarintField field = new VarintField(128 << 3 | WIRETYPE_VARINT);\n    field.write(buf, 0xffffffffffffffffL);\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    skipValue(readBuffer, WIRETYPE_VARINT);\n  }\n\n  @Test void field_skipValue_LENGTH_DELIMITED() {\n    Utf8Field field = new Utf8Field(128 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    field.write(buf, \"订单维护服务\");\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    skipValue(readBuffer, WIRETYPE_LENGTH_DELIMITED);\n  }\n\n  @Test void field_skipValue_FIXED64() {\n    Fixed64Field field = new Fixed64Field(128 << 3 | WIRETYPE_FIXED64);\n    field.write(buf, 0xffffffffffffffffL);\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    skipValue(readBuffer, WIRETYPE_FIXED64);\n  }\n\n  @Test void field_skipValue_FIXED32() {\n    Fixed32Field field = new Fixed32Field(128 << 3 | WIRETYPE_FIXED32);\n    buf.writeByte(field.key);\n    buf.writeByte(0xff);\n    buf.writeByte(0xff);\n    buf.writeByte(0xff);\n    buf.writeByte(0xff);\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    skipValue(readBuffer, WIRETYPE_FIXED32);\n  }\n\n  @Test void field_readLengthPrefix_LENGTH_DELIMITED() {\n    BytesField field = new BytesField(128 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    field.write(buf, new byte[10]);\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    assertThat(readBuffer.readVarint32())\n      .isEqualTo(10);\n  }\n\n  @Test void field_readLengthPrefixAndValue_LENGTH_DELIMITED_truncated() {\n    BytesField field = new BytesField(128 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    bytes = new byte[10];\n    WriteBuffer.wrap(bytes).writeVarint(100); // much larger than the buffer size\n\n    try {\n      field.readLengthPrefixAndValue(ReadBuffer.wrap(bytes));\n      failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n    } catch (IllegalArgumentException e) {\n      assertThat(e).hasMessage(\"Truncated: length 100 > bytes available 9\");\n    }\n  }\n\n  @Test void field_read_FIXED64() {\n    Fixed64Field field = new Fixed64Field(128 << 3 | WIRETYPE_FIXED64);\n    field.write(buf, 0xffffffffffffffffL);\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes, 1 /* skip the key */, bytes.length - 1);\n    assertThat(field.readValue(readBuffer))\n      .isEqualTo(0xffffffffffffffffL);\n  }\n\n  void skipValue(ReadBuffer buffer, int wireType) {\n    assertThat(Field.skipValue(buffer, wireType))\n      .isTrue();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/Proto3SpanWriterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.internal.Proto3ZipkinFields.SPAN;\n\npublic class Proto3SpanWriterTest {\n  Proto3SpanWriter writer = new Proto3SpanWriter();\n\n  /** proto messages always need a key, so the non-list form is just a single-field */\n  @Test void write_startsWithSpanKeyAndLengthPrefix() {\n    byte[] bytes = writer.write(CLIENT_SPAN);\n\n    assertThat(bytes)\n      .hasSize(writer.sizeInBytes(CLIENT_SPAN))\n      .startsWith((byte) 10, SPAN.sizeOfValue(CLIENT_SPAN));\n  }\n\n  @Test void writeList_startsWithSpanKeyAndLengthPrefix() {\n    byte[] bytes = writer.writeList(List.of(CLIENT_SPAN));\n\n    assertThat(bytes)\n      .hasSize(writer.sizeInBytes(CLIENT_SPAN))\n      .startsWith((byte) 10, SPAN.sizeOfValue(CLIENT_SPAN));\n  }\n\n  @Test void writeList_multiple() {\n    byte[] bytes = writer.writeList(List.of(CLIENT_SPAN, CLIENT_SPAN));\n\n    assertThat(bytes)\n      .hasSize(writer.sizeInBytes(CLIENT_SPAN) * 2)\n      .startsWith((byte) 10, SPAN.sizeOfValue(CLIENT_SPAN));\n  }\n\n  @Test void writeList_empty() {\n    assertThat(writer.writeList(List.of()))\n      .isEmpty();\n  }\n\n  @Test void writeList_offset_startsWithSpanKeyAndLengthPrefix() {\n    byte[] bytes = new byte[2048];\n    writer.writeList(List.of(CLIENT_SPAN, CLIENT_SPAN), bytes, 0);\n\n    assertThat(bytes)\n      .startsWith((byte) 10, SPAN.sizeOfValue(CLIENT_SPAN));\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/Proto3ZipkinFieldsTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Proto3ZipkinFields.AnnotationField;\nimport zipkin2.internal.Proto3ZipkinFields.EndpointField;\nimport zipkin2.internal.Proto3ZipkinFields.TagField;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.atIndex;\nimport static org.assertj.core.data.MapEntry.entry;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.internal.Proto3Fields.WIRETYPE_LENGTH_DELIMITED;\nimport static zipkin2.internal.Proto3ZipkinFields.SPAN;\n\nclass Proto3ZipkinFieldsTest {\n  byte[] bytes = new byte[2048]; // bigger than needed to test sizeInBytes\n  WriteBuffer buf = WriteBuffer.wrap(bytes);\n\n  /** A map entry is an embedded messages: one for field the key and one for the value */\n  @Test void tag_sizeInBytes() {\n    TagField field = new TagField(1 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    assertThat(field.sizeInBytes(entry(\"123\", \"56789\")))\n      .isEqualTo(0\n        + 1 /* tag of embedded key field */ + 1 /* len */ + 3\n        + 1 /* tag of embedded value field  */ + 1 /* len */ + 5\n        + 1 /* tag of map entry field */ + 1 /* len */\n      );\n  }\n\n  @Test void annotation_sizeInBytes() {\n    AnnotationField field = new AnnotationField(1 << 3 | WIRETYPE_LENGTH_DELIMITED);\n    assertThat(field.sizeInBytes(Annotation.create(1L, \"12345678\")))\n      .isEqualTo(0\n        + 1 /* tag of timestamp field */ + 8 /* 8 byte number */\n        + 1 /* tag of value field */ + 1 /* len */ + 8 // 12345678\n        + 1 /* tag of annotation field */ + 1 /* len */\n      );\n  }\n\n  @Test void endpoint_sizeInBytes() {\n    EndpointField field = new EndpointField(1 << 3 | WIRETYPE_LENGTH_DELIMITED);\n\n    assertThat(field.sizeInBytes(Endpoint.newBuilder()\n      .serviceName(\"12345678\")\n      .ip(\"192.168.99.101\")\n      .ip(\"2001:db8::c001\")\n      .port(80)\n      .build()))\n      .isEqualTo(0\n        + 1 /* tag of servicename field */ + 1 /* len */ + 8 // 12345678\n        + 1 /* tag of ipv4 field */ + 1 /* len */ + 4 // octets in ipv4\n        + 1 /* tag of ipv6 field */ + 1 /* len */ + 16 // octets in ipv6\n        + 1 /* tag of port field */ + 1 /* small varint */\n        + 1 /* tag of endpoint field */ + 1 /* len */\n      );\n  }\n\n  @Test void span_write_startsWithFieldInListOfSpans() {\n    SPAN.write(buf, spanBuilder().build());\n\n    assertThat(bytes).startsWith(\n      0b00001010 /* span key */, 20 /* bytes for length of the span */\n    );\n  }\n\n  @Test void span_write_writesIds() {\n    SPAN.write(buf, spanBuilder().build());\n    assertThat(bytes).startsWith(\n      0b00001010 /* span key */, 20 /* bytes for length of the span */,\n      0b00001010 /* trace ID key */, 8 /* bytes for 64-bit trace ID */,\n      0, 0, 0, 0, 0, 0, 0, 1, // hex trace ID\n      0b00011010 /* span ID key */, 8 /* bytes for 64-bit span ID */,\n      0, 0, 0, 0, 0, 0, 0, 2 // hex span ID\n    );\n    assertThat(buf.pos())\n      .isEqualTo(3 * 2 /* overhead of three fields */ + 2 * 8 /* 64-bit fields */)\n      .isEqualTo(22); // easier math on the next test\n  }\n\n  @Test void span_read_ids() {\n    assertRoundTrip(spanBuilder().parentId(\"1\").build());\n  }\n\n  @Test void span_read_name() {\n    assertRoundTrip(spanBuilder().name(\"romeo\").build());\n  }\n\n  @Test void span_read_kind() {\n    assertRoundTrip(spanBuilder().kind(Span.Kind.CONSUMER).build());\n  }\n\n  @Test void span_read_timestamp_duration() {\n    assertRoundTrip(spanBuilder().timestamp(TODAY).duration(134).build());\n  }\n\n  @Test void span_read_endpoints() {\n    assertRoundTrip(spanBuilder().localEndpoint(FRONTEND).remoteEndpoint(BACKEND).build());\n  }\n\n  @Test void span_read_annotation() {\n    assertRoundTrip(spanBuilder().addAnnotation(TODAY, \"parked on sidewalk\").build());\n  }\n\n  @Test void span_read_tag() {\n    assertRoundTrip(spanBuilder().putTag(\"foo\", \"bar\").build());\n  }\n\n  @Test void span_read_tag_empty() {\n    assertRoundTrip(spanBuilder().putTag(\"empty\", \"\").build());\n  }\n\n  @Test void span_read_shared() {\n    assertRoundTrip(spanBuilder().shared(true).build());\n  }\n\n  @Test void span_read_debug() {\n    assertRoundTrip(spanBuilder().debug(true).build());\n  }\n\n  @Test void span_read() {\n    assertRoundTrip(CLIENT_SPAN);\n  }\n\n  @Test void span_write_omitsEmptyEndpoints() {\n    SPAN.write(buf, spanBuilder()\n      .localEndpoint(Endpoint.newBuilder().build())\n      .remoteEndpoint(Endpoint.newBuilder().build())\n      .build());\n\n    assertThat(buf.pos())\n      .isEqualTo(22);\n  }\n\n  @Test void span_write_kind() {\n    SPAN.write(buf, spanBuilder().kind(Span.Kind.PRODUCER).build());\n    assertThat(bytes)\n      .contains(0b0100000, atIndex(22)) // (field_number << 3) | wire_type = 4 << 3 | 0\n      .contains(0b0000011, atIndex(23)); // producer's index is 3\n  }\n\n  @Test void span_read_kind_tolerant() {\n    assertRoundTrip(spanBuilder().kind(Span.Kind.CONSUMER).build());\n\n    bytes[23] = (byte) (Span.Kind.values().length + 1); // undefined kind\n    assertThat(SPAN.read(ReadBuffer.wrap(bytes)))\n      .isEqualTo(spanBuilder().build()); // skips undefined kind instead of dying\n\n    bytes[23] = 0; // serialized zero\n    assertThat(SPAN.read(ReadBuffer.wrap(bytes)))\n      .isEqualTo(spanBuilder().build());\n  }\n\n  @Test void span_write_debug() {\n    SPAN.write(buf, CLIENT_SPAN.toBuilder().debug(true).build());\n\n    assertThat(bytes)\n      .contains(0b01100000, atIndex(buf.pos() - 2)) // (field_number << 3) | wire_type = 12 << 3 | 0\n      .contains(1, atIndex(buf.pos() - 1)); // true\n  }\n\n  @Test void span_write_shared() {\n    SPAN.write(buf, CLIENT_SPAN.toBuilder().kind(Span.Kind.SERVER).shared(true).build());\n\n    assertThat(bytes)\n      .contains(0b01101000, atIndex(buf.pos() - 2)) // (field_number << 3) | wire_type = 13 << 3 | 0\n      .contains(1, atIndex(buf.pos() - 1)); // true\n  }\n\n  static Span.Builder spanBuilder() {\n    return Span.newBuilder().traceId(\"1\").id(\"2\");\n  }\n\n  void assertRoundTrip(Span span) {\n    SPAN.write(buf, span);\n\n    assertThat(SPAN.read(ReadBuffer.wrap(bytes)))\n      .isEqualTo(span);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/ReadBufferTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport org.junit.jupiter.api.Test;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\n\nclass ReadBufferTest {\n  @Test void byteBuffer_limited() {\n    ByteBuffer buf = ByteBuffer.wrap(\"glove\".getBytes(UTF_8));\n    buf.get();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buf.slice());\n    assertThat(readBuffer.readUtf8(readBuffer.available()))\n      .isEqualTo(\"love\");\n  }\n\n  @Test void byteBuffer_arrayOffset() {\n    ByteBuffer buf = ByteBuffer.wrap(\"glove\".getBytes(UTF_8), 1, 4);\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buf.slice());\n    assertThat(readBuffer.pos()).isEqualTo(0);\n    assertThat(readBuffer.available()).isEqualTo(4);\n    assertThat(readBuffer.readUtf8(readBuffer.available()))\n      .isEqualTo(\"love\");\n  }\n\n  @Test void readVarint32() {\n    assertReadVarint32(0);\n    assertReadVarint32(0b0011_1111_1111_1111);\n    assertReadVarint32(0xFFFFFFFF);\n  }\n\n  static void assertReadVarint32(int value) {\n    byte[] bytes = new byte[WriteBuffer.varintSizeInBytes(value)];\n    WriteBuffer.wrap(bytes).writeVarint(value);\n\n    assertThat(ReadBuffer.wrap(bytes).readVarint32())\n      .isEqualTo(value);\n  }\n\n  @Test void readShort_bytes() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02};\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes);\n\n    assertThat(readBuffer.readShort()).isEqualTo((short) 0x0102);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readShort_byteBuff() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02};\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readShort()).isEqualTo((short) 0x0102);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readShort_byteBuff_littleEndian() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02};\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readShort()).isEqualTo((short) 0x0102);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readInt_bytes() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04};\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes);\n\n    assertThat(readBuffer.readInt()).isEqualTo(0x01020304);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readInt_byteBuff() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04};\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readInt()).isEqualTo(0x01020304);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readInt_byteBuff_littleEndian() {\n    byte[] bytes = {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04};\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readInt()).isEqualTo(0x01020304);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLong_bytes() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes);\n\n    assertThat(readBuffer.readLong())\n      .isEqualTo(0x0102030405060708L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLong_byteBuff() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readLong())\n      .isEqualTo(0x0102030405060708L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLong_byteBuff_littleEndian() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readLong())\n      .isEqualTo(0x0102030405060708L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLongLe_bytes() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ReadBuffer readBuffer = ReadBuffer.wrap(bytes);\n\n    assertThat(readBuffer.readLongLe())\n      .isEqualTo(0x0807060504030201L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLongLe_byteBuff() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readLongLe())\n      .isEqualTo(0x0807060504030201L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readLongLe_byteBuff_littleEndian() {\n    byte[] bytes = {\n      (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,\n      (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08,\n    };\n\n    ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asReadOnlyBuffer();\n    ReadBuffer readBuffer = ReadBuffer.wrapUnsafe(buffer);\n    assertThat(readBuffer).isInstanceOf(ReadBuffer.Buff.class);\n\n    assertThat(readBuffer.readLongLe())\n      .isEqualTo(0x0807060504030201L);\n    assertThat(readBuffer.available()).isZero();\n  }\n\n  @Test void readVarint32_malformedTooBig() {\n    byte[] bytes = new byte[8];\n    WriteBuffer.wrap(bytes).writeLongLe(0xffffffffffffL);\n\n    try {\n      ReadBuffer.wrap(bytes).readVarint32();\n      failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n    } catch (IllegalArgumentException e) {\n      assertThat(e)\n        .hasMessage(\"Greater than 32-bit varint at position 4\");\n    }\n  }\n\n  @Test void readVarint64() {\n    assertReadVarint64(0L);\n    assertReadVarint64(0b0011_1111_1111_1111L);\n    assertReadVarint64(0xffffffffffffffffL);\n  }\n\n  static void assertReadVarint64(long value) {\n    byte[] bytes = new byte[WriteBuffer.varintSizeInBytes(value)];\n    WriteBuffer.wrap(bytes).writeVarint(value);\n\n    assertThat(ReadBuffer.wrap(bytes).readVarint64())\n      .isEqualTo(value);\n  }\n\n  @Test void readVarint64_malformedTooBig() {\n    byte[] bytes = new byte[16];\n    WriteBuffer buffer = WriteBuffer.wrap(bytes);\n    buffer.writeLongLe(0xffffffffffffffffL);\n    buffer.writeLongLe(0xffffffffffffffffL);\n\n    try {\n      ReadBuffer.wrap(bytes).readVarint64();\n      failBecauseExceptionWasNotThrown(IllegalArgumentException.class);\n    } catch (IllegalArgumentException e) {\n      assertThat(e)\n        .hasMessage(\"Greater than 64-bit varint at position 9\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/SpanNodeTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass SpanNodeTest {\n  List<String> messages = new ArrayList<>();\n\n  Logger logger = new Logger(\"\", null) {\n    {\n      setLevel(Level.ALL);\n    }\n\n    @Override public void log(Level level, String msg) {\n      assertThat(level).isEqualTo(Level.FINE);\n      messages.add(msg);\n    }\n  };\n\n  @Test void addChild_nullNotAllowed() {\n    assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> {\n      Span.Builder builder = Span.newBuilder().traceId(\"a\");\n      SpanNode a = new SpanNode(builder.id(\"a\").build());\n      a.addChild(null);\n    });\n  }\n\n  @Test void addChild_selfNotAllowed() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      Span.Builder builder = Span.newBuilder().traceId(\"a\");\n      SpanNode a = new SpanNode(builder.id(\"a\").build());\n      a.addChild(a);\n    });\n  }\n\n  /** Ensures internal deduping occurs */\n  @Test void build_redundantIgnored() {\n    Span.Builder builder = Span.newBuilder().traceId(\"a\");\n    List<Span> trace = List.of(builder.id(\"a\").build(), builder.id(\"b\").build(), builder.build());\n\n    SpanNode tree = new SpanNode.Builder(logger).build(trace);\n    assertThat(tree.span).isEqualTo(trace.get(0));\n    assertThat(tree.children()).extracting(SpanNode::span).containsExactly(trace.get(1));\n  }\n\n  /**\n   * The following tree should traverse in alphabetical order\n   *\n   * <p><pre>{@code\n   *          a\n   *        / | \\\n   *       b  c  d\n   *      /|\\\n   *     e f g\n   *          \\\n   *           h\n   * }</pre>\n   */\n  @Test void traversesBreadthFirst() {\n    Span.Builder builder = Span.newBuilder().traceId(\"a\");\n    SpanNode a = new SpanNode(builder.id(\"a\").build());\n    SpanNode b = new SpanNode(builder.id(\"b\").build());\n    SpanNode c = new SpanNode(builder.id(\"c\").build());\n    SpanNode d = new SpanNode(builder.id(\"d\").build());\n    // root(a) has children b, c, d\n    a.addChild(b).addChild(c).addChild(d);\n    SpanNode e = new SpanNode(builder.id(\"e\").build());\n    SpanNode f = new SpanNode(builder.id(\"f\").build());\n    SpanNode g = new SpanNode(builder.id(\"1\").build());\n    // child(b) has children e, f, g\n    b.addChild(e).addChild(f).addChild(g);\n    SpanNode h = new SpanNode(builder.id(\"2\").build());\n    // f has no children\n    // child(g) has child h\n    g.addChild(h);\n\n    assertThat(a.traverse()).toIterable()\n      .extracting(SpanNode::span)\n      .extracting(s -> s.id().replaceAll(\"0\", \"\"))\n      .containsExactly(\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"1\", \"2\");\n  }\n\n  /**\n   * Makes sure that the trace tree is constructed based on parent-child, not by parameter order.\n   */\n  @Test void constructsTraceTree() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"c\").id(\"d\").build()\n    );\n    assertAncestry(trace);\n  }\n\n  /** Same as {@link #constructsTraceTree()}, except with shared span ID */\n  @Test void constructsTraceTree_sharedId() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").shared(true).build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\").build()\n    );\n    assertAncestry(trace);\n  }\n\n  @Test void constructsTraceTree_sharedRootId() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"a\").shared(true).build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\").build()\n    );\n    assertAncestry(trace);\n  }\n\n  void assertAncestry(List<Span> trace) {\n    SpanNode root = buildTree(trace);\n    assertThat(root.span()).isEqualTo(trace.get(0));\n\n    SpanNode current = root;\n    for (int i = 1, length = trace.size() - 1; i < length; i++) {\n      current = current.children.get(0);\n      assertThat(current.span).isEqualTo(trace.get(i));\n      assertThat(current.children).extracting(SpanNode::span)\n        .containsExactly(trace.get(i + 1));\n    }\n  }\n\n  @Test void constructsTraceTree_qualifiesChildrenOfDuplicateServerSpans() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").build(),\n      localServiceName(\"foo\", Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").shared(true)),\n      localServiceName(\"bar\", Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").shared(true)),\n      localServiceName(\"bar\", Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\")),\n      localServiceName(\"foo\", Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"d\"))\n    );\n\n    assertServerAncestry(trace);\n  }\n\n  @Test void constructsTraceTree_qualifiesChildrenOfDuplicateServerSpans_mixedShared() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").build(),\n      localServiceName(\"foo\", Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\")),\n      localServiceName(\"bar\", Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").shared(true)),\n      localServiceName(\"bar\", Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"d\")),\n      localServiceName(\"foo\", Span.newBuilder().traceId(\"a\").parentId(\"c\").id(\"e\"))\n    );\n\n    assertServerAncestry(trace);\n  }\n\n  void assertServerAncestry(List<Span> trace) {\n    SpanNode a = buildTree(trace);\n    assertThat(a.span()).isEqualTo(trace.get(0));\n\n    SpanNode b_client = a.children().get(0);\n    assertThat(b_client.span()).isEqualTo(trace.get(1));\n    assertThat(b_client.children()).extracting(SpanNode::span)\n      .containsExactly(trace.get(3), trace.get(2));\n\n    SpanNode b_server_bar = b_client.children().get(0);\n    assertThat(b_server_bar.children()).extracting(SpanNode::span)\n      .containsExactly(trace.get(4));\n\n    SpanNode b_server_foo = b_client.children().get(1);\n    assertThat(b_server_foo.children()).extracting(SpanNode::span)\n      .containsExactly(trace.get(5));\n  }\n\n  static Span localServiceName(String serviceName, Span.Builder builder) {\n    return builder.localEndpoint(Endpoint.newBuilder().serviceName(serviceName).build()).build();\n  }\n\n  SpanNode buildTree(List<Span> trace) {\n    // TRACE is sorted with root span first, lets reverse them to make\n    // sure the trace is stitched together by id.\n    List<Span> copy = new ArrayList<>(trace);\n    Collections.reverse(copy);\n\n    return new SpanNode.Builder(logger).build(copy);\n  }\n\n  @Test void constructsTraceTree_dedupes() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"a\").build()\n    );\n\n    SpanNode root = new SpanNode.Builder(logger).build(trace);\n\n    assertThat(root.span())\n      .isEqualTo(trace.get(0));\n    assertThat(root.children())\n      .isEmpty();\n  }\n\n  @Test void constructsTraceTree_duplicateRoots() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"a\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"b\").build()\n    );\n\n    SpanNode root = new SpanNode.Builder(logger).build(trace);\n\n    assertThat(root.span())\n      .isEqualTo(trace.get(0));\n    assertThat(root.children())\n      .extracting(SpanNode::span)\n      .containsExactly(trace.get(1));\n  }\n\n  @Test void build_noChildLeftBehind() {\n    List<Span> spans = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"b\").name(\"root-0\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"c\").name(\"child-0\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"d\").name(\"child-1\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"e\").name(\"lost-0\").build(),\n      Span.newBuilder().traceId(\"a\").id(\"f\").name(\"lost-1\").build());\n    int treeSize = 0;\n    SpanNode tree = new SpanNode.Builder(logger).build(spans);\n    Iterator<SpanNode> iter = tree.traverse();\n    while (iter.hasNext()) {\n      iter.next();\n      treeSize++;\n    }\n    assertThat(treeSize).isEqualTo(spans.size());\n    assertThat(messages).containsExactly(\n      \"building trace tree: traceId=000000000000000a\",\n      \"attributing span missing parent to root: traceId=000000000000000a, rootSpanId=000000000000000b, spanId=000000000000000e\",\n      \"attributing span missing parent to root: traceId=000000000000000a, rootSpanId=000000000000000b, spanId=000000000000000f\"\n    );\n  }\n\n  @Test void build_headless() {\n    Span s2 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").name(\"s2\").build();\n    Span s3 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"c\").name(\"s3\").build();\n    Span s4 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"d\").name(\"s4\").build();\n\n    SpanNode root = new SpanNode.Builder(logger).build(List.of(s2, s3, s4));\n\n    assertThat(root.span()).isNull();\n    assertThat(root.children()).extracting(SpanNode::span)\n      .containsExactly(s2, s3, s4);\n    assertThat(messages).containsExactly(\n      \"building trace tree: traceId=000000000000000a\",\n      \"substituting dummy node for missing root span: traceId=000000000000000a\"\n    );\n  }\n\n  @Test void build_outOfOrder() {\n    Span s2 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").name(\"s2\").build();\n    Span s3 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"c\").name(\"s3\").build();\n    Span s4 = Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"d\").name(\"s4\").build();\n\n    SpanNode root = new SpanNode.Builder(logger).build(List.of(s2, s3, s4));\n\n    assertThat(root.span()).isNull();\n    assertThat(root.children()).extracting(SpanNode::span)\n      .containsExactly(s2, s3, s4);\n    assertThat(messages).containsExactly(\n      \"building trace tree: traceId=000000000000000a\",\n      \"substituting dummy node for missing root span: traceId=000000000000000a\"\n    );\n  }\n\n  @Test @Disabled void addNode_skipsOnCycle() {\n    Span.newBuilder().traceId(\"a\").parentId(\"d\").id(\"b\").name(\"s2\").build();\n    Span.newBuilder().traceId(\"a\").parentId(\"b\").id(\"d\").name(\"s3\").build();\n\n    // TODO: see how spans like ^^ affect the node tree\n  }\n\n  // uses the same data as javascript\n  @Test void build_skewedTrace() {\n    List<Span> httpTrace = List.of(\n      Span.newBuilder()\n        .traceId(\"1e223ff1f80f1c69\").parentId(\"74280ae0c10d8062\").id(\"43210ae0c10d1234\")\n        .name(\"async\")\n        .timestamp(1470150004008762L)\n        .duration(65000L)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"serviceb\").ip(\"192.0.0.0\").build())\n        .build(),\n      Span.newBuilder()\n        .traceId(\"1e223ff1f80f1c69\").parentId(\"bf396325699c84bf\").id(\"43210ae0c10d1234\")\n        .kind(Span.Kind.SERVER)\n        .name(\"post\")\n        .timestamp(1541138169255688L)\n        .duration(168731L)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"serviceb\").ip(\"192.0.0.0\").build())\n        .shared(true)\n        .build(),\n      Span.newBuilder()\n        .traceId(\"1e223ff1f80f1c69\").id(\"bb1f0e21882325b8\")\n        .kind(Span.Kind.SERVER)\n        .name(\"get\")\n        .timestamp(1470150004071068L)\n        .duration(99411L)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"servicea\").ip(\"127.0.0.0\").build())\n        .build(),\n      Span.newBuilder()\n        .traceId(\"1e223ff1f80f1c69\").parentId(\"bb1f0e21882325b8\").id(\"74280ae0c10d8062\")\n        .kind(Span.Kind.CLIENT)\n        .name(\"post\")\n        .timestamp(1470150004074202L)\n        .duration(94539L)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"servicea\").ip(\"127.0.0.0\").build())\n        .build());\n\n    SpanNode root = new SpanNode.Builder(logger).build(httpTrace);\n    assertThat(root.traverse()).toIterable().extracting(SpanNode::span)\n      .containsExactlyInAnyOrderElementsOf(httpTrace);\n  }\n\n  @Test void ordersChildrenByTimestamp() {\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(\"a\").id(\"1\").build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"1\").id(\"a\").name(\"a\").timestamp(2L).build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"1\").id(\"b\").name(\"b\").timestamp(1L).build(),\n      Span.newBuilder().traceId(\"a\").parentId(\"1\").id(\"c\").name(\"c\").build()\n    );\n\n    SpanNode root = new SpanNode.Builder(logger).build(trace);\n\n    assertThat(root.children()).extracting(n -> n.span().name())\n      .containsExactly(\"c\", \"b\", \"a\"); // null first\n  }\n\n  @Test void build_changingIps() {\n    // This trace was taken from the middle of a real broken one, IDs and timestamps changed\n    List<Span> httpTrace = List.of(\n      Span.newBuilder()\n        .traceId(\"1\").parentId(\"a\").id(\"c\")\n        .kind(Span.Kind.SERVER)\n        .timestamp(1)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"my-service\").ip(\"10.2.3.4\").build())\n        .shared(true)\n        .build(),\n      Span.newBuilder()\n        .traceId(\"1\").parentId(\"c\").id(\"b\")\n        .kind(Span.Kind.CLIENT)\n        .timestamp(2)\n        // note the IP is different\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"my-service\").ip(\"169.2.3.4\").build())\n        .build(),\n      Span.newBuilder()\n        .traceId(\"1\").parentId(\"c\").id(\"a\")\n        .timestamp(3)\n        .localEndpoint(Endpoint.newBuilder().serviceName(\"my-service\").ip(\"10.2.3.4\").build())\n        .build());\n\n    SpanNode root = new SpanNode.Builder(logger).build(httpTrace);\n    assertThat(root.traverse()).toIterable().extracting(SpanNode::span)\n      .containsExactlyElementsOf(httpTrace);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/TraceTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.tuple;\n\nclass TraceTest {\n\n  /**\n   * Some don't propagate the server's parent ID which creates a race condition. Try to unwind it.\n   *\n   * <p>See https://github.com/openzipkin/zipkin/pull/1745\n   */\n  @Test void backfillsMissingParentIdOnSharedSpan() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false),\n      // below the parent ID is null as it wasn't propagated\n      span(\"a\", null, \"b\", Kind.SERVER, \"backend\", null, true)\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", null, true)\n    );\n  }\n\n  @Test void backfillsMissingSharedFlag() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", \"1.2.3.4\", false),\n      // below the shared flag was forgotten\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"5.6.7.8\", false)\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", \"1.2.3.4\", false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"5.6.7.8\", true)\n    );\n  }\n\n  /** Some truncate an incoming trace ID to 64-bits. */\n  @Test void choosesBestTraceId() {\n    List<Span> trace = List.of(\n      span(\"7180c278b62e8f6a216a2aea45d08fc9\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"7180c278b62e8f6a216a2aea45d08fc9\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false),\n      span(\"216a2aea45d08fc9\", \"a\", \"b\", Kind.SERVER, \"backend\", null, true)\n    );\n\n    assertThat(Trace.merge(trace)).flatExtracting(Span::traceId).containsExactly(\n      \"7180c278b62e8f6a216a2aea45d08fc9\",\n      \"7180c278b62e8f6a216a2aea45d08fc9\",\n      \"7180c278b62e8f6a216a2aea45d08fc9\"\n    );\n  }\n\n  /** Let's pretend people use crappy data, but only on the first hop. */\n  @Test void mergesWhenMissingEndpoints() {\n    List<Span> trace = List.of(\n      Span.newBuilder()\n        .traceId(\"a\")\n        .id(\"a\")\n        .putTag(\"service\", \"frontend\")\n        .putTag(\"span.kind\", \"SERVER\")\n        .build(),\n      Span.newBuilder()\n        .traceId(\"a\")\n        .parentId(\"a\")\n        .id(\"b\")\n        .putTag(\"service\", \"frontend\")\n        .putTag(\"span.kind\", \"CLIENT\")\n        .timestamp(1L)\n        .build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", null, true),\n      Span.newBuilder().traceId(\"a\").parentId(\"a\").id(\"b\").duration(10L).build()\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      Span.newBuilder()\n        .traceId(\"a\")\n        .id(\"a\")\n        .putTag(\"service\", \"frontend\")\n        .putTag(\"span.kind\", \"SERVER\")\n        .build(),\n      Span.newBuilder()\n        .traceId(\"a\")\n        .parentId(\"a\")\n        .id(\"b\")\n        .putTag(\"service\", \"frontend\")\n        .putTag(\"span.kind\", \"CLIENT\")\n        .timestamp(1L)\n        .duration(10L)\n        .build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", null, true)\n    );\n  }\n\n  /**\n   * If a client request is proxied by something that does transparent retried. It can be the case\n   * that two servers share the same ID (accidentally!)\n   */\n  @Test void doesntMergeSharedSpansOnDifferentIPs() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false).toBuilder()\n        .timestamp(1L).addAnnotation(3L, \"brave.flush\").build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.4\", true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", true),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false).toBuilder()\n        .duration(10L).build()\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false).toBuilder()\n        .timestamp(1L).duration(10L).addAnnotation(3L, \"brave.flush\").build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.4\", true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", true)\n    );\n  }\n\n  // Same as above, but the late reported data has no parent id or endpoint\n  @Test void putsRandomDataOnFirstSpanWithEndpoint() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, null, null, false),\n      span(\"a\", \"a\", \"b\", null, \"frontend\", null, false).toBuilder()\n        .timestamp(1L).addAnnotation(3L, \"brave.flush\").build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.4\", true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", true),\n      span(\"a\", \"a\", \"b\", null, null, null, false).toBuilder()\n        .duration(10L).build()\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false).toBuilder()\n        .timestamp(1L).duration(10L).addAnnotation(3L, \"brave.flush\").build(),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.4\", true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", true)\n    );\n  }\n\n  // not a good idea to send parts of a local endpoint separately, but this helps ensure data isn't\n  // accidentally partitioned in a overly fine grain\n  @Test void mergesIncompleteEndpoints() {\n    List<Span> trace = List.of(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, null, \"1.2.3.4\", false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, null, \"1.2.3.5\", true),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", null, true)\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", \"1.2.3.4\", false),\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", true)\n    );\n  }\n\n  @Test void deletesSelfReferencingParentId() {\n    List<Span> trace = List.of(\n      span(\"a\", \"a\", \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false)\n    );\n\n    assertThat(Trace.merge(trace)).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(\n      span(\"a\", null, \"a\", Kind.SERVER, \"frontend\", null, false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false)\n    );\n  }\n\n  @Test void worksWhenMissingParentSpan() {\n    String missingParentId = \"a\";\n    List<Span> trace = List.of(\n      span(\"a\", missingParentId, \"b\", Kind.SERVER, \"backend\", \"1.2.3.4\", false),\n      span(\"a\", missingParentId, \"c\", Kind.SERVER, \"backend\", null, false)\n    );\n\n    assertThat(Trace.merge(trace)).containsExactlyElementsOf(trace);\n  }\n\n  // some instrumentation don't add shared flag to servers\n  @Test void cleanupComparator_ordersClientFirst() {\n    List<Span> trace = Arrays.asList( // to allow sorting\n      span(\"a\", \"a\", \"b\", Kind.SERVER, \"backend\", \"1.2.3.5\", false),\n      span(\"a\", \"a\", \"b\", Kind.CLIENT, \"frontend\", null, false)\n    );\n\n    Collections.sort(trace, Trace.CLEANUP_COMPARATOR);\n    assertThat(trace.get(0).kind()).isEqualTo(Kind.CLIENT);\n  }\n\n  /** Comparators are meant to be transitive. This exploits edge cases to fool our comparator. */\n  @Test void cleanupComparator_transitiveKindComparison() {\n    List<Span> trace = new ArrayList<>();\n    Endpoint aEndpoint = Endpoint.newBuilder().serviceName(\"a\").build();\n    Endpoint bEndpoint = Endpoint.newBuilder().serviceName(\"b\").build();\n    Span template = Span.newBuilder().traceId(\"a\").id(\"a\").build();\n    // If there is a transitive ordering problem, TimSort will throw an IllegalArgumentException\n    // when there are at least 32 elements.\n    for (int i = 0, length = 7; i < length; i++) {\n      trace.add(template.toBuilder().shared(true).localEndpoint(bEndpoint).build());\n      trace.add(template.toBuilder().kind(Kind.CLIENT).localEndpoint(bEndpoint).build());\n      trace.add(template.toBuilder().localEndpoint(aEndpoint).build());\n      trace.add(template);\n      trace.add(template.toBuilder().kind(Kind.CLIENT).localEndpoint(aEndpoint).build());\n    }\n\n    Collections.sort(trace, Trace.CLEANUP_COMPARATOR);\n\n    assertThat(new LinkedHashSet<>(trace))\n      .extracting(Span::shared, Span::kind, s -> s.localServiceName())\n      .containsExactly(\n        tuple(null, Kind.CLIENT, \"a\"),\n        tuple(null, Kind.CLIENT, \"b\"),\n        tuple(null, null, null),\n        tuple(null, null, \"a\"),\n        tuple(true, null, \"b\")\n      );\n  }\n\n  static Span span(String traceId, @Nullable String parentId, String id, @Nullable Kind kind,\n    @Nullable String local, @Nullable String ip, boolean shared) {\n    Span.Builder result = Span.newBuilder().traceId(traceId).parentId(parentId).id(id).kind(kind);\n    if (local != null || ip != null) {\n      result.localEndpoint(Endpoint.newBuilder().serviceName(local).ip(ip).build());\n    }\n    return result.shared(shared).build();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/TracesAdapterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.TestObjects;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass TracesAdapterTest {\n  InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n  TracesAdapter adapter = new TracesAdapter(storage);\n\n  /**\n   * The contract for {@link zipkin2.storage.SpanStore#getTrace(java.lang.String)} is to return\n   * empty on not found. This ensures a list of results aren't padded with empty ones.\n   */\n  @Test void getTraces_doesntReturnEmptyElements() throws Exception {\n    storage.accept(TestObjects.TRACE).execute();\n\n    assertThat(adapter.getTraces(List.of()).execute())\n      .isEmpty();\n\n    assertThat(adapter.getTraces(List.of(\"1\")).execute())\n      .isEmpty();\n\n    assertThat(adapter.getTraces(List.of(\"1\", \"2\")).execute())\n      .isEmpty();\n\n    assertThat(adapter.getTraces(List.of(\"1\", TestObjects.TRACE.get(0).traceId(), \"3\")).execute())\n      .containsExactly(TestObjects.TRACE);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/V1JsonSpanWriterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\n\nclass V1JsonSpanWriterTest {\n  V1JsonSpanWriter writer = new V1JsonSpanWriter();\n  byte[] bytes = new byte[2048]; // bigger than needed to test sizeInBytes\n  WriteBuffer buf = WriteBuffer.wrap(bytes);\n\n  @Test void sizeInBytes() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(writer.sizeInBytes(CLIENT_SPAN)).isEqualTo(buf.pos());\n  }\n\n  @Test void writesCoreAnnotations_client() {\n    writer.write(CLIENT_SPAN, buf);\n\n    writesCoreAnnotations(\"cs\", \"cr\");\n  }\n\n  @Test void writesCoreAnnotations_server() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.SERVER).build(), buf);\n\n    writesCoreAnnotations(\"sr\", \"ss\");\n  }\n\n  @Test void writesCoreAnnotations_producer() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.PRODUCER).build(), buf);\n\n    writesCoreAnnotations(\"ms\", \"ws\");\n  }\n\n  @Test void writesCoreAnnotations_consumer() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.CONSUMER).build(), buf);\n\n    writesCoreAnnotations(\"wr\", \"mr\");\n  }\n\n  void writesCoreAnnotations(String begin, String end) {\n    String json = new String(bytes, UTF_8);\n\n    assertThat(json)\n        .contains(\"{\\\"timestamp\\\":\" + CLIENT_SPAN.timestamp() + \",\\\"value\\\":\\\"\" + begin + \"\\\"\");\n    assertThat(json)\n        .contains(\"{\\\"timestamp\\\":\"\n          + (CLIENT_SPAN.timestampAsLong() + CLIENT_SPAN.durationAsLong())\n          + \",\\\"value\\\":\\\"\" + end + \"\\\"\");\n  }\n\n  @Test void writesCoreSendAnnotations_client() {\n    writer.write(CLIENT_SPAN.toBuilder().duration(null).build(), buf);\n\n    writesCoreSendAnnotations(\"cs\");\n  }\n\n  @Test void writesCoreSendAnnotations_server() {\n    writer.write(CLIENT_SPAN.toBuilder().duration(null).kind(Span.Kind.SERVER).build(), buf);\n\n    writesCoreSendAnnotations(\"sr\");\n  }\n\n  @Test void writesCoreSendAnnotations_producer() {\n    writer.write(CLIENT_SPAN.toBuilder().duration(null).kind(Span.Kind.PRODUCER).build(), buf);\n\n    writesCoreSendAnnotations(\"ms\");\n  }\n\n  @Test void writesCoreSendAnnotations_consumer() {\n    writer.write(CLIENT_SPAN.toBuilder().duration(null).kind(Span.Kind.CONSUMER).build(), buf);\n\n    writesCoreSendAnnotations(\"mr\");\n  }\n\n  void writesCoreSendAnnotations(String begin) {\n    String json = new String(bytes, UTF_8);\n\n    assertThat(json)\n        .contains(\"{\\\"timestamp\\\":\" + CLIENT_SPAN.timestamp() + \",\\\"value\\\":\\\"\" + begin + \"\\\"\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_client() {\n    writer.write(CLIENT_SPAN.toBuilder().build(), buf);\n\n    writesAddressBinaryAnnotation(\"sa\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_server() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.SERVER).build(), buf);\n\n    writesAddressBinaryAnnotation(\"ca\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_producer() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.PRODUCER).build(), buf);\n\n    writesAddressBinaryAnnotation(\"ma\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_consumer() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.CONSUMER).build(), buf);\n\n    writesAddressBinaryAnnotation(\"ma\");\n  }\n\n  void writesAddressBinaryAnnotation(String address) {\n    assertThat(new String(bytes, UTF_8))\n      .contains(\"{\\\"key\\\":\\\"\" + address + \"\\\",\\\"value\\\":true,\\\"endpoint\\\":\");\n  }\n\n  @Test void writes128BitTraceId() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n        .startsWith(\"{\\\"traceId\\\":\\\"\" + CLIENT_SPAN.traceId() + \"\\\"\");\n  }\n\n  @Test void annotationsHaveEndpoints() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n        .contains(\n            \"\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"frontend\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}\");\n  }\n\n  @Test void writesTimestampAndDuration() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n        .contains(\n            \"\\\"timestamp\\\":\" + CLIENT_SPAN.timestamp() + \",\\\"duration\\\":\" + CLIENT_SPAN.duration());\n  }\n\n  @Test void skipsTimestampAndDuration_shared() {\n    writer.write(CLIENT_SPAN.toBuilder().kind(Span.Kind.SERVER).shared(true).build(), buf);\n\n    assertThat(new String(bytes, UTF_8))\n        .doesNotContain(\n            \"\\\"timestamp\\\":\" + CLIENT_SPAN.timestamp() + \",\\\"duration\\\":\" + CLIENT_SPAN.duration());\n  }\n\n  @Test void writesEmptySpanName() {\n    Span span =\n        Span.newBuilder()\n            .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n            .parentId(\"6b221d5bc9e6496c\")\n            .id(\"5b4185666d50f68b\")\n            .build();\n\n    writer.write(span, buf);\n\n    assertThat(new String(bytes, UTF_8)).contains(\"\\\"name\\\":\\\"\\\"\");\n  }\n\n  @Test void writesEmptyServiceName() {\n    Span span =\n        CLIENT_SPAN\n            .toBuilder()\n            .localEndpoint(Endpoint.newBuilder().ip(\"127.0.0.1\").build())\n            .build();\n\n    writer.write(span, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .contains(\"\\\"value\\\":\\\"foo\\\",\\\"endpoint\\\":{\\\"serviceName\\\":\\\"\\\",\\\"ipv4\\\":\\\"127.0.0.1\\\"}\");\n  }\n\n  @Test void tagsAreBinaryAnnotations() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n        .contains(\n            \"\"\"\n            \"binaryAnnotations\":[\\\n            {\"key\":\"clnt/finagle.version\",\"value\":\"6.45.0\",\"endpoint\":{\"serviceName\":\"frontend\",\"ipv4\":\"127.0.0.1\"}},\\\n            {\"key\":\"http.path\",\"value\":\"/api\",\"endpoint\":{\"serviceName\":\"frontend\",\"ipv4\":\"127.0.0.1\"}}\\\n            \"\"\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/V1ThriftSpanWriterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.Span.Kind.CLIENT;\nimport static zipkin2.Span.Kind.CONSUMER;\nimport static zipkin2.Span.Kind.PRODUCER;\nimport static zipkin2.Span.Kind.SERVER;\nimport static zipkin2.internal.ThriftField.TYPE_I16;\nimport static zipkin2.internal.ThriftField.TYPE_I32;\nimport static zipkin2.internal.ThriftField.TYPE_I64;\nimport static zipkin2.internal.ThriftField.TYPE_LIST;\nimport static zipkin2.internal.ThriftField.TYPE_STRING;\nimport static zipkin2.internal.ThriftField.TYPE_STRUCT;\n\nclass V1ThriftSpanWriterTest {\n  Span span = Span.newBuilder().traceId(\"1\").id(\"2\").build();\n  Endpoint endpoint = Endpoint.newBuilder().serviceName(\"frontend\").ip(\"1.2.3.4\").build();\n  byte[] bytes = new byte[2048]; // bigger than needed to test sizeOf\n  WriteBuffer buf = WriteBuffer.wrap(bytes);\n\n  V1ThriftSpanWriter writer = new V1ThriftSpanWriter();\n  byte[] endpointBytes = new byte[ThriftEndpointCodec.sizeInBytes(endpoint)];\n\n  @BeforeEach void init() {\n    ThriftEndpointCodec.write(endpoint, WriteBuffer.wrap(endpointBytes, 0));\n  }\n\n  @Test void endpoint_highPort() {\n    int highPort = 63840;\n    Endpoint endpoint = Endpoint.newBuilder().ip(\"127.0.0.1\").port(63840).build();\n    byte[] buff = new byte[ThriftEndpointCodec.sizeInBytes(endpoint)];\n    ThriftEndpointCodec.write(endpoint, WriteBuffer.wrap(buff, 0));\n\n    assertThat(buff)\n      .containsSequence(TYPE_I32, 0, 1, 127, 0, 0, 1) // ipv4\n      .containsSequence(TYPE_I16, 0, 2, (highPort >> 8) & 0xFF, highPort & 0xFF); // port\n\n    assertThat(ThriftEndpointCodec.read(ReadBuffer.wrap(buff)).portAsInt())\n      .isEqualTo(highPort);\n  }\n\n  @Test void write_startsWithI64Prefix() {\n    byte[] buff = writer.write(span);\n\n    assertThat(buff)\n      .hasSize(writer.sizeInBytes(span))\n      .startsWith(TYPE_I64, 0, 1); // short value of field number 1\n  }\n\n  @Test void writeList_startsWithListPrefix() {\n    byte[] buff = writer.writeList(List.of(span));\n\n    assertThat(buff)\n      .hasSize(5 + writer.sizeInBytes(span))\n      .startsWith( // member type of the list and an integer with the count\n        TYPE_STRUCT, 0, 0, 0, 1);\n  }\n\n  @Test void writeList_startsWithListPrefix_multiple() {\n    byte[] buff = writer.writeList(List.of(span, span));\n\n    assertThat(buff)\n      .hasSize(5 + writer.sizeInBytes(span) * 2)\n      .startsWith( // member type of the list and an integer with the count\n        TYPE_STRUCT, 0, 0, 0, 2);\n  }\n\n  @Test void writeList_empty() {\n    assertThat(writer.writeList(List.of())).isEmpty();\n  }\n\n  @Test void writeList_offset_startsWithListPrefix() {\n    writer.writeList(List.of(span, span), bytes, 1);\n\n    assertThat(bytes)\n      .startsWith( // member type of the list and an integer with the count\n        0, TYPE_STRUCT, 0, 0, 0, 2);\n  }\n\n  @Test void doesntWriteAnnotationsWhenMissingTimestamp() {\n    writer.write(span.toBuilder().kind(CLIENT).build(), buf);\n\n    byte[] bytes2 = new byte[2048];\n    writer.write(span, WriteBuffer.wrap(bytes2));\n\n    assertThat(bytes).containsExactly(bytes2);\n  }\n\n  @Test void writesCoreAnnotations_client_noEndpoint() {\n    writesCoreAnnotationsNoEndpoint(CLIENT, \"cs\", \"cr\");\n  }\n\n  @Test void writesCoreAnnotations_server_noEndpoint() {\n    writesCoreAnnotationsNoEndpoint(SERVER, \"sr\", \"ss\");\n  }\n\n  @Test void writesCoreAnnotations_producer_noEndpoint() {\n    writesCoreAnnotationsNoEndpoint(PRODUCER, \"ms\", \"ws\");\n  }\n\n  @Test void writesCoreAnnotations_consumer_noEndpoint() {\n    writesCoreAnnotationsNoEndpoint(CONSUMER, \"wr\", \"mr\");\n  }\n\n  void writesCoreAnnotationsNoEndpoint(Span.Kind kind, String begin, String end) {\n    span = span.toBuilder().kind(kind).timestamp(5).duration(10).build();\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 6, TYPE_STRUCT, 0, 0, 0, 2) // two annotations\n      .containsSequence(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5) // timestamp\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 2, begin.charAt(0), begin.charAt(1))\n      .containsSequence(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 15) // timestamp\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 2, end.charAt(0), end.charAt(1));\n  }\n\n  @Test void writesBeginAnnotation_client_noEndpoint() {\n    writesBeginAnnotationNoEndpoint(CLIENT, \"cs\");\n  }\n\n  @Test void writesBeginAnnotation_server_noEndpoint() {\n    writesBeginAnnotationNoEndpoint(SERVER, \"sr\");\n  }\n\n  @Test void writesBeginAnnotation_producer_noEndpoint() {\n    writesBeginAnnotationNoEndpoint(PRODUCER, \"ms\");\n  }\n\n  @Test void writesBeginAnnotation_consumer_noEndpoint() {\n    writesBeginAnnotationNoEndpoint(CONSUMER, \"mr\");\n  }\n\n  void writesBeginAnnotationNoEndpoint(Span.Kind kind, String begin) {\n    span = span.toBuilder().kind(kind).timestamp(5).build();\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 6, TYPE_STRUCT, 0, 0, 0, 1) // one annotation\n      .containsSequence(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5) // timestamp\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 2, begin.charAt(0), begin.charAt(1));\n  }\n\n  @Test void writesAddressBinaryAnnotation_client() {\n    writesAddressBinaryAnnotation(CLIENT, \"sa\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_server() {\n    writesAddressBinaryAnnotation(SERVER, \"ca\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_producer() {\n    writesAddressBinaryAnnotation(PRODUCER, \"ma\");\n  }\n\n  @Test void writesAddressBinaryAnnotation_consumer() {\n    writesAddressBinaryAnnotation(CONSUMER, \"ma\");\n  }\n\n  void writesAddressBinaryAnnotation(Span.Kind kind, String addr) {\n    writer.write(span.toBuilder().kind(kind).remoteEndpoint(endpoint).build(), buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 8, TYPE_STRUCT, 0, 0, 0, 1) // one binary annotation\n      .containsSequence(TYPE_STRING, 0, 1, 0, 0, 0, 2, addr.charAt(0), addr.charAt(1)) // key\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 1, 1) // value\n      .containsSequence(TYPE_I32, 0, 3, 0, 0, 0, 0) // type 0 == boolean\n      .containsSequence(endpointBytes);\n  }\n\n  @Test void annotationsHaveEndpoints() {\n    writer.write(span.toBuilder().localEndpoint(endpoint).addAnnotation(5, \"foo\").build(), buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 6, TYPE_STRUCT, 0, 0, 0, 1) // one annotation\n      .containsSequence(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5) // timestamp\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 3, 'f', 'o', 'o') // value\n      .containsSequence(endpointBytes);\n  }\n\n  @Test void writesTimestampAndDuration() {\n    writer.write(span.toBuilder().timestamp(5).duration(10).build(), buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_I64, 0, 10, 0, 0, 0, 0, 0, 0, 0, 5) // timestamp\n      .containsSequence(TYPE_I64, 0, 11, 0, 0, 0, 0, 0, 0, 0, 10); // duration\n  }\n\n  @Test void writesEmptySpanName() {\n    Span span = Span.newBuilder().traceId(\"1\").id(\"2\").build();\n\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(\n        ThriftField.TYPE_STRING, 0, 3, 0, 0, 0, 0); // name (empty is 32 zero bits)\n  }\n\n  @Test void writesTraceAndSpanIds() {\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .startsWith(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1) // trace ID\n      .containsSequence(TYPE_I64, 0, 4, 0, 0, 0, 0, 0, 0, 0, 2); // ID\n  }\n\n  @Test void writesParentAnd128BitTraceId() {\n    writer.write(\n      Span.newBuilder().traceId(\"00000000000000010000000000000002\").parentId(\"3\").id(\"4\").build(),\n      buf);\n\n    assertThat(bytes)\n      .startsWith(TYPE_I64, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2) // trace ID\n      .containsSequence(TYPE_I64, 0, 12, 0, 0, 0, 0, 0, 0, 0, 1) // trace ID high\n      .containsSequence(TYPE_I64, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3); // parent ID\n  }\n\n  /**\n   * For finagle compatibility\n   */\n  @Test void writesEmptyAnnotationAndBinaryAnnotations() {\n    Span span = Span.newBuilder().traceId(\"1\").id(\"2\").build();\n\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 6, TYPE_STRUCT, 0, 0, 0, 0) // empty annotations\n      .containsSequence(TYPE_LIST, 0, 8, TYPE_STRUCT, 0, 0, 0, 0); // empty binary annotations\n  }\n\n  @Test void writesEmptyLocalComponentWhenNoAnnotationsOrTags() {\n    span = span.toBuilder().name(\"foo\").localEndpoint(endpoint).build();\n\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 8, TYPE_STRUCT, 0, 0, 0, 1) // one binary annotation\n      .containsSequence(TYPE_STRING, 0, 1, 0, 0, 0, 2, 'l', 'c') // key\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 0) // empty value\n      .containsSequence(TYPE_I32, 0, 3, 0, 0, 0, 6) // type 6 == string\n      .containsSequence(endpointBytes);\n  }\n\n  @Test void writesEmptyServiceName() {\n    span =\n      span.toBuilder()\n        .name(\"foo\")\n        .localEndpoint(Endpoint.newBuilder().ip(\"127.0.0.1\").build())\n        .build();\n\n    writer.write(span, buf);\n\n    assertThat(bytes)\n      .containsSequence(\n        ThriftField.TYPE_STRING, 0, 3, 0, 0, 0, 0); // serviceName (empty is 32 zero bits)\n  }\n\n  /** To match finagle */\n  @Test void writesDebugFalse() {\n    span = span.toBuilder().debug(false).build();\n\n    writer.write(span, buf);\n\n    assertThat(bytes).containsSequence(ThriftField.TYPE_BOOL, 0);\n  }\n\n  @Test void tagsAreBinaryAnnotations() {\n    writer.write(span.toBuilder().putTag(\"foo\", \"bar\").build(), buf);\n\n    assertThat(bytes)\n      .containsSequence(TYPE_LIST, 0, 8, TYPE_STRUCT, 0, 0, 0, 1) // one binary annotation\n      .containsSequence(TYPE_STRING, 0, 1, 0, 0, 0, 3, 'f', 'o', 'o') // key\n      .containsSequence(TYPE_STRING, 0, 2, 0, 0, 0, 3, 'b', 'a', 'r') // value\n      .containsSequence(TYPE_I32, 0, 3, 0, 0, 0, 6); // type 6 == string\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/V2SpanWriterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.TODAY;\n\nclass V2SpanWriterTest {\n  V2SpanWriter writer = new V2SpanWriter();\n  byte[] bytes = new byte[2048]; // bigger than needed to test sizeInBytes\n  WriteBuffer buf = WriteBuffer.wrap(bytes);\n\n  @Test void sizeInBytes() {\n    writer.write(CLIENT_SPAN, buf);\n    assertThat(writer.sizeInBytes(CLIENT_SPAN))\n      .isEqualTo(buf.pos());\n  }\n\n  @Test void writes128BitTraceId() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .startsWith(\"{\\\"traceId\\\":\\\"\" + CLIENT_SPAN.traceId() + \"\\\"\");\n  }\n\n  @Test void writesAnnotationWithoutEndpoint() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .contains(\"{\\\"timestamp\\\":\" + (TODAY + 100) * 1000L + \",\\\"value\\\":\\\"foo\\\"}\");\n  }\n\n  @Test void omitsEmptySpanName() {\n    Span span = Span.newBuilder()\n      .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n      .parentId(\"6b221d5bc9e6496c\")\n      .id(\"5b4185666d50f68b\")\n      .build();\n\n    writer.write(span, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .doesNotContain(\"name\");\n  }\n\n  @Test void omitsEmptyServiceName() {\n    Span span = CLIENT_SPAN.toBuilder()\n      .localEndpoint(Endpoint.newBuilder().ip(\"127.0.0.1\").build())\n      .build();\n\n    writer.write(span, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .contains(\"\\\"localEndpoint\\\":{\\\"ipv4\\\":\\\"127.0.0.1\\\"}\");\n  }\n\n  @Test void tagsAreAMap() {\n    writer.write(CLIENT_SPAN, buf);\n\n    assertThat(new String(bytes, UTF_8))\n      .contains(\"\\\"tags\\\":{\\\"clnt/finagle.version\\\":\\\"6.45.0\\\",\\\"http.path\\\":\\\"/api\\\"}\");\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/internal/WriteBufferTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.internal;\n\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass WriteBufferTest {\n  // Adapted from http://stackoverflow.com/questions/8511490/calculating-length-in-utf-8-of-java-string-without-actually-encoding-it\n  @Test void utf8SizeInBytes() {\n    for (int codepoint = 0; codepoint <= 0x10FFFF; codepoint++) {\n      if (codepoint == 0xD800) codepoint = 0xDFFF + 1; // skip surrogates\n      if (Character.isDefined(codepoint)) {\n        String test = new String(Character.toChars(codepoint));\n        int expected = test.getBytes(UTF_8).length;\n        int actual = WriteBuffer.utf8SizeInBytes(test);\n        if (actual != expected) {\n          throw new AssertionError(actual + \" length != \" + expected + \" for \" + codepoint);\n        }\n      }\n    }\n  }\n\n  /** Uses test data and codepoint wrapping trick from okhttp3.FormBodyTest */\n  @Test void utf8_malformed() {\n    for (int codepoint : List.of(0xD800, 0xDFFF, 0xD83D)) {\n      String test = new String(new int[] {'a', codepoint, 'c'}, 0, 3);\n      assertThat(WriteBuffer.utf8SizeInBytes(test))\n        .isEqualTo(3);\n\n      byte[] bytes = new byte[3];\n      WriteBuffer.wrap(bytes).writeUtf8(test);\n      assertThat(bytes)\n        .containsExactly('a', '?', 'c');\n    }\n  }\n\n  @Test void utf8_21Bit_truncated() {\n    // https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)\n    char[] array = \"\\uD83C\\uDC00\\uD83C\\uDC01\".toCharArray();\n    array[array.length - 1] = 'c';\n    String test = new String(array, 0, array.length - 1);\n    assertThat(WriteBuffer.utf8SizeInBytes(test))\n      .isEqualTo(5);\n\n    byte[] bytes = new byte[5];\n    WriteBuffer.wrap(bytes).writeUtf8(test);\n    assertThat(new String(bytes, UTF_8))\n      .isEqualTo(\"\\uD83C\\uDC00?\");\n  }\n\n  @Test void utf8_21Bit_brokenLowSurrogate() {\n    // https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)\n    char[] array = \"\\uD83C\\uDC00\\uD83C\\uDC01\".toCharArray();\n    array[array.length - 1] = 'c';\n    String test = new String(array);\n    assertThat(WriteBuffer.utf8SizeInBytes(test))\n      .isEqualTo(6);\n\n    byte[] bytes = new byte[6];\n    WriteBuffer.wrap(bytes).writeUtf8(test);\n    assertThat(new String(bytes, UTF_8))\n      .isEqualTo(\"\\uD83C\\uDC00?c\");\n  }\n\n  @Test void utf8_matchesJRE() {\n    // examples from http://utf8everywhere.org/\n    for (String string : List.of(\n      \"Приве́т नमस्ते שָׁלוֹם\",\n      \"ю́ cyrillic small letter yu with acute\",\n      \"∃y ∀x ¬(x ≺ y)\"\n    )) {\n      int encodedSize = WriteBuffer.utf8SizeInBytes(string);\n      assertThat(encodedSize)\n        .isEqualTo(string.getBytes(UTF_8).length);\n\n      byte[] bytes = new byte[encodedSize];\n      WriteBuffer.wrap(bytes).writeUtf8(string);\n      assertThat(new String(bytes, UTF_8))\n        .isEqualTo(string);\n    }\n  }\n\n  @Test void utf8_matchesAscii() {\n    String ascii = \"86154a4ba6e913854d1e00c0db9010db\";\n    int encodedSize = WriteBuffer.utf8SizeInBytes(ascii);\n    assertThat(encodedSize)\n      .isEqualTo(ascii.length());\n\n    byte[] bytes = new byte[encodedSize];\n    WriteBuffer.wrap(bytes).writeAscii(ascii);\n    assertThat(new String(bytes, UTF_8))\n      .isEqualTo(ascii);\n\n    WriteBuffer.wrap(bytes).writeUtf8(ascii);\n    assertThat(new String(bytes, UTF_8))\n      .isEqualTo(ascii);\n  }\n\n  @Test void emoji() {\n    byte[] emojiBytes = {(byte) 0xF0, (byte) 0x9F, (byte) 0x98, (byte) 0x81};\n    String emoji = new String(emojiBytes, UTF_8);\n    assertThat(WriteBuffer.utf8SizeInBytes(emoji))\n      .isEqualTo(emojiBytes.length);\n\n    byte[] bytes = new byte[emojiBytes.length];\n    WriteBuffer.wrap(bytes).writeUtf8(emoji);\n    assertThat(bytes)\n      .isEqualTo(emojiBytes);\n  }\n\n  @Test void writeAscii_long() {\n    assertThat(writeAscii(-1005656679588439279L))\n      .isEqualTo(\"-1005656679588439279\");\n    assertThat(writeAscii(0L))\n      .isEqualTo(\"0\");\n    assertThat(writeAscii(-9223372036854775808L /* Long.MIN_VALUE */))\n      .isEqualTo(\"-9223372036854775808\");\n    assertThat(writeAscii(123456789L))\n      .isEqualTo(\"123456789\");\n  }\n\n  static String writeAscii(long v) {\n    byte[] bytes = new byte[WriteBuffer.asciiSizeInBytes(v)];\n    WriteBuffer.wrap(bytes).writeAscii(v);\n    return new String(bytes, UTF_8);\n  }\n\n  // Test creating Buffer for a long string\n  @Test void writeString() {\n    StringBuilder builder = new StringBuilder();\n    for (int i = 0; i < 100000; i++) {\n      builder.append(\"a\");\n    }\n    String string = builder.toString();\n    byte[] bytes = new byte[string.length()];\n    WriteBuffer.wrap(bytes).writeAscii(string);\n    assertThat(new String(bytes, UTF_8)).isEqualTo(string);\n  }\n\n  @Test void unsignedVarintSize_32_largest() {\n    // largest to encode is a negative number\n    assertThat(WriteBuffer.varintSizeInBytes(Integer.MIN_VALUE))\n      .isEqualTo(5);\n  }\n\n  @Test void unsignedVarintSize_64_largest() {\n    // largest to encode is a negative number\n    assertThat(WriteBuffer.varintSizeInBytes(Long.MIN_VALUE))\n      .isEqualTo(10);\n  }\n\n  @Test void writeLongLe_matchesByteBuffer() {\n    for (long number : List.of(Long.MIN_VALUE, 0L, Long.MAX_VALUE)) {\n      byte[] bytes = new byte[8];\n      WriteBuffer.wrap(bytes).writeLongLe(number);\n\n      ByteBuffer byteBuffer = ByteBuffer.allocate(8);\n      byteBuffer.order(ByteOrder.LITTLE_ENDIAN);\n      byteBuffer.putLong(number);\n\n      assertThat(bytes)\n        .containsExactly(byteBuffer.array());\n    }\n  }\n\n  // https://developers.google.com/protocol-buffers/docs/encoding#varints\n  @Test void writeVarint_32() {\n    int number = 300;\n\n    byte[] bytes = new byte[WriteBuffer.varintSizeInBytes(number)];\n    WriteBuffer.wrap(bytes).writeVarint(number);\n\n    assertThat(bytes)\n      .containsExactly(0b1010_1100, 0b0000_0010);\n  }\n\n  // https://developers.google.com/protocol-buffers/docs/encoding#varints\n  @Test void writeVarint_64() {\n    long number = 300;\n\n    byte[] bytes = new byte[WriteBuffer.varintSizeInBytes(number)];\n    WriteBuffer.wrap(bytes).writeVarint(number);\n\n    assertThat(bytes)\n      .containsExactly(0b1010_1100, 0b0000_0010);\n  }\n\n  @Test void writeVarint_ports() {\n    // normal case\n    byte[] bytes = new byte[WriteBuffer.varintSizeInBytes(80)];\n    WriteBuffer.wrap(bytes).writeVarint(80);\n\n    assertThat(bytes)\n      .containsExactly(0b0101_0000);\n\n    // largest value to not require more than 2 bytes (14 bits set)\n    bytes = new byte[WriteBuffer.varintSizeInBytes(16383)];\n    WriteBuffer.wrap(bytes).writeVarint(16383);\n\n    assertThat(bytes)\n      .containsExactly(0b1111_1111, 0b0111_1111);\n\n    // worst case is a byte longer than fixed 16\n    bytes = new byte[WriteBuffer.varintSizeInBytes(65535)];\n    WriteBuffer.wrap(bytes).writeVarint(65535);\n\n    assertThat(bytes)\n      .containsExactly(0b1111_1111, 0b1111_1111, 0b0000_0011);\n\n    // most bits\n    bytes = new byte[WriteBuffer.varintSizeInBytes(0xFFFFFFFF)];\n    WriteBuffer.wrap(bytes).writeVarint(0xFFFFFFFF);\n\n    // we have a total of 32 bits encoded\n    assertThat(bytes)\n      .containsExactly(0b1111_1111, 0b1111_1111, 0b1111_1111, 0b1111_1111, 0b0000_1111);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/storage/ForwardingStorageComponentTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\n\nclass ForwardingStorageComponentTest {\n  /**\n   * This test is intentionally brittle. It should break if we add new methods on {@link\n   * StorageComponent}!\n   */\n  @Test void declaresAllMethodsToForward() {\n    assertThat(ForwardingStorageComponent.class.getDeclaredMethods())\n      .extracting(Method::getName)\n      .containsAll(Stream.concat(\n        Stream.of(Component.class.getDeclaredMethods()).map(Method::getName),\n        Stream.of(StorageComponent.class.getDeclaredMethods()).map(Method::getName)\n      ).collect(Collectors.toList()));\n  }\n\n  StorageComponent delegate = mock(StorageComponent.class);\n  StorageComponent forwarder = new ForwardingStorageComponent() {\n    @Override protected StorageComponent delegate() {\n      return delegate;\n    }\n  };\n\n  @AfterEach void verifyNoExtraCalls() {\n    verifyNoMoreInteractions(delegate);\n  }\n\n  @Test void delegatesToString() {\n    assertThat(forwarder.toString()).isEqualTo(delegate.toString());\n  }\n\n  @Test void delegatesCheck() {\n    CheckResult down = CheckResult.failed(new RuntimeException(\"failed\"));\n    when(delegate.check()).thenReturn(down);\n\n    assertThat(forwarder.check()).isEqualTo(down);\n\n    verify(delegate).check();\n  }\n\n  @Test void delegatesIsOverCapacity() {\n    Exception wayOver = new RejectedExecutionException();\n    when(delegate.isOverCapacity(wayOver)).thenReturn(true);\n\n    assertThat(forwarder.isOverCapacity(wayOver)).isEqualTo(true);\n\n    verify(delegate).isOverCapacity(wayOver);\n  }\n\n  @Test void delegatesClose() throws IOException {\n    doNothing().when(delegate).close();\n\n    forwarder.close();\n\n    verify(delegate).close();\n  }\n\n  @Test void delegatesSpanStore() {\n    SpanStore spanStore = mock(SpanStore.class);\n    when(delegate.spanStore()).thenReturn(spanStore);\n\n    assertThat(forwarder.spanStore()).isEqualTo(spanStore);\n\n    verify(delegate).spanStore();\n  }\n\n  @Test void delegatesAutocompleteTags() {\n    AutocompleteTags autocompleteTags = mock(AutocompleteTags.class);\n    when(delegate.autocompleteTags()).thenReturn(autocompleteTags);\n\n    assertThat(forwarder.autocompleteTags()).isEqualTo(autocompleteTags);\n\n    verify(delegate).autocompleteTags();\n  }\n\n  @Test void delegatesServiceAndSpanNames() {\n    ServiceAndSpanNames serviceAndSpanNames = mock(ServiceAndSpanNames.class);\n    when(delegate.serviceAndSpanNames()).thenReturn(serviceAndSpanNames);\n\n    assertThat(forwarder.serviceAndSpanNames()).isEqualTo(serviceAndSpanNames);\n\n    verify(delegate).serviceAndSpanNames();\n  }\n\n  @Test void delegatesSpanConsumer() {\n    SpanConsumer spanConsumer = mock(SpanConsumer.class);\n    when(delegate.spanConsumer()).thenReturn(spanConsumer);\n\n    assertThat(forwarder.spanConsumer()).isEqualTo(spanConsumer);\n\n    verify(delegate).spanConsumer();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/storage/GroupByTraceIdTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass GroupByTraceIdTest {\n  Span oneOne = Span.newBuilder().traceId(1, 1).id(1).build();\n  Span twoOne = Span.newBuilder().traceId(2, 1).id(1).build();\n  Span zeroOne = Span.newBuilder().traceId(0, 1).id(1).build();\n\n  @Test void map_groupsEverythingWhenNotStrict() {\n    List<Span> spans = List.of(oneOne, twoOne, zeroOne);\n\n    assertThat(GroupByTraceId.create(false).map(spans)).containsExactly(spans);\n  }\n\n  @Test void map_groupsByTraceIdHighWheStrict() {\n    List<Span> spans = List.of(oneOne, twoOne, zeroOne);\n\n    assertThat(GroupByTraceId.create(true).map(spans))\n      .containsExactly(List.of(oneOne), List.of(twoOne), List.of(zeroOne));\n  }\n\n  @Test void map_modifiable() {\n    List<Span> spans = List.of(oneOne, twoOne, zeroOne);\n\n    List<List<Span>> modifiable = GroupByTraceId.create(true).map(spans);\n\n    // This transform is used in cassandra v1 and filters when traces match on lower 64bits, but not\n    // the higher ones.\n    assertThat(StrictTraceId.filterTraces(List.of(twoOne.traceId())).map(modifiable))\n      .containsExactly(List.of(twoOne));\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/storage/InMemoryStorageTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Component;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.requestBuilder;\n\nclass InMemoryStorageTest {\n  InMemoryStorage storage =\n    InMemoryStorage.newBuilder().autocompleteKeys(List.of(\"http.path\")).build();\n\n  @Test void getTraces_filteringMatchesMostRecentTraces() throws IOException {\n    List<Endpoint> endpoints = IntStream.rangeClosed(1, 10)\n      .mapToObj(i -> Endpoint.newBuilder().serviceName(\"service\" + i).ip(\"127.0.0.1\").build())\n      .toList();\n\n    long gapBetweenSpans = 100;\n    List<Span> earlySpans =\n      IntStream.rangeClosed(1, 10).mapToObj(i -> Span.newBuilder().name(\"early\")\n        .traceId(Integer.toHexString(i)).id(Integer.toHexString(i))\n        .timestamp((TODAY - i) * 1000).duration(1L)\n        .localEndpoint(endpoints.get(i - 1)).build()).collect(toList());\n\n    List<Span> lateSpans = IntStream.rangeClosed(1, 10).mapToObj(i -> Span.newBuilder().name(\"late\")\n      .traceId(Integer.toHexString(i + 10)).id(Integer.toHexString(i + 10))\n      .timestamp((TODAY + gapBetweenSpans - i) * 1000).duration(1L)\n      .localEndpoint(endpoints.get(i - 1)).build()).collect(toList());\n\n    storage.accept(earlySpans).execute();\n    storage.accept(lateSpans).execute();\n\n    List<Span>[] earlyTraces =\n      earlySpans.stream().map(Collections::singletonList).toArray(List[]::new);\n    List<Span>[] lateTraces =\n      lateSpans.stream().map(Collections::singletonList).toArray(List[]::new);\n\n    //sanity checks\n    assertThat(storage.getTraces(requestBuilder().serviceName(\"service1\").build()).execute())\n      .containsExactly(lateTraces[0], earlyTraces[0]);\n\n    assertThat(storage.getTraces(requestBuilder().build()).execute())\n      .hasSize(20);\n\n    assertThat(storage.getTraces(requestBuilder()\n      .limit(10).build()).execute())\n      .containsExactly(lateTraces);\n\n    assertThat(storage.getTraces(requestBuilder()\n      .endTs(TODAY + gapBetweenSpans).lookback(gapBetweenSpans).build()).execute())\n      .containsExactly(lateTraces);\n\n    assertThat(storage.getTraces(requestBuilder()\n      .endTs(TODAY).build()).execute())\n      .containsExactly(earlyTraces);\n  }\n\n  /** Ensures we don't overload a partition due to key equality being conflated with order */\n  @Test void differentiatesOnTraceIdWhenTimestampEqual() throws IOException {\n    storage.accept(List.of(CLIENT_SPAN)).execute();\n    storage.accept(List.of(CLIENT_SPAN.toBuilder().traceId(\"333\").build())).execute();\n\n    assertThat(storage).extracting(\"spansByTraceIdTimestamp.delegate\")\n      .satisfies(map -> assertThat((Map) map).hasSize(2));\n  }\n\n  /** It should be safe to run dependency link jobs twice */\n  @Test void replayOverwrites() throws IOException {\n    Span span = Span.newBuilder().traceId(\"10\").id(\"10\").name(\"receive\")\n      .kind(Span.Kind.CONSUMER)\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .remoteEndpoint(Endpoint.newBuilder().serviceName(\"kafka\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n\n    storage.accept(List.of(span)).execute();\n    storage.accept(List.of(span)).execute();\n\n    assertThat(storage.getDependencies(TODAY + 1000L, TODAY).execute()).containsOnly(\n      DependencyLink.newBuilder().parent(\"kafka\").child(\"app\").callCount(1L).build()\n    );\n  }\n\n  @Test void getSpanNames_skipsNullSpanName() throws IOException {\n    Span span1 = Span.newBuilder().traceId(\"1\").id(\"1\").name(\"root\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n\n    Span span2 = Span.newBuilder().traceId(\"1\").parentId(\"1\").id(\"2\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n\n    storage.accept(List.of(span1, span2)).execute();\n\n    assertThat(storage.getSpanNames(\"app\").execute()).containsOnly(\n      \"root\"\n    );\n  }\n\n  @Test void getTagsAndThenValues() throws IOException {\n    Span span1 = Span.newBuilder().traceId(\"1\").id(\"1\").name(\"root\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .putTag(\"environment\", \"dev\")\n      .putTag(\"http.method\", \"GET\")\n      .timestamp(TODAY * 1000)\n      .build();\n    Span span2 = Span.newBuilder().traceId(\"1\").parentId(\"1\").id(\"2\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .putTag(\"environment\", \"dev\")\n      .putTag(\"http.method\", \"POST\")\n      .putTag(\"http.path\", \"/users\")\n      .timestamp(TODAY * 1000)\n      .build();\n    Span span3 = Span.newBuilder().traceId(\"2\").id(\"3\").name(\"root\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .putTag(\"environment\", \"dev\")\n      .putTag(\"http.method\", \"GET\")\n      .timestamp(TODAY * 1000)\n      .build();\n    Span span4 = Span.newBuilder().traceId(\"2\").parentId(\"3\").id(\"4\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .putTag(\"environment\", \"dev\")\n      .putTag(\"http.method\", \"POST\")\n      .putTag(\"http.path\", \"/users\")\n      .timestamp(TODAY * 1000)\n      .build();\n    storage.accept(List.of(span1, span2, span3, span4)).execute();\n\n    assertThat(storage.getKeys().execute()).containsOnlyOnce(\"http.path\");\n    assertThat(storage.getValues(\"http.path\").execute()).containsOnlyOnce(\"/users\");\n  }\n\n  @Test void getTraces_byTraceIds() throws IOException {\n    Span trace1Span1 = Span.newBuilder().traceId(\"1\").id(\"1\").name(\"root\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n    Span trace1Span2 = Span.newBuilder().traceId(\"1\").parentId(\"1\").id(\"2\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n\n    Span trace2Span1 = Span.newBuilder().traceId(\"2\").id(\"1\").name(\"root\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n    Span trace2Span2 = Span.newBuilder().traceId(\"2\").parentId(\"1\").id(\"2\")\n      .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n      .timestamp(TODAY * 1000)\n      .build();\n\n    storage.accept(List.of(trace1Span1, trace1Span2, trace2Span1, trace2Span2)).execute();\n\n    assertThat(storage.getTraces(List.of(\"1\", \"2\")).execute()).containsExactly(\n      List.of(trace1Span1, trace1Span2),\n      List.of(trace2Span1, trace2Span2)\n    );\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    try (InMemoryStorage storage = InMemoryStorage.newBuilder().build()) {\n      assertThat(storage).hasToString(\"InMemoryStorage{}\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/storage/QueryRequestTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.entry;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nclass QueryRequestTest {\n  QueryRequest.Builder queryBuilder =\n    QueryRequest.newBuilder().endTs(TestObjects.TODAY).lookback(60).limit(10);\n  Span span = Span.newBuilder().traceId(\"10\").id(\"10\").name(\"receive\")\n    .localEndpoint(Endpoint.newBuilder().serviceName(\"app\").build())\n    .kind(Span.Kind.CONSUMER)\n    .timestamp(TestObjects.TODAY * 1000)\n    .build();\n\n  @Test void serviceNameCanBeNull() {\n    assertThat(queryBuilder.build().serviceName())\n      .isNull();\n  }\n\n  @Test void serviceName_coercesEmptyToNull() {\n    assertThat(queryBuilder.serviceName(\"\").build().serviceName())\n      .isNull();\n  }\n\n  @Test void remoteServiceNameCanBeNull() {\n    assertThat(queryBuilder.build().remoteServiceName())\n      .isNull();\n  }\n\n  @Test void remoteServiceName_coercesEmptyToNull() {\n    assertThat(queryBuilder.remoteServiceName(\"\").build().remoteServiceName())\n      .isNull();\n  }\n\n  @Test void spanName_coercesAllToNull() {\n    assertThat(queryBuilder.spanName(\"all\").build().spanName())\n      .isNull();\n  }\n\n  @Test void spanName_coercesEmptyToNull() {\n    assertThat(queryBuilder.spanName(\"\").build().spanName())\n      .isNull();\n  }\n\n  @Test void annotationQuerySkipsEmptyKeys() {\n    Map<String, String> query = new LinkedHashMap<>();\n    query.put(\"\", \"bar\");\n\n    assertThat(queryBuilder.annotationQuery(query).build().annotationQueryString())\n      .isNull();\n  }\n\n  @Test void annotationQueryTrimsSpaces() {\n    // spaces in http.path mixed with 'and'\n    assertThat(queryBuilder.parseAnnotationQuery(\"fo and o and bar and http.path = /a \").annotationQuery)\n      .containsOnly(entry(\"fo\", \"\"), entry(\"o\", \"\"), entry(\"bar\", \"\"), entry(\"http.path\", \"/a\"));\n    // http.path in the beginning, more spaces\n    assertThat(queryBuilder.parseAnnotationQuery(\" http.path = /a   and fo and o   and bar\").annotationQuery)\n      .containsOnly(entry(\"fo\", \"\"), entry(\"o\", \"\"), entry(\"bar\", \"\"), entry(\"http.path\", \"/a\"));\n    // @adriancole said this would be hard to parse, annotation containing spaces\n    assertThat(queryBuilder.parseAnnotationQuery(\"L O L\").annotationQuery)\n      .containsOnly(entry(\"L O L\", \"\"));\n    // annotation with spaces combined with tag\n    assertThat(queryBuilder.parseAnnotationQuery(\"L O L and http.path = /a\").annotationQuery)\n      .containsOnly(entry(\"L O L\", \"\"), entry(\"http.path\", \"/a\"));\n    assertThat(queryBuilder.parseAnnotationQuery(\"bar =123 and L O L and http.path = /a and A B C\").annotationQuery)\n      .containsOnly(entry(\"L O L\", \"\"), entry(\"http.path\", \"/a\"), entry(\"bar\", \"123\"), entry(\"A B C\", \"\"));\n  }\n\n  @Test void annotationQueryParameterSpecificity() {\n    // when a parameter is specified both as a tag and annotation, the tag wins because it's considered to be more\n    // specific\n    assertThat(queryBuilder.parseAnnotationQuery(\"a=123 and a\").annotationQuery).containsOnly(entry(\"a\", \"123\"));\n    assertThat(queryBuilder.parseAnnotationQuery(\"a and a=123\").annotationQuery).containsOnly(entry(\"a\", \"123\"));\n    // also last tag wins\n    assertThat(queryBuilder.parseAnnotationQuery(\"a=123 and a=456\").annotationQuery).containsOnly(entry(\"a\", \"456\"));\n    assertThat(queryBuilder.parseAnnotationQuery(\"a and a=123 and a=456\").annotationQuery).containsOnly(entry(\"a\", \"456\"));\n  }\n\n  @Test void endTsMustBePositive() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.endTs(0L).build();\n    });\n    assertThat(exception.getMessage()).contains(\"endTs <= 0\");\n  }\n\n  @Test void lookbackMustBePositive() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.lookback(0).build();\n    });\n    assertThat(exception.getMessage()).contains(\"lookback <= 0\");\n  }\n\n  @Test void limitMustBePositive() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.limit(0).build();\n    });\n    assertThat(exception.getMessage()).contains(\"limit <= 0\");\n  }\n\n  @Test void annotationQuery_roundTrip() {\n    String annotationQuery = \"http.method=GET and error\";\n\n    QueryRequest request = queryBuilder\n      .serviceName(\"security-service\")\n      .parseAnnotationQuery(annotationQuery)\n      .build();\n\n    assertThat(request.annotationQuery())\n      .containsEntry(\"error\", \"\")\n      .containsEntry(\"http.method\", \"GET\");\n\n    assertThat(request.annotationQueryString())\n      .isEqualTo(annotationQuery);\n  }\n\n  @Test void annotationQuery_missingValue() {\n    String annotationQuery = \"http.method=\";\n\n    QueryRequest request = queryBuilder\n      .serviceName(\"security-service\")\n      .parseAnnotationQuery(annotationQuery)\n      .build();\n\n    assertThat(request.annotationQuery())\n      .containsKey(\"http.method\");\n  }\n\n  @Test void annotationQueryWhenNoInputIsEmpty() {\n    assertThat(queryBuilder.build().annotationQuery())\n      .isEmpty();\n  }\n\n  @Test void minDuration_mustBePositive() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.minDuration(0L).build();\n    });\n    assertThat(exception.getMessage()).contains(\"minDuration <= 0\");\n  }\n\n  @Test void maxDuration_onlyWithMinDuration() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.maxDuration(0L).build();\n    });\n    assertThat(exception.getMessage()).contains(\"maxDuration is only valid with minDuration\");\n  }\n\n  @Test void maxDuration_greaterThanOrEqualToMinDuration() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {\n\n      queryBuilder.minDuration(1L).maxDuration(0L).build();\n    });\n    assertThat(exception.getMessage()).contains(\"maxDuration < minDuration\");\n  }\n\n  @Test void test_matchesTimestamp() {\n    QueryRequest request = queryBuilder\n      .build();\n\n    assertThat(request.test(List.of(span)))\n      .isTrue();\n  }\n\n  @Test void test_rootSpanNotFirst() {\n    QueryRequest request = queryBuilder\n      .build();\n\n    assertThat(request.test(List.of(\n      span.toBuilder().id(\"2\").parentId(span.id()).timestamp(null).build(),\n      span\n    ))).isTrue();\n  }\n\n  @Test void test_noRootSpanLeastWins() {\n    QueryRequest request = queryBuilder\n      .build();\n\n    assertThat(request.test(List.of(\n      span.toBuilder().id(\"2\").parentId(span.id()).timestamp(span.timestamp() + TestObjects.DAY * 1000).build(),\n      span.toBuilder().id(\"3\").parentId(span.id()).build()\n    ))).isTrue();\n  }\n\n  @Test void test_noTimestamp() {\n    QueryRequest request = queryBuilder\n      .build();\n\n    assertThat(request.test(List.of(span.toBuilder().timestamp(null).build())))\n      .isFalse();\n  }\n\n  @Test void test_timestampPastLookback() {\n    QueryRequest request = queryBuilder\n      .endTs(TestObjects.TODAY + 70)\n      .build();\n\n    assertThat(request.test(List.of(span)))\n      .isFalse();\n  }\n\n  @Test void test_wrongServiceName() {\n    QueryRequest request = queryBuilder\n      .serviceName(\"aloha\")\n      .build();\n\n    assertThat(request.test(List.of(span)))\n      .isFalse();\n  }\n\n  @Test void test_spanName() {\n    QueryRequest request = queryBuilder\n      .spanName(\"aloha\")\n      .build();\n\n    assertThat(request.test(List.of(span)))\n      .isFalse();\n\n    assertThat(request.test(List.of(span.toBuilder().name(\"aloha\").build())))\n      .isTrue();\n  }\n\n  @Test void test_remoteServiceName() {\n    QueryRequest request = queryBuilder\n      .remoteServiceName(\"db\")\n      .build();\n\n    assertThat(request.test(List.of(span)))\n      .isFalse();\n\n    assertThat(request.test(List.of(span.toBuilder().remoteEndpoint(Endpoint.newBuilder().serviceName(\"db\").build()).build())))\n      .isTrue();\n  }\n\n  @Test void test_minDuration() {\n    QueryRequest request = queryBuilder\n      .minDuration(100L)\n      .build();\n\n    assertThat(request.test(List.of(span.toBuilder().duration(99L).build())))\n      .isFalse();\n\n    assertThat(request.test(List.of(span.toBuilder().duration(100L).build())))\n      .isTrue();\n  }\n\n  @Test void test_maxDuration() {\n    QueryRequest request = queryBuilder\n      .minDuration(100L)\n      .maxDuration(110L)\n      .build();\n\n    assertThat(request.test(List.of(span.toBuilder().duration(99L).build())))\n      .isFalse();\n\n    assertThat(request.test(List.of(span.toBuilder().duration(100L).build())))\n      .isTrue();\n\n    assertThat(request.test(List.of(span.toBuilder().duration(111L).build())))\n      .isFalse();\n  }\n\n  Span foo = span.toBuilder().traceId(\"1\").name(\"call1\").id(\"1\")\n    .addAnnotation(span.timestamp(), \"foo\").build();\n  // would be foo bar, except lexicographically bar precedes foo\n  Span barAndFoo = span.toBuilder().traceId(\"2\").name(\"call2\").id(\"2\")\n    .addAnnotation(span.timestamp(), \"bar\")\n    .addAnnotation(span.timestamp(), \"foo\").build();\n  Span fooAndBazAndQux = span.toBuilder().traceId(\"3\").name(\"call3\").id(\"3\")\n    .addAnnotation(span.timestamp(), \"foo\")\n    .putTag(\"baz\", \"qux\")\n    .build();\n  Span barAndFooAndBazAndQux = span.toBuilder().traceId(\"4\").name(\"call4\").id(\"4\")\n    .addAnnotation(span.timestamp(), \"bar\")\n    .addAnnotation(span.timestamp(), \"foo\")\n    .putTag(\"baz\", \"qux\")\n    .build();\n\n  @Test void test_annotationQuery_tagKey() {\n    QueryRequest query = queryBuilder\n      .parseAnnotationQuery(\"baz\").build();\n\n    assertThat(query.test(List.of(foo)))\n      .isFalse();\n    assertThat(query.test(List.of(barAndFoo)))\n      .isFalse();\n    assertThat(query.test(List.of(barAndFooAndBazAndQux)))\n      .isTrue();\n    assertThat(query.test(List.of(fooAndBazAndQux)))\n      .isTrue();\n  }\n\n  @Test void test_annotationQuery_annotation() {\n    QueryRequest query = queryBuilder\n      .parseAnnotationQuery(\"foo\").build();\n\n    assertThat(query.test(List.of(foo)))\n      .isTrue();\n    assertThat(query.test(List.of(barAndFoo)))\n      .isTrue();\n    assertThat(query.test(List.of(barAndFooAndBazAndQux)))\n      .isTrue();\n    assertThat(query.test(List.of(fooAndBazAndQux)))\n      .isTrue();\n  }\n\n  @Test void test_annotationQuery_twoAnnotation() {\n    QueryRequest query = queryBuilder\n      .parseAnnotationQuery(\"foo and bar\").build();\n\n    assertThat(query.test(List.of(foo)))\n      .isFalse();\n    assertThat(query.test(List.of(barAndFoo)))\n      .isTrue();\n    assertThat(query.test(List.of(barAndFooAndBazAndQux)))\n      .isTrue();\n    assertThat(query.test(List.of(fooAndBazAndQux)))\n      .isFalse();\n  }\n\n  @Test void test_annotationQuery_annotationsAndTag() {\n    QueryRequest query = queryBuilder\n      .parseAnnotationQuery(\"foo and bar and baz=qux\").build();\n\n    assertThat(query.test(List.of(foo)))\n      .isFalse();\n    assertThat(query.test(List.of(barAndFoo)))\n      .isFalse();\n    assertThat(query.test(List.of(barAndFooAndBazAndQux)))\n      .isTrue();\n    assertThat(query.test(List.of(fooAndBazAndQux)))\n      .isFalse();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/storage/StrictTraceIdTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.TRACE;\nimport static zipkin2.TestObjects.requestBuilder;\n\nclass StrictTraceIdTest {\n\n  @Test void filterTraces_skipsOnNoClash() {\n    Span oneOne = Span.newBuilder().traceId(1, 1).id(1).build();\n    Span oneTwo = Span.newBuilder().traceId(1, 2).id(1).build();\n    List<List<Span>> traces = List.of(List.of(oneOne), List.of(oneTwo));\n\n    assertThat(StrictTraceId.filterTraces(\n      requestBuilder().spanName(\"11\").build()\n    ).map(traces)).isSameAs(traces);\n  }\n\n  @Test void filterTraces_onSpanName() {\n    assertThat(StrictTraceId.filterTraces(\n      requestBuilder().spanName(\"11\").build()\n    ).map(traces())).flatExtracting(l -> l).isEmpty();\n\n    assertThat(StrictTraceId.filterTraces(\n      requestBuilder().spanName(\"1\").build()\n    ).map(traces())).containsExactly(traces().get(0));\n  }\n\n  @Test void filterTraces_onTag() {\n    assertThat(StrictTraceId.filterTraces(\n      requestBuilder().parseAnnotationQuery(\"foo=0\").build()\n    ).map(traces())).flatExtracting(l -> l).isEmpty();\n\n    assertThat(StrictTraceId.filterTraces(\n      requestBuilder().parseAnnotationQuery(\"foo=1\").build()\n    ).map(traces())).containsExactly(traces().get(0));\n  }\n\n  @Test void filterSpans() {\n    ArrayList<Span> trace = new ArrayList<>(TRACE);\n\n    assertThat(StrictTraceId.filterSpans(CLIENT_SPAN.traceId()).map(trace))\n      .isEqualTo(TRACE);\n\n    trace.set(1, CLIENT_SPAN.toBuilder().traceId(CLIENT_SPAN.traceId().substring(16)).build());\n    assertThat(StrictTraceId.filterSpans(CLIENT_SPAN.traceId()).map(trace))\n      .doesNotContain(CLIENT_SPAN);\n  }\n\n  List<List<Span>> traces() {\n    // 64-bit trace ID\n    Span span1 = Span.newBuilder().traceId(CLIENT_SPAN.traceId().substring(16)).id(\"1\")\n      .name(\"1\")\n      .putTag(\"foo\", \"1\")\n      .timestamp(TODAY * 1000L)\n      .localEndpoint(FRONTEND)\n      .build();\n    // 128-bit trace ID prefixed by above\n    Span span2 =\n      span1.toBuilder().traceId(CLIENT_SPAN.traceId()).name(\"2\").putTag(\"foo\", \"2\").build();\n    // Different 128-bit trace ID prefixed by above\n    Span span3 =\n      span1.toBuilder().traceId(\"1\" + span1.traceId()).name(\"3\").putTag(\"foo\", \"3\").build();\n\n    return new ArrayList<>(List.of(List.of(span1), List.of(span2), List.of(span3)));\n  }\n\n  @Test void hasClashOnLowerTraceId() {\n    Span oneOne = Span.newBuilder().traceId(1, 1).id(1).build();\n    Span twoOne = Span.newBuilder().traceId(2, 1).id(1).build();\n    Span zeroOne = Span.newBuilder().traceId(0, 1).id(1).build();\n    Span oneTwo = Span.newBuilder().traceId(1, 2).id(1).build();\n\n    assertThat(StrictTraceId.hasClashOnLowerTraceId(List.of(List.of(oneOne), List.of(oneTwo))))\n      .isFalse();\n    assertThat(StrictTraceId.hasClashOnLowerTraceId(List.of(List.of(oneOne), List.of(twoOne))))\n      .isTrue();\n    assertThat(StrictTraceId.hasClashOnLowerTraceId(List.of(List.of(oneOne), List.of(zeroOne))))\n      .isTrue();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/v1/SpanConverterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\n\nclass SpanConverterTest {\n  Endpoint kafka = Endpoint.newBuilder().serviceName(\"kafka\").build();\n  V2SpanConverter v2SpanConverter = new V2SpanConverter();\n  V1SpanConverter v1SpanConverter = new V1SpanConverter();\n\n  @Test void client() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"get\")\n            .kind(Kind.CLIENT)\n            .localEndpoint(FRONTEND)\n            .remoteEndpoint(BACKEND)\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996238000L, \"ws\")\n            .addAnnotation(1472470996403000L, \"wr\")\n            .putTag(\"http.path\", \"/api\")\n            .putTag(\"clnt/finagle.version\", \"6.45.0\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"ws\", FRONTEND) // ts order retained\n            .addAnnotation(1472470996403000L, \"wr\", FRONTEND)\n            .addAnnotation(1472470996406000L, \"cr\", FRONTEND)\n            .addBinaryAnnotation(\"http.path\", \"/api\", FRONTEND)\n            .addBinaryAnnotation(\"clnt/finagle.version\", \"6.45.0\", FRONTEND)\n            .addBinaryAnnotation(\"sa\", BACKEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void client_unfinished() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"get\")\n            .kind(Kind.CLIENT)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .addAnnotation(1472470996238000L, \"ws\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"ws\", FRONTEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void client_kindInferredFromAnnotation() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"get\")\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .duration(1472470996238000L - 1472470996199000L)\n            .addAnnotation(1472470996199000L, \"cs\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(1472470996238000L - 1472470996199000L)\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"cr\", FRONTEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n  }\n\n  @Test void lateRemoteEndpoint_cr() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"get\")\n        .kind(Kind.CLIENT)\n        .localEndpoint(FRONTEND)\n        .remoteEndpoint(BACKEND)\n        .addAnnotation(1472470996199000L, \"cr\")\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"get\")\n        .addAnnotation(1472470996199000L, \"cr\", FRONTEND)\n        .addBinaryAnnotation(\"sa\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void lateRemoteEndpoint_sa() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .remoteEndpoint(BACKEND)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .addBinaryAnnotation(\"sa\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void noAnnotationsExceptAddresses() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"get\")\n            .localEndpoint(FRONTEND)\n            .remoteEndpoint(BACKEND)\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addBinaryAnnotation(\"lc\", \"\", FRONTEND)\n            .addBinaryAnnotation(\"sa\", BACKEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void server() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .id(\"2\")\n            .name(\"get\")\n            .kind(Kind.SERVER)\n            .localEndpoint(BACKEND)\n            .remoteEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .putTag(\"http.path\", \"/api\")\n            .putTag(\"finagle.version\", \"6.45.0\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .id(2L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n            .addAnnotation(1472470996406000L, \"ss\", BACKEND)\n            .addBinaryAnnotation(\"http.path\", \"/api\", BACKEND)\n            .addBinaryAnnotation(\"finagle.version\", \"6.45.0\", BACKEND)\n            .addBinaryAnnotation(\"ca\", FRONTEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** This shows a historical finagle span, which has client-side socket info. */\n  @Test void server_clientAddress() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(BACKEND)\n        .remoteEndpoint(FRONTEND.toBuilder().port(63840).build())\n        .timestamp(TODAY)\n        .duration(207000L)\n        .addAnnotation(TODAY + 500L,\n          \"Gc(9,0.PSScavenge,2015-09-17 12:37:02 +0000,304.milliseconds+762.microseconds)\")\n        .putTag(\"srv/finagle.version\", \"6.28.0\")\n        .shared(true)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .addAnnotation(v2.timestampAsLong(), \"sr\", v2.localEndpoint())\n        .addAnnotation(\n          v2.timestampAsLong() + 500L,\n          \"Gc(9,0.PSScavenge,2015-09-17 12:37:02 +0000,304.milliseconds+762.microseconds)\",\n          v2.localEndpoint())\n        .addAnnotation(v2.timestampAsLong() + v2.durationAsLong(), \"ss\", v2.localEndpoint())\n        // Sometimes, finagle does not add port info on binary annotations/tags, but does elsewhere\n        .addBinaryAnnotation(\"srv/finagle.version\", \"6.28.0\",\n          v2.localEndpoint().toBuilder().port(0).build())\n        .addBinaryAnnotation(\"sa\", v2.localEndpoint())\n        .addBinaryAnnotation(\"ca\", v2.remoteEndpoint())\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** Buggy instrumentation can send data with missing endpoints. Make sure we can record it. */\n  @Test void missingEndpoints() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"1\")\n            .id(\"2\")\n            .name(\"foo\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(1L)\n            .id(2L)\n            .name(\"foo\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** No special treatment for invalid core annotations: missing endpoint */\n  @Test void missingEndpoints_coreAnnotation() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"1\")\n            .id(\"2\")\n            .name(\"foo\")\n            .timestamp(1472470996199000L)\n            .addAnnotation(1472470996199000L, \"sr\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(1L)\n            .id(2L)\n            .name(\"foo\")\n            .timestamp(1472470996199000L)\n            .addAnnotation(1472470996199000L, \"sr\", null)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void server_shared_v1_no_timestamp_duration() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId('2')\n        .id(\"3\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .shared(true)\n        .localEndpoint(BACKEND)\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(\"1\")\n        .parentId('2')\n        .id(\"3\")\n        .name(\"get\")\n        .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n        .addAnnotation(1472470996406000L, \"ss\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void server_incomplete_shared() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId('2')\n        .id(\"3\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .shared(true)\n        .localEndpoint(BACKEND)\n        .timestamp(1472470996199000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(\"1\")\n        .parentId('2')\n        .id(\"3\")\n        .name(\"get\")\n        .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** Late flushed data on a v2 span */\n  @Test void lateRemoteEndpoint_ss() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .id(\"2\")\n            .name(\"get\")\n            .kind(Kind.SERVER)\n            .localEndpoint(BACKEND)\n            .remoteEndpoint(FRONTEND)\n            .addAnnotation(1472470996199000L, \"ss\")\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .id(2L)\n            .name(\"get\")\n            .addAnnotation(1472470996199000L, \"ss\", BACKEND)\n            .addBinaryAnnotation(\"ca\", FRONTEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** Late flushed data on a v1 v1 */\n  @Test void lateRemoteEndpoint_ca() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .id(\"2\")\n            .kind(Kind.SERVER)\n            .remoteEndpoint(FRONTEND)\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .id(2L)\n            .addBinaryAnnotation(\"ca\", FRONTEND)\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void localSpan_emptyComponent() {\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .id(\"2\")\n            .name(\"local\")\n            .localEndpoint(Endpoint.newBuilder().serviceName(\"frontend\").build())\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .build();\n\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .id(2L)\n            .name(\"local\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addBinaryAnnotation(\"lc\", \"\", Endpoint.newBuilder().serviceName(\"frontend\").build())\n            .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void producer_remote() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"send\")\n        .kind(Kind.PRODUCER)\n        .localEndpoint(FRONTEND)\n        .remoteEndpoint(kafka)\n        .timestamp(1472470996199000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"send\")\n        .timestamp(1472470996199000L)\n        .addAnnotation(1472470996199000L, \"ms\", FRONTEND)\n        .addBinaryAnnotation(\"ma\", kafka)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void producer_duration() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"send\")\n        .kind(Kind.PRODUCER)\n        .localEndpoint(FRONTEND)\n        .timestamp(1472470996199000L)\n        .duration(51000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"send\")\n        .timestamp(1472470996199000L)\n        .duration(51000L)\n        .addAnnotation(1472470996199000L, \"ms\", FRONTEND)\n        .addAnnotation(1472470996250000L, \"ws\", FRONTEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void consumer() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"next-message\")\n        .kind(Kind.CONSUMER)\n        .localEndpoint(BACKEND)\n        .timestamp(1472470996199000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"next-message\")\n        .timestamp(1472470996199000L)\n        .addAnnotation(1472470996199000L, \"mr\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void consumer_remote() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"next-message\")\n        .kind(Kind.CONSUMER)\n        .localEndpoint(BACKEND)\n        .remoteEndpoint(kafka)\n        .timestamp(1472470996199000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"next-message\")\n        .timestamp(1472470996199000L)\n        .addAnnotation(1472470996199000L, \"mr\", BACKEND)\n        .addBinaryAnnotation(\"ma\", kafka)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void consumer_duration() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"next-message\")\n        .kind(Kind.CONSUMER)\n        .localEndpoint(BACKEND)\n        .timestamp(1472470996199000L)\n        .duration(51000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"next-message\")\n        .timestamp(1472470996199000L)\n        .duration(51000L)\n        .addAnnotation(1472470996199000L, \"wr\", BACKEND)\n        .addAnnotation(1472470996250000L, \"mr\", BACKEND)\n        .build();\n\n    assertThat(v2SpanConverter.convert(v2)).usingRecursiveComparison().isEqualTo(v1);\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void clientAndServer() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"ws\", FRONTEND)\n            .addAnnotation(1472470996250000L, \"sr\", BACKEND)\n            .addAnnotation(1472470996350000L, \"ss\", BACKEND)\n            .addAnnotation(1472470996403000L, \"wr\", FRONTEND)\n            .addAnnotation(1472470996406000L, \"cr\", FRONTEND)\n            .addBinaryAnnotation(\"http.path\", \"/api\", FRONTEND)\n            .addBinaryAnnotation(\"http.path\", \"/BACKEND\", BACKEND)\n            .addBinaryAnnotation(\"clnt/finagle.version\", \"6.45.0\", FRONTEND)\n            .addBinaryAnnotation(\"srv/finagle.version\", \"6.44.0\", BACKEND)\n            .addBinaryAnnotation(\"ca\", FRONTEND)\n            .addBinaryAnnotation(\"sa\", BACKEND)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"get\");\n\n    // the v1 side owns timestamp and duration\n    Span clientV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.CLIENT)\n            .localEndpoint(FRONTEND)\n            .remoteEndpoint(BACKEND)\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996238000L, \"ws\")\n            .addAnnotation(1472470996403000L, \"wr\")\n            .putTag(\"http.path\", \"/api\")\n            .putTag(\"clnt/finagle.version\", \"6.45.0\")\n            .build();\n\n    // notice v1 tags are different than the v1, and the v1's annotations aren't here\n    Span serverV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.SERVER)\n            .shared(true)\n            .localEndpoint(BACKEND)\n            .remoteEndpoint(FRONTEND)\n            .timestamp(1472470996250000L)\n            .duration(100000L)\n            .putTag(\"http.path\", \"/BACKEND\")\n            .putTag(\"srv/finagle.version\", \"6.44.0\")\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(clientV2, serverV2);\n  }\n\n  /**\n   * The old v1 format had no means of saying it is shared or not. This uses lack of timestamp as a\n   * signal\n   */\n  @Test void assumesServerWithoutTimestampIsShared() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .addAnnotation(1472470996250000L, \"sr\", BACKEND)\n            .addAnnotation(1472470996350000L, \"ss\", BACKEND)\n            .build();\n\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"get\")\n            .kind(Kind.SERVER)\n            .shared(true)\n            .localEndpoint(BACKEND)\n            .timestamp(1472470996250000L)\n            .duration(100000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void clientAndServer_loopback() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996250000L, \"sr\", FRONTEND)\n            .addAnnotation(1472470996350000L, \"ss\", FRONTEND)\n            .addAnnotation(1472470996406000L, \"cr\", FRONTEND)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"get\");\n\n    Span clientV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.CLIENT)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .duration(207000L)\n            .build();\n\n    Span serverV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.SERVER)\n            .shared(true)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996250000L)\n            .duration(100000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(clientV2, serverV2);\n  }\n\n  @Test void oneway_loopback() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"get\")\n            .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n            .addAnnotation(1472470996250000L, \"sr\", FRONTEND)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"get\");\n\n    Span clientV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.CLIENT)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .build();\n\n    Span serverV2 =\n        newBuilder\n            .clone()\n            .kind(Kind.SERVER)\n            .shared(true)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996250000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(clientV2, serverV2);\n  }\n\n  @Test void producer() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"send\")\n            .addAnnotation(1472470996199000L, \"ms\", FRONTEND)\n            .build();\n\n    Span v2 =\n        Span.newBuilder()\n            .traceId(\"1\")\n            .parentId(\"2\")\n            .id(\"3\")\n            .name(\"send\")\n            .kind(Kind.PRODUCER)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** Fix a v1 reported half in new style and half in old style, ex via a bridge */\n  @Test void client_missingCs() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .kind(Kind.CLIENT)\n        .localEndpoint(FRONTEND)\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .addAnnotation(1472470996406000L, \"cs\", FRONTEND)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void server_missingSr() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(BACKEND)\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .name(\"get\")\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .addAnnotation(1472470996406000L, \"ss\", BACKEND)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /**\n   * Intentionally create service loopback endpoints as dependency linker can correct it later if\n   * incorrect, provided the server is instrumented.\n   */\n  @Test void redundantAddressAnnotations_client() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .kind(Kind.CLIENT)\n        .name(\"get\")\n        .localEndpoint(FRONTEND)\n        .remoteEndpoint(FRONTEND)\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"get\")\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n        .addAnnotation(1472470996406000L, \"cr\", FRONTEND)\n        .addBinaryAnnotation(\"ca\", FRONTEND)\n        .addBinaryAnnotation(\"sa\", FRONTEND)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /**\n   * On server spans, ignore service name on remote address binary annotation that appear loopback\n   * based on the service name. This could happen when finagle service labels are used incorrectly,\n   * which as common in early instrumentation.\n   *\n   * <p>This prevents an uncorrectable scenario which results in extra (loopback) links on server\n   * spans.\n   */\n  @Test void redundantServiceNameOnAddressAnnotations_server() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .kind(Kind.SERVER)\n        .name(\"get\")\n        .localEndpoint(FRONTEND)\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"get\")\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .addAnnotation(1472470996199000L, \"sr\", FRONTEND)\n        .addAnnotation(1472470996406000L, \"ss\", FRONTEND)\n        .addBinaryAnnotation(\"ca\", FRONTEND)\n        .addBinaryAnnotation(\"sa\", FRONTEND)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void redundantServiceNameOnAddressAnnotations_serverRetainsClientSocket() {\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .kind(Kind.SERVER)\n        .name(\"get\")\n        .localEndpoint(BACKEND)\n        .remoteEndpoint(FRONTEND.toBuilder().serviceName(null).build())\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .build();\n\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"get\")\n        .timestamp(1472470996199000L)\n        .duration(207000L)\n        .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n        .addAnnotation(1472470996406000L, \"ss\", BACKEND)\n        .addBinaryAnnotation(\"ca\", FRONTEND.toBuilder().serviceName(\"backend\").build())\n        .addBinaryAnnotation(\"sa\", BACKEND)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  /** shared v1 IDs for messaging spans isn't supported, but shouldn't break */\n  @Test void producerAndConsumer() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1L)\n            .parentId(2L)\n            .id(3L)\n            .name(\"whatev\")\n            .addAnnotation(1472470996199000L, \"ms\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"ws\", FRONTEND)\n            .addAnnotation(1472470996403000L, \"wr\", BACKEND)\n            .addAnnotation(1472470996406000L, \"mr\", BACKEND)\n            .addBinaryAnnotation(\"ma\", kafka)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"whatev\");\n\n    Span producer =\n        newBuilder\n            .clone()\n            .kind(Kind.PRODUCER)\n            .localEndpoint(FRONTEND)\n            .remoteEndpoint(kafka)\n            .timestamp(1472470996199000L)\n            .duration(1472470996238000L - 1472470996199000L)\n            .build();\n\n    Span consumer =\n        newBuilder\n            .clone()\n            .kind(Kind.CONSUMER)\n            .shared(true)\n            .localEndpoint(BACKEND)\n            .remoteEndpoint(kafka)\n            .timestamp(1472470996403000L)\n            .duration(1472470996406000L - 1472470996403000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(producer, consumer);\n  }\n\n  /** shared v1 IDs for messaging spans isn't supported, but shouldn't break */\n  @Test void producerAndConsumer_loopback_shared() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1)\n            .parentId(2)\n            .id(3)\n            .name(\"message\")\n            .addAnnotation(1472470996199000L, \"ms\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"ws\", FRONTEND)\n            .addAnnotation(1472470996403000L, \"wr\", FRONTEND)\n            .addAnnotation(1472470996406000L, \"mr\", FRONTEND)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"message\");\n\n    Span producer =\n        newBuilder\n            .clone()\n            .kind(Kind.PRODUCER)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996199000L)\n            .duration(1472470996238000L - 1472470996199000L)\n            .build();\n\n    Span consumer =\n        newBuilder\n            .clone()\n            .kind(Kind.CONSUMER)\n            .shared(true)\n            .localEndpoint(FRONTEND)\n            .timestamp(1472470996403000L)\n            .duration(1472470996406000L - 1472470996403000L)\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(producer, consumer);\n  }\n\n  @Test void onlyAddressAnnotations() {\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1)\n        .parentId(2)\n        .id(3)\n        .name(\"rpc\")\n        .addBinaryAnnotation(\"ca\", FRONTEND)\n        .addBinaryAnnotation(\"sa\", BACKEND)\n        .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").parentId(\"2\").id(\"3\").name(\"rpc\")\n      .localEndpoint(FRONTEND)\n      .remoteEndpoint(BACKEND)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void dataMissingEndpointGoesOnFirstSpan() {\n    V1Span v1 =\n        V1Span.newBuilder()\n            .traceId(1)\n            .id(2)\n            .name(\"missing\")\n            .addAnnotation(1472470996199000L, \"foo\", FRONTEND)\n            .addAnnotation(1472470996238000L, \"bar\", FRONTEND)\n            .addAnnotation(1472470996250000L, \"baz\", BACKEND)\n            .addAnnotation(1472470996350000L, \"qux\", BACKEND)\n            .addAnnotation(1472470996403000L, \"missing\", null)\n            .addBinaryAnnotation(\"foo\", \"bar\", FRONTEND)\n            .addBinaryAnnotation(\"baz\", \"qux\", BACKEND)\n            .addBinaryAnnotation(\"missing\", \"\", null)\n            .build();\n\n    Span.Builder newBuilder = Span.newBuilder().traceId(\"1\").id(\"2\").name(\"missing\");\n\n    Span first =\n        newBuilder\n            .clone()\n            .localEndpoint(FRONTEND)\n            .addAnnotation(1472470996199000L, \"foo\")\n            .addAnnotation(1472470996238000L, \"bar\")\n            .addAnnotation(1472470996403000L, \"missing\")\n            .putTag(\"foo\", \"bar\")\n            .putTag(\"missing\", \"\")\n            .build();\n\n    Span second =\n        newBuilder\n            .clone()\n            .localEndpoint(BACKEND)\n            .addAnnotation(1472470996250000L, \"baz\")\n            .addAnnotation(1472470996350000L, \"qux\")\n            .putTag(\"baz\", \"qux\")\n            .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(first, second);\n  }\n\n  /**\n   * This emulates a situation in mysql where the row representing a span has the client's timestamp\n   */\n  @Test void parsesSharedFlagFromRPCSpan() {\n    V1Span v1 =\n      V1Span.newBuilder()\n        .traceId(1L)\n        .parentId(2L)\n        .id(3L)\n        .name(\"get\")\n        .timestamp(10)\n        .addAnnotation(20, \"sr\", BACKEND)\n        .addAnnotation(30, \"ss\", BACKEND)\n        .build();\n\n    Span v2 =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .parentId(\"2\")\n        .id(\"3\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .shared(true)\n        .localEndpoint(BACKEND)\n        .timestamp(20)\n        .duration(10L)\n        .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/v1/V1SpanConverterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.FRONTEND;\n\nclass V1SpanConverterTest {\n  Endpoint kafka = Endpoint.newBuilder().serviceName(\"kafka\").build();\n  V1SpanConverter v1SpanConverter = new V1SpanConverter();\n\n  @Test void convert_ma() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"mr\", BACKEND)\n      .addBinaryAnnotation(\"ma\", kafka)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.CONSUMER)\n      .timestamp(1472470996199000L)\n      .localEndpoint(BACKEND)\n      .remoteEndpoint(kafka)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void convert_sa() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n      .addBinaryAnnotation(\"sa\", BACKEND)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.CLIENT)\n      .timestamp(1472470996199000L)\n      .localEndpoint(FRONTEND)\n      .remoteEndpoint(BACKEND)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void convert_ca() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n      .addBinaryAnnotation(\"ca\", FRONTEND)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.SERVER)\n      .timestamp(1472470996199000L)\n      .localEndpoint(BACKEND)\n      .remoteEndpoint(FRONTEND)\n      .shared(true)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  // Following 3 tests show leniency for old versions of zipkin-ruby which serialized address binary\n  // annotations as \"1\" instead of true\n  @Test void convert_ma_incorrect_value() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"mr\", BACKEND)\n      .addBinaryAnnotation(\"ma\", \"1\", kafka)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.CONSUMER)\n      .timestamp(1472470996199000L)\n      .localEndpoint(BACKEND)\n      .remoteEndpoint(kafka)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void convert_sa_incorrect_value() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"cs\", FRONTEND)\n      .addBinaryAnnotation(\"sa\", \"1\", BACKEND)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.CLIENT)\n      .timestamp(1472470996199000L)\n      .localEndpoint(FRONTEND)\n      .remoteEndpoint(BACKEND)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n\n  @Test void convert_ca_incorrect_value() {\n    V1Span v1 = V1Span.newBuilder()\n      .traceId(1L)\n      .id(2L)\n      .addAnnotation(1472470996199000L, \"sr\", BACKEND)\n      .addBinaryAnnotation(\"ca\", \"1\", FRONTEND)\n      .build();\n\n    Span v2 = Span.newBuilder().traceId(\"1\").id(\"2\")\n      .kind(Kind.SERVER)\n      .timestamp(1472470996199000L)\n      .localEndpoint(BACKEND)\n      .remoteEndpoint(FRONTEND)\n      .shared(true)\n      .build();\n\n    assertThat(v1SpanConverter.convert(v1)).containsExactly(v2);\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/java/zipkin2/v1/V1SpanTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.v1;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass V1SpanTest {\n  V1Span.Builder builder = V1Span.newBuilder().traceId(\"1\").id(\"1\");\n\n  @Test void annotationEndpoint_emptyToNull() {\n    assertThat(builder.addAnnotation(1, \"foo\", Endpoint.newBuilder().build()).annotations)\n      .extracting(V1Annotation::endpoint)\n      .containsOnlyNulls();\n  }\n\n  @Test void binaryAnnotationEndpoint_emptyToNull() {\n    assertThat(\n      builder.addBinaryAnnotation(\"foo\", \"bar\", Endpoint.newBuilder().build()).binaryAnnotations)\n      .extracting(V1BinaryAnnotation::endpoint)\n      .containsOnlyNulls();\n  }\n\n  @Test void binaryAnnotationEndpoint_ignoresEmptyAddress() {\n    assertThat(\n      builder.addBinaryAnnotation(\"ca\", Endpoint.newBuilder().build()).binaryAnnotations)\n      .isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin/src/test/resources/log4j2.properties",
    "content": "appenders=console\nappender.console.type=Console\nappender.console.name=STDOUT\nappender.console.layout.type=PatternLayout\nappender.console.layout.pattern=%d{ABSOLUTE} %-5p [%t] %C{2} (%F:%L) - %m%n\nrootLogger.level=warn\nrootLogger.appenderRefs=stdout\nrootLogger.appenderRef.stdout.ref=STDOUT\n"
  },
  {
    "path": "zipkin-collector/README.md",
    "content": "# zipkin-collector\n\nModules here implement popular transport options available by default in\nthe [server build](../zipkin-server).\n\nPlease note all modules here require JRE 17+\n\nThese libraries are also usable outside the server, for example in\ncustom collectors or storage pipelines. While compatibility guarantees\nare strong, choices may be dropped over time.\n\nCollector modules ending in `-v1` are discouraged for new sites as they\nuse an older data model. At some point in the future, we will stop\npublishing v1 collector options.\n"
  },
  {
    "path": "zipkin-collector/activemq/RATIONALE.md",
    "content": "# Rational for collector-activemq\n\n## Diverse need\nActiveMQ was formerly requested in April, 2018 through issue #1990 which had two other thumbs-up. An\nearly draft of this implementation was developed by @IAMTJW and resulting in another user asking for\nit. In June of 2019 there were a couple more requests for this on gitter, notably about Amazon MQ.\n\n## On ActiveMQ 5.x\nAll users who expressed interest were interestd in ActiveMQ 5.x (aka Classic), not Artemis.\nMoreover, at the time of creation Amazon MQ only supported ActiveMQ 5.x.\n\nArtemis has higher throughput potential, but has more conflicting dependencies and would add 8MiB to\nthe server. Moreover, no-one has asked for it.\n\n## On part of the default server\nActiveMQ's client is 2MiB, which will increase the jar size, something that we've been tracking\nrecently. To be fair, this is not a large module. In comparison, one dependency of Kafka, `zstd-jni`\nalone is almost 4MiB. There are no dependencies likely to conflict at runtime, and only one dodgy\ndependency, [hawtbuf](https://github.com/fusesource/hawtbuf), on account of it being abandoned since\n2014.\n\nApart from size, ActiveMQ is a stable integration, included in Spring Boot, and could be useful for\nother integrations as an in-memory queue. Moreover, bundling makes integration with zipkin-aws far\neasier in the same way as bundling elasticsearch does.\n\n## On a potential single-transport client\n\nThis package is using the normal activemq-jms client. During a [mail thread](https://marc.info/?l=activemq-users&m=155356007513108), we learned the\nthe STOMP and AMQP 1.0 protocol are the more portable options for a portable integration as\nActiveMQ, Artemis and RabbitMQ all support these. On the other hand Kafka does not support these\nprotocols. Any future portability work could be limited by this. Meanwhile, using the standard JMS\nclient will make troubleshooting most natural to end users.\n"
  },
  {
    "path": "zipkin-collector/activemq/README.md",
    "content": "# collector-activemq\n\n## ActiveMQCollector\nThis collector consumes an ActiveMQ 5.x queue for messages that contain a list of spans. Underneath\nthis uses the ActiveMQ 5.x JMS client, which has two notable dependencies `slf4j-api` and `hawtbuf`.\n\nThe message's binary data includes a list of spans. Supported encodings\nare the same as the http [POST /spans](https://zipkin.io/zipkin-api/#/paths/%252Fspans) body.\n\n### Json\nThe message's binary data is a list of spans in json. The first character must be '[' (decimal 91).\n\n`Codec.JSON.writeSpans(spans)` performs the correct json encoding.\n\nHere's an example, sending a list of a single span to the zipkin queue:\n\n```bash\n$ curl -u admin:admin -X POST -s localhost:8161/api/message/zipkin?type=queue \\\n    -H \"Content-Type: application/json\" \\\n    -d '[{\"traceId\":\"1\",\"name\":\"bang\",\"id\":\"2\",\"timestamp\":1470150004071068,\"duration\":1,\"localEndpoint\":{\"serviceName\":\"flintstones\"},\"tags\":{\"lc\":\"bamm-bamm\"}}]'\n```\n"
  },
  {
    "path": "zipkin-collector/activemq/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector-activemq</artifactId>\n  <name>Collector: ActiveMQ</name>\n  <description>Zipkin span collector for ActiveMQ transport</description>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n    <activemq.version>5.18.5</activemq.version>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.apache.activemq</groupId>\n      <artifactId>activemq-client</artifactId>\n      <version>${activemq.version}</version>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/activemq/src/main/java/zipkin2/collector/activemq/ActiveMQCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport javax.jms.JMSException;\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorComponent;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.StorageComponent;\n\n/** This collector consumes encoded binary messages from a ActiveMQ queue. */\npublic final class ActiveMQCollector extends CollectorComponent {\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /** Configuration including defaults needed to consume spans from a ActiveMQ queue. */\n  public static final class Builder extends CollectorComponent.Builder {\n    Collector.Builder delegate = Collector.newBuilder(ActiveMQCollector.class);\n    CollectorMetrics metrics = CollectorMetrics.NOOP_METRICS;\n    ActiveMQConnectionFactory connectionFactory;\n    String queue = \"zipkin\";\n    int concurrency = 1;\n\n    @Override public Builder storage(StorageComponent storage) {\n      this.delegate.storage(storage);\n      return this;\n    }\n\n    @Override public Builder sampler(CollectorSampler sampler) {\n      this.delegate.sampler(sampler);\n      return this;\n    }\n\n    @Override public Builder metrics(CollectorMetrics metrics) {\n      if (metrics == null) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics.forTransport(\"activemq\");\n      this.delegate.metrics(this.metrics);\n      return this;\n    }\n\n    public Builder connectionFactory(ActiveMQConnectionFactory connectionFactory) {\n      if (connectionFactory == null) throw new NullPointerException(\"connectionFactory == null\");\n      this.connectionFactory = connectionFactory;\n      return this;\n    }\n\n    /** Queue zipkin spans will be consumed from. Defaults to \"zipkin\". */\n    public Builder queue(String queue) {\n      if (queue == null) throw new NullPointerException(\"queue == null\");\n      this.queue = queue;\n      return this;\n    }\n\n    /** Count of concurrent message listeners on the queue. Defaults to 1 */\n    public Builder concurrency(int concurrency) {\n      if (concurrency < 1) throw new IllegalArgumentException(\"concurrency < 1\");\n      this.concurrency = concurrency;\n      return this;\n    }\n\n    @Override public ActiveMQCollector build() {\n      if (connectionFactory == null) throw new NullPointerException(\"connectionFactory == null\");\n      return new ActiveMQCollector(this);\n    }\n  }\n\n  final String queue;\n  final LazyInit lazyInit;\n\n  ActiveMQCollector(Builder builder) {\n    this.queue = builder.queue;\n    this.lazyInit = new LazyInit(builder);\n  }\n\n  @Override public ActiveMQCollector start() {\n    lazyInit.init();\n    return this;\n  }\n\n  @Override public CheckResult check() {\n    if (lazyInit.result == null) {\n      return CheckResult.failed(new IllegalStateException(\"Collector not yet started\"));\n    }\n    return lazyInit.result.checkResult;\n  }\n\n  @Override public void close() throws IOException {\n    lazyInit.close();\n  }\n\n  @Override public final String toString() {\n    return \"ActiveMQCollector{\"\n      + \"brokerURL=\" + lazyInit.connectionFactory.getBrokerURL()\n      + \", queue=\" + lazyInit.queue\n      + \"}\";\n  }\n\n  static RuntimeException uncheckedException(String prefix, JMSException e) {\n    Exception cause = e.getLinkedException();\n    if (cause instanceof IOException exception) {\n      return new UncheckedIOException(prefix + message(cause), exception);\n    }\n    return new RuntimeException(prefix + message(e), e);\n  }\n\n  static String message(Exception cause) {\n    return cause.getMessage() != null ? cause.getMessage() : cause.getClass().getSimpleName();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/activemq/src/main/java/zipkin2/collector/activemq/ActiveMQSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport javax.jms.BytesMessage;\nimport javax.jms.JMSException;\nimport javax.jms.Message;\nimport javax.jms.MessageListener;\nimport javax.jms.Queue;\nimport javax.jms.QueueReceiver;\nimport javax.jms.QueueSession;\nimport javax.jms.Session;\nimport javax.jms.TextMessage;\nimport org.apache.activemq.ActiveMQConnection;\nimport org.apache.activemq.transport.TransportListener;\nimport zipkin2.Callback;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * Consumes spans from messages on a ActiveMQ queue. Malformed messages will be discarded. Errors in\n * the storage component will similarly be ignored, with no retry of the message.\n */\nfinal class ActiveMQSpanConsumer implements TransportListener, MessageListener, Closeable {\n  static final Callback<Void> NOOP = new Callback<Void>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n    }\n  };\n\n  static final CheckResult\n    CLOSED = CheckResult.failed(new IllegalStateException(\"Collector intentionally closed\")),\n    INTERRUPTION = CheckResult.failed(new IOException(\"Recoverable error on ActiveMQ connection\"));\n\n  final Collector collector;\n  final CollectorMetrics metrics;\n\n  final ActiveMQConnection connection;\n  final Map<QueueSession, QueueReceiver> sessionToReceiver = new LinkedHashMap<>();\n\n  volatile CheckResult checkResult = CheckResult.OK;\n\n  ActiveMQSpanConsumer(Collector collector, CollectorMetrics metrics, ActiveMQConnection conn) {\n    this.collector = collector;\n    this.metrics = metrics;\n    this.connection = conn;\n    connection.addTransportListener(this);\n  }\n\n  /** JMS contract is one session per thread: we need a new session up to our concurrency level. */\n  void registerInNewSession(ActiveMQConnection connection, String queue) throws JMSException {\n    // Pass redundant info as we can't use default method in activeMQ\n    QueueSession session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);\n    // No need to do anything on ActiveMQ side as physical queues are created on demand\n    Queue destination = session.createQueue(queue);\n    QueueReceiver receiver = session.createReceiver(destination);\n    receiver.setMessageListener(this);\n    sessionToReceiver.put(session, receiver);\n  }\n\n  @Override public void onCommand(Object o) {\n  }\n\n  @Override public void onException(IOException error) {\n    checkResult = CheckResult.failed(error);\n  }\n\n  @Override public void transportInterupted() {\n    checkResult = INTERRUPTION;\n  }\n\n  @Override public void transportResumed() {\n    checkResult = CheckResult.OK;\n  }\n\n  @Override public void onMessage(Message message) {\n    metrics.incrementMessages();\n    byte[] serialized; // TODO: consider how to reuse buffers here\n    try {\n      if (message instanceof BytesMessage bytesMessage) {\n        serialized = new byte[(int) bytesMessage.getBodyLength()];\n        bytesMessage.readBytes(serialized);\n      } else if (message instanceof TextMessage textMessage) {\n        String text = textMessage.getText();\n        serialized = text.getBytes(UTF_8);\n      } else {\n        metrics.incrementMessagesDropped();\n        return;\n      }\n    } catch (Exception e) {\n      metrics.incrementMessagesDropped();\n      return;\n    }\n\n    metrics.incrementBytes(serialized.length);\n    if (serialized.length == 0) return; // lenient on empty messages\n    collector.acceptSpans(serialized, NOOP);\n  }\n\n  @Override public void close() {\n    if (checkResult == CLOSED) return;\n    checkResult = CLOSED;\n    connection.removeTransportListener(this);\n    try {\n      for (Map.Entry<QueueSession, QueueReceiver> sessionReceiver : sessionToReceiver.entrySet()) {\n        sessionReceiver.getValue().setMessageListener(null); // deregister this\n        sessionReceiver.getKey().close();\n      }\n      connection.close();\n    } catch (JMSException ignored) {\n      // EmptyCatch ignored\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/activemq/src/main/java/zipkin2/collector/activemq/LazyInit.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport javax.jms.JMSException;\nimport org.apache.activemq.ActiveMQConnection;\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\n\nimport static zipkin2.collector.activemq.ActiveMQCollector.uncheckedException;\n\n/**\n * Lazy creates a connection and registers a message listener up to the specified concurrency level.\n * This listener will also receive health notifications.\n */\nfinal class LazyInit {\n  final Collector collector;\n  final CollectorMetrics metrics;\n  final ActiveMQConnectionFactory connectionFactory;\n  final String queue;\n  final int concurrency;\n\n  volatile ActiveMQSpanConsumer result;\n\n  LazyInit(ActiveMQCollector.Builder builder) {\n    collector = builder.delegate.build();\n    metrics = builder.metrics;\n    connectionFactory = builder.connectionFactory;\n    queue = builder.queue;\n    concurrency = builder.concurrency;\n  }\n\n  ActiveMQSpanConsumer init() {\n    if (result == null) {\n      synchronized (this) {\n        if (result == null) {\n          result = doInit();\n        }\n      }\n    }\n    return result;\n  }\n\n  void close() {\n    ActiveMQSpanConsumer maybe = result;\n    if (maybe != null) result.close();\n  }\n\n  ActiveMQSpanConsumer doInit() {\n    final ActiveMQConnection connection;\n    try {\n      connection = (ActiveMQConnection) connectionFactory.createQueueConnection();\n      connection.start();\n    } catch (JMSException e) {\n      throw uncheckedException(\"Unable to establish connection to ActiveMQ broker: \", e);\n    }\n\n    try {\n      ActiveMQSpanConsumer result = new ActiveMQSpanConsumer(collector, metrics, connection);\n\n      for (int i = 0; i < concurrency; i++) {\n        result.registerInNewSession(connection, queue);\n      }\n\n      return result;\n    } catch (JMSException e) {\n      try {\n        connection.close();\n      } catch (JMSException ignored) {\n        // EmptyCatch ignored\n      }\n      throw uncheckedException(\"Unable to create queueReceiver(\" + queue + \"): \", e);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/activemq/src/test/java/zipkin2/collector/activemq/ActiveMQExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport java.time.Duration;\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\nclass ActiveMQExtension implements BeforeAllCallback, AfterAllCallback {\n  static final Logger LOGGER = LoggerFactory.getLogger(ActiveMQExtension.class);\n  static final int ACTIVEMQ_PORT = 61616;\n\n  ActiveMQContainer container = new ActiveMQContainer();\n\n  @Override public void beforeAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using brokerURL \" + brokerURL());\n  }\n\n  @Override public void afterAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.stop();\n  }\n\n  ActiveMQCollector.Builder newCollectorBuilder(String queue) {\n    ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();\n    connectionFactory.setBrokerURL(brokerURL());\n    return ActiveMQCollector.builder().queue(queue).connectionFactory(connectionFactory);\n  }\n\n  String brokerURL() {\n    return \"failover:tcp://\" + container.getHost() + \":\" + container.getMappedPort(ACTIVEMQ_PORT);\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class ActiveMQContainer extends GenericContainer<ActiveMQContainer> {\n    ActiveMQContainer() {\n      super(parse(\"ghcr.io/openzipkin/zipkin-activemq:3.4.3\"));\n      withExposedPorts(ACTIVEMQ_PORT);\n      waitStrategy = Wait.forListeningPorts(ACTIVEMQ_PORT);\n      withStartupTimeout(Duration.ofSeconds(60));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/activemq/src/test/java/zipkin2/collector/activemq/ITActiveMQCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.lang.reflect.Method;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Optional;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport javax.jms.BytesMessage;\nimport javax.jms.Connection;\nimport javax.jms.Queue;\nimport javax.jms.QueueReceiver;\nimport javax.jms.QueueSender;\nimport javax.jms.QueueSession;\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.TestObjects.UTF_8;\nimport static zipkin2.codec.SpanBytesEncoder.PROTO3;\nimport static zipkin2.codec.SpanBytesEncoder.THRIFT;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Timeout(60)\n@Tag(\"docker\")\nclass ITActiveMQCollector {\n  @RegisterExtension static ActiveMQExtension activemq = new ActiveMQExtension();\n  List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1]);\n\n  public String testName;\n\n  InMemoryCollectorMetrics metrics = new InMemoryCollectorMetrics();\n  InMemoryCollectorMetrics activemqMetrics = metrics.forTransport(\"activemq\");\n\n  CopyOnWriteArraySet<Thread> threadsProvidingSpans = new CopyOnWriteArraySet<>();\n  LinkedBlockingQueue<List<Span>> receivedSpans = new LinkedBlockingQueue<>();\n  SpanConsumer consumer;\n  ActiveMQCollector collector;\n\n  @BeforeEach void start(TestInfo testInfo) {\n    Optional<Method> testMethod = testInfo.getTestMethod();\n    if (testMethod.isPresent()) {\n      this.testName = testMethod.get().getName();\n    }\n    threadsProvidingSpans.clear();\n    receivedSpans.clear();\n    consumer = (spans) -> {\n      threadsProvidingSpans.add(Thread.currentThread());\n      receivedSpans.add(spans);\n      return Call.create(null);\n    };\n    activemqMetrics.clear();\n    collector = builder().build().start();\n  }\n\n  @AfterEach void stop() throws IOException {\n    collector.close();\n  }\n\n  @Test void checkPasses() {\n    assertThat(collector.check().ok()).isTrue();\n  }\n\n  @Test void startFailsWithInvalidActiveMqServer() {\n    Throwable exception = assertThrows(UncheckedIOException.class, () -> {\n      collector.close();\n\n      ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();\n      // we can be pretty certain ActiveMQ isn't running on localhost port 80\n      connectionFactory.setBrokerURL(\"tcp://localhost:80\");\n      collector = builder().connectionFactory(connectionFactory).build();\n      collector.start();\n    });\n    assertThat(exception.getMessage()).contains(\"Unable to establish connection to ActiveMQ broker: Connection refused\");\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    assertThat(collector).hasToString(\n      \"ActiveMQCollector{brokerURL=%s, queue=%s}\".formatted(activemq.brokerURL(), testName));\n  }\n\n  /** Ensures list encoding works: a json encoded list of spans */\n  @Test void messageWithMultipleSpans_json() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.JSON_V1);\n  }\n\n  /** Ensures list encoding works: a version 2 json list of spans */\n  @Test void messageWithMultipleSpans_json2() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.JSON_V2);\n  }\n\n  /** Ensures list encoding works: proto3 ListOfSpans */\n  @Test void messageWithMultipleSpans_proto3() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.PROTO3);\n  }\n\n  void messageWithMultipleSpans(SpanBytesEncoder encoder) throws Exception {\n    byte[] message = encoder.encodeList(spans);\n    pushMessage(collector.queue, message);\n\n    assertThat(receivedSpans.take()).isEqualTo(spans);\n\n    assertThat(activemqMetrics.messages()).isEqualTo(1);\n    assertThat(activemqMetrics.messagesDropped()).isZero();\n    assertThat(activemqMetrics.bytes()).isEqualTo(message.length);\n    assertThat(activemqMetrics.spans()).isEqualTo(spans.size());\n    assertThat(activemqMetrics.spansDropped()).isZero();\n  }\n\n  /** Ensures malformed spans don't hang the collector */\n  @Test void skipsMalformedData() throws Exception {\n    byte[] malformed1 = \"[\\\"='\".getBytes(UTF_8); // screwed up json\n    byte[] malformed2 = \"malformed\".getBytes(UTF_8);\n    pushMessage(collector.queue, THRIFT.encodeList(spans));\n    pushMessage(collector.queue, new byte[0]);\n    pushMessage(collector.queue, malformed1);\n    pushMessage(collector.queue, malformed2);\n    pushMessage(collector.queue, THRIFT.encodeList(spans));\n\n    Thread.sleep(1000);\n\n    assertThat(activemqMetrics.messages()).isEqualTo(5);\n    assertThat(activemqMetrics.messagesDropped()).isEqualTo(2); // only malformed, not empty\n    assertThat(activemqMetrics.bytes()).isEqualTo(\n      THRIFT.encodeList(spans).length * 2 + malformed1.length + malformed2.length);\n    assertThat(activemqMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(activemqMetrics.spansDropped()).isZero();\n  }\n\n  /** Guards against errors that leak from storage, such as InvalidQueryException */\n  @Test void skipsOnSpanStorageException() throws Exception {\n    collector.close();\n\n    AtomicInteger counter = new AtomicInteger();\n    consumer = (input) -> new Call.Base<Void>() {\n      @Override protected Void doExecute() {\n        throw new AssertionError();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        if (counter.getAndIncrement() == 1) {\n          callback.onError(new RuntimeException(\"storage fell over\"));\n        } else {\n          receivedSpans.add(spans);\n          callback.onSuccess(null);\n        }\n      }\n\n      @Override public Call<Void> clone() {\n        throw new AssertionError();\n      }\n    };\n\n    collector = builder().storage(buildStorage(consumer)).build().start();\n\n    pushMessage(collector.queue, PROTO3.encodeList(spans));\n    pushMessage(collector.queue, PROTO3.encodeList(spans)); // tossed on error\n    pushMessage(collector.queue, PROTO3.encodeList(spans));\n\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    // the only way we could read this, is if the malformed span was skipped.\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n\n    assertThat(activemqMetrics.messages()).isEqualTo(3);\n    assertThat(activemqMetrics.messagesDropped()).isZero(); // storage failure not message failure\n    assertThat(activemqMetrics.bytes()).isEqualTo(PROTO3.encodeList(spans).length * 3);\n    assertThat(activemqMetrics.spans()).isEqualTo(spans.size() * 3);\n    assertThat(activemqMetrics.spansDropped()).isEqualTo(spans.size()); // only one dropped\n  }\n\n  @Test void messagesDistributedAcrossMultipleThreadsSuccessfully() throws Exception {\n    collector.close();\n\n    CountDownLatch latch = new CountDownLatch(2);\n    collector = builder().concurrency(2).storage(buildStorage((spans) -> {\n      latch.countDown();\n      try {\n        latch.await(); // await the other one as this proves 2 threads are in use\n      } catch (InterruptedException e) {\n        throw new AssertionError(e);\n      }\n      return consumer.accept(spans);\n    })).build().start();\n\n    pushMessage(collector.queue, new byte[] {}); // empty bodies don't go to storage\n    pushMessage(collector.queue, PROTO3.encodeList(spans));\n    pushMessage(collector.queue, PROTO3.encodeList(spans));\n\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    latch.countDown();\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n\n    assertThat(threadsProvidingSpans).hasSize(2);\n\n    assertThat(activemqMetrics.messages()).isEqualTo(3); // 2 + empty body for warmup\n    assertThat(activemqMetrics.messagesDropped()).isZero();\n    assertThat(activemqMetrics.bytes()).isEqualTo(PROTO3.encodeList(spans).length * 2);\n    assertThat(activemqMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(activemqMetrics.spansDropped()).isZero();\n  }\n\n  ActiveMQCollector.Builder builder() {\n    // prevent test flakes by having each run in an individual queue\n    return activemq.newCollectorBuilder(testName)\n      .storage(buildStorage(consumer))\n      .metrics(metrics)\n      .queue(testName);\n  }\n\n  static StorageComponent buildStorage(final SpanConsumer spanConsumer) {\n    return new ForwardingStorageComponent() {\n      @Override protected StorageComponent delegate() {\n        throw new AssertionError();\n      }\n\n      @Override public SpanConsumer spanConsumer() {\n        return spanConsumer;\n      }\n    };\n  }\n\n  void pushMessage(String queueName, byte[] message) throws Exception {\n    ActiveMQSpanConsumer consumer = collector.lazyInit.result;\n\n    // Look up the existing session for this queue, so that there is no chance of flakes.\n    QueueSession session = null;\n    Queue queue = null;\n    for (Map.Entry<QueueSession, QueueReceiver> entry : consumer.sessionToReceiver.entrySet()) {\n      if (entry.getValue().getQueue().getQueueName().equals(queueName)) {\n        session = entry.getKey();\n        queue = entry.getValue().getQueue();\n        break;\n      }\n    }\n    if (session == null) {\n      throw new NoSuchElementException(\"couldn't find session for queue \" + queueName);\n    }\n\n    Connection conn = collector.lazyInit.result.connection;\n\n    try (QueueSender sender = session.createSender(queue)) {\n      BytesMessage bytesMessage = session.createBytesMessage();\n      bytesMessage.writeBytes(message);\n      sender.send(bytesMessage);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/activemq/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\norg.slf4j.simpleLogger.log.zipkin2.collector.activemq=debug\n"
  },
  {
    "path": "zipkin-collector/core/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector</artifactId>\n  <name>Collector: Core Library</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-api</artifactId>\n      <version>${slf4j.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.github.valfirst</groupId>\n      <artifactId>slf4j-test</artifactId>\n      <version>3.0.3</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <plugins>\n      <plugin>\n        <artifactId>maven-surefire-plugin</artifactId>\n        <configuration>\n          <classpathDependencyExcludes>\n            <classpathDependencyExcludes>org.slf4j:slf4j-simple</classpathDependencyExcludes>\n          </classpathDependencyExcludes>\n        </configuration>\n      </plugin>\n    </plugins>\n  </build>\n</project>\n"
  },
  {
    "path": "zipkin-collector/core/src/main/java/zipkin2/collector/Collector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.Executor;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.SpanBytesDecoderDetector;\nimport zipkin2.codec.BytesDecoder;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.storage.StorageComponent;\n\nimport static zipkin2.Call.propagateIfFatal;\n\n/**\n * This component takes action on spans received from a transport. This includes deserializing,\n * sampling and scheduling for storage.\n *\n * <p>Callbacks passed do not propagate to the storage layer. They only return success or failures\n * before storage is attempted. This ensures that calling threads are disconnected from storage\n * threads.\n */\npublic class Collector { // not final for mock\n  static final Callback<Void> NOOP_CALLBACK = new Callback<Void>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n    }\n  };\n\n  /** Needed to scope this to the correct logging category */\n  public static Builder newBuilder(Class<?> loggingClass) {\n    if (loggingClass == null) throw new NullPointerException(\"loggingClass == null\");\n    return new Builder(LoggerFactory.getLogger(loggingClass.getName()));\n  }\n\n  public static final class Builder {\n    final Logger logger;\n    StorageComponent storage;\n    CollectorSampler sampler;\n    CollectorMetrics metrics;\n\n    Builder(Logger logger) {\n      this.logger = logger;\n    }\n\n    /** Sets {@link {@link CollectorComponent.Builder#storage(StorageComponent)}} */\n    public Builder storage(StorageComponent storage) {\n      if (storage == null) throw new NullPointerException(\"storage == null\");\n      this.storage = storage;\n      return this;\n    }\n\n    /** Sets {@link {@link CollectorComponent.Builder#metrics(CollectorMetrics)}} */\n    public Builder metrics(CollectorMetrics metrics) {\n      if (metrics == null) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics;\n      return this;\n    }\n\n    /** Sets {@link {@link CollectorComponent.Builder#sampler(CollectorSampler)}} */\n    public Builder sampler(CollectorSampler sampler) {\n      if (sampler == null) throw new NullPointerException(\"sampler == null\");\n      this.sampler = sampler;\n      return this;\n    }\n\n    public Collector build() {\n      return new Collector(this);\n    }\n  }\n\n  final Logger logger;\n  final CollectorMetrics metrics;\n  final CollectorSampler sampler;\n  final StorageComponent storage;\n\n  Collector(Builder builder) {\n    if (builder.logger == null) throw new NullPointerException(\"logger == null\");\n    this.logger = builder.logger;\n    this.metrics = builder.metrics == null ? CollectorMetrics.NOOP_METRICS : builder.metrics;\n    if (builder.storage == null) throw new NullPointerException(\"storage == null\");\n    this.storage = builder.storage;\n    this.sampler = builder.sampler == null ? CollectorSampler.ALWAYS_SAMPLE : builder.sampler;\n  }\n\n  public void accept(List<Span> spans, Callback<Void> callback) {\n    accept(spans, callback, Runnable::run);\n  }\n\n  /**\n   * Calls to get the storage component could be blocking. This ensures requests that block\n   * callers (such as http or gRPC) do not add additional load during such events.\n   *\n   * @param executor the executor used to enqueue the storage request.\n   */\n  public void accept(List<Span> spans, Callback<Void> callback, Executor executor) {\n    if (spans.isEmpty()) {\n      callback.onSuccess(null);\n      return;\n    }\n    metrics.incrementSpans(spans.size());\n\n    List<Span> sampledSpans = sample(spans);\n    if (sampledSpans.isEmpty()) {\n      callback.onSuccess(null);\n      return;\n    }\n\n    // In order to ensure callers are not blocked, we swap callbacks when we get to the storage\n    // phase of this process. Here, we create a callback whose sole purpose is classifying later\n    // errors on this bundle of spans in the same log category. This allows people to only turn on\n    // debug logging in one place.\n    try {\n      executor.execute(new StoreSpans(sampledSpans));\n      callback.onSuccess(null);\n    } catch (Throwable unexpected) { // ensure if a future is supplied we always set value or error\n      callback.onError(unexpected);\n      throw unexpected;\n    }\n  }\n\n  /** Like {@link #acceptSpans(byte[], BytesDecoder, Callback)}, except using a byte buffer. */\n  public void acceptSpans(ByteBuffer encoded, SpanBytesDecoder decoder, Callback<Void> callback,\n    Executor executor) {\n    List<Span> spans;\n    try {\n      spans = decoder.decodeList(encoded);\n    } catch (RuntimeException | Error e) {\n      handleDecodeError(e, callback);\n      return;\n    }\n    accept(spans, callback, executor);\n  }\n\n  /**\n   * Before calling this, call {@link CollectorMetrics#incrementMessages()}, and {@link\n   * CollectorMetrics#incrementBytes(int)}. Do not call any other metrics callbacks as those are\n   * handled internal to this method.\n   *\n   * @param serialized not empty message\n   */\n  public void acceptSpans(byte[] serialized, Callback<Void> callback) {\n    BytesDecoder<Span> decoder;\n    try {\n      decoder = SpanBytesDecoderDetector.decoderForListMessage(serialized);\n    } catch (RuntimeException | Error e) {\n      handleDecodeError(e, callback);\n      return;\n    }\n    acceptSpans(serialized, decoder, callback);\n  }\n\n  /**\n   * Before calling this, call {@link CollectorMetrics#incrementMessages()}, and {@link\n   * CollectorMetrics#incrementBytes(int)}. Do not call any other metrics callbacks as those are\n   * handled internal to this method.\n   *\n   * @param serializedSpans not empty message\n   */\n  public void acceptSpans(\n    byte[] serializedSpans, BytesDecoder<Span> decoder, Callback<Void> callback) {\n    List<Span> spans;\n    try {\n      spans = decodeList(decoder, serializedSpans);\n    } catch (RuntimeException | Error e) {\n      handleDecodeError(e, callback);\n      return;\n    }\n    accept(spans, callback);\n  }\n\n  List<Span> decodeList(BytesDecoder<Span> decoder, byte[] serialized) {\n    List<Span> out = new ArrayList<>();\n    decoder.decodeList(serialized, out);\n    return out;\n  }\n\n  void store(List<Span> sampledSpans, Callback<Void> callback) {\n    storage.spanConsumer().accept(sampledSpans).enqueue(callback);\n  }\n\n  String idString(Span span) {\n    return span.traceId() + \"/\" + span.id();\n  }\n\n  List<Span> sample(List<Span> input) {\n    List<Span> sampled = new ArrayList<>(input.size());\n    for (int i = 0, length = input.size(); i < length; i++) {\n      Span s = input.get(i);\n      if (sampler.isSampled(s.traceId(), Boolean.TRUE.equals(s.debug()))) {\n        sampled.add(s);\n      }\n    }\n    int dropped = input.size() - sampled.size();\n    if (dropped > 0) metrics.incrementSpansDropped(dropped);\n    return sampled;\n  }\n\n  class StoreSpans implements Callback<Void>, Runnable {\n    final List<Span> spans;\n\n    StoreSpans(List<Span> spans) {\n      this.spans = spans;\n    }\n\n    @Override public void run() {\n      try {\n        store(spans, this);\n      } catch (RuntimeException | Error e) {\n        // While unexpected, invoking the storage command could raise an error synchronously. When\n        // that's the case, we wouldn't have invoked callback.onSuccess, so we need to handle the\n        // error here.\n        onError(e);\n      }\n    }\n\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n      handleStorageError(spans, t, NOOP_CALLBACK);\n    }\n\n    @Override public String toString() {\n      return appendSpanIds(spans, new StringBuilder(\"StoreSpans(\")) + \")\";\n    }\n  }\n\n  void handleDecodeError(Throwable e, Callback<Void> callback) {\n    metrics.incrementMessagesDropped();\n    handleError(e, \"Cannot decode spans\"::toString, callback);\n  }\n\n  /**\n   * When storing spans, an exception can be raised before or after the fact. This adds context of\n   * span ids to give logs more relevance.\n   */\n  void handleStorageError(List<Span> spans, Throwable e, Callback<Void> callback) {\n    metrics.incrementSpansDropped(spans.size());\n    // The exception could be related to a span being huge. Instead of filling logs,\n    // print trace id, span id pairs\n    handleError(e, () -> appendSpanIds(spans, new StringBuilder(\"Cannot store spans \")), callback);\n  }\n\n  void handleError(Throwable e, Supplier<String> defaultLogMessage, Callback<Void> callback) {\n    propagateIfFatal(e);\n    callback.onError(e);\n    if (!logger.isDebugEnabled()) return;\n\n    String error = e.getMessage() != null ? e.getMessage() : \"\";\n    // We have specific code that customizes log messages. Use this when the case.\n    if (error.startsWith(\"Malformed\") || error.startsWith(\"Truncated\")) {\n      logger.debug(error, e);\n    } else { // otherwise, beautify the message\n      String message =\n        \"%s due to %s(%s)\".formatted(defaultLogMessage.get(), e.getClass().getSimpleName(), error);\n      logger.debug(message, e);\n    }\n  }\n\n  // TODO: this logic needs to be redone as service names are more important than span IDs. Also,\n  // span IDs repeat between client and server!\n  String appendSpanIds(List<Span> spans, StringBuilder message) {\n    message.append(\"[\");\n    int i = 0;\n    Iterator<Span> iterator = spans.iterator();\n    while (iterator.hasNext() && i++ < 3) {\n      message.append(idString(iterator.next()));\n      if (iterator.hasNext()) message.append(\", \");\n    }\n    if (iterator.hasNext()) message.append(\"...\");\n\n    return message.append(\"]\").toString();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/main/java/zipkin2/collector/CollectorComponent.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport java.util.List;\nimport zipkin2.Component;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\n/**\n * The collector represents the server-side of a transport. Its job is to take spans from a\n * transport and store ones it has sampled.\n *\n * <p>Call {@link #start()} to start collecting spans.\n */\npublic abstract class CollectorComponent extends Component {\n\n  /**\n   * Starts the server-side of the transport, typically listening or looking up a queue.\n   *\n   * <p>Many implementations block the calling thread until services are available.\n   */\n  public abstract CollectorComponent start();\n\n  public abstract static class Builder {\n    /**\n     * Once spans are sampled, they are {@link SpanConsumer#accept(List)} queued for storage} using\n     * this component.\n     */\n    public abstract Builder storage(StorageComponent storage);\n\n    /**\n     * Aggregates and reports collection metrics to a monitoring system. Should be {@link\n     * CollectorMetrics#forTransport(String) scoped to this transport}. Defaults to no-op.\n     */\n    public abstract Builder metrics(CollectorMetrics metrics);\n\n    /**\n     * {@link CollectorSampler#isSampled(String, boolean) samples spans} to reduce load on the\n     * storage system. Defaults to always sample.\n     */\n    public abstract Builder sampler(CollectorSampler sampler);\n\n    public abstract CollectorComponent build();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/main/java/zipkin2/collector/CollectorMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport java.util.Collection;\nimport java.util.List;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.storage.SpanConsumer;\n\n/**\n * Instrumented applications report spans over a transport such as Kafka. Zipkin collectors receive\n * these messages, {@link SpanBytesDecoder#decode(byte[], Collection) decoding them into spans},\n * {@link CollectorSampler#isSampled(String, boolean) apply sampling}, and {@link\n * SpanConsumer#accept(List) queues them for storage}.\n *\n * <p>Callbacks on this type are invoked by zipkin collectors to improve the visibility of the\n * system. A typical implementation will report metrics to a telemetry system for analysis and\n * reporting.\n *\n * <h3>Spans Collected vs Queryable Spans</h3>\n *\n * <p>A span queried may be comprised of multiple spans collected. While instrumentation should\n * report complete spans, Instrumentation often patch the same span twice, ex adding annotations.\n * Also, RPC spans include at least 2 messages due to the client and the server reporting\n * separately. Finally, some storage components merge patches at ingest. For these reasons, you\n * should be cautious to alert on queryable spans vs stored spans, unless you control the\n * instrumentation in such a way that queryable spans/message is reliable.\n *\n * <h3>Key Relationships</h3>\n *\n * <p>The following relationships can be used to consider health of the tracing system.\n *\n * <pre>\n * <ul>\n * <li>Successful Messages = {@link #incrementMessages() Accepted messages} -\n * {@link #incrementMessagesDropped() Dropped messages}. Alert when this is less than amount of\n * messages sent from instrumentation.</li>\n * <li>Stored spans &lt;= {@link #incrementSpans(int) Accepted spans} - {@link\n * #incrementSpansDropped(int) Dropped spans}. Alert when this drops below the\n * {@link CollectorSampler#isSampled(long, boolean) collection-tier sample rate}.\n * </li>\n * </ul>\n * </pre>\n */\npublic interface CollectorMetrics {\n\n  /**\n   * Those who wish to partition metrics by transport can call this method to include the transport\n   * type in the backend metric key.\n   *\n   * <p>For example, an implementation may by default report {@link #incrementSpans(int) incremented\n   * spans} to the key \"zipkin.collector.span.accepted\". When {@code metrics.forTransport(\"kafka\"}\n   * is called, the counter would report to \"zipkin.collector.kafka.span.accepted\"\n   *\n   * @param transportType ex \"http\", \"rabbitmq\", \"kafka\"\n   */\n  CollectorMetrics forTransport(String transportType);\n\n  /**\n   * Increments count of messages received, which contain 0 or more spans. Ex POST requests or Kafka\n   * messages consumed.\n   */\n  void incrementMessages();\n\n  /**\n   * Increments count of messages that could not be read. Ex malformed content, or peer disconnect.\n   */\n  void incrementMessagesDropped();\n\n  /**\n   * Increments the count of spans read from a successful message. When bundling is used, accepted\n   * spans will be a larger number than successful messages.\n   */\n  void incrementSpans(int quantity);\n\n  /**\n   * Increments the number of bytes containing serialized spans in a message.\n   *\n   * <p>Note: this count should relate to the raw data structures, like json or thrift, and discount\n   * compression, enveloping, etc.\n   */\n  void incrementBytes(int quantity);\n\n  /**\n   * Increments the count of spans dropped for any reason. For example, failure queueing to storage\n   * or sampling decisions.\n   */\n  void incrementSpansDropped(int quantity);\n\n  CollectorMetrics NOOP_METRICS =\n      new CollectorMetrics() {\n\n        @Override\n        public CollectorMetrics forTransport(String transportType) {\n          return this;\n        }\n\n        @Override\n        public void incrementMessages() {}\n\n        @Override\n        public void incrementMessagesDropped() {}\n\n        @Override\n        public void incrementSpans(int quantity) {}\n\n        @Override\n        public void incrementBytes(int quantity) {}\n\n        @Override\n        public void incrementSpansDropped(int quantity) {}\n\n        @Override\n        public String toString() {\n          return \"NoOpCollectorMetrics\";\n        }\n      };\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/main/java/zipkin2/collector/CollectorSampler.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport zipkin2.Span;\nimport zipkin2.internal.HexCodec;\n\n/**\n * CollectorSampler decides if a particular trace should be \"sampled\", i.e. recorded in permanent\n * storage. This involves a consistent decision based on the span's trace ID with one notable\n * exception: {@link Span#debug() Debug} spans are always stored.\n *\n * <h3>Implementation</h3>\n *\n * <p>Accepts a percentage of trace ids by comparing their absolute value against a potentially\n * dynamic boundary. eg {@code isSampled == abs(traceId) <= boundary}\n *\n * <p>While idempotent, this implementation's sample rate won't exactly match the input rate because\n * trace ids are not perfectly distributed across 64bits. For example, tests have shown an error\n * rate of 3% when 100K trace ids are {@link java.util.Random#nextLong random}.\n */\npublic abstract class CollectorSampler {\n  public static final CollectorSampler ALWAYS_SAMPLE = CollectorSampler.create(1.0f);\n\n  /**\n   * Returns a trace ID sampler with the indicated rate.\n   *\n   * @param rate minimum sample rate is 0.0001, or 0.01% of traces\n   */\n  public static CollectorSampler create(float rate) {\n    if (rate < 0 || rate > 1)\n      throw new IllegalArgumentException(\"rate should be between 0 and 1: was \" + rate);\n    final long boundary = (long) (Long.MAX_VALUE * rate); // safe cast as less <= 1\n    return new CollectorSampler() {\n      @Override\n      protected long boundary() {\n        return boundary;\n      }\n    };\n  }\n\n  protected abstract long boundary();\n\n  /**\n   * Returns true if spans with this trace ID should be recorded to storage.\n   *\n   * <p>Zipkin v1 allows storage-layer sampling, which can help prevent spikes in traffic from\n   * overloading the system. Debug spans are always stored.\n   *\n   * <p>This uses only the lower 64 bits of the trace ID as instrumentation still send mixed trace\n   * ID width.\n   *\n   * @param hexTraceId the lower 64 bits of the span's trace ID are checked against the boundary\n   * @param debug when true, always passes sampling\n   */\n  public boolean isSampled(String hexTraceId, boolean debug) {\n    if (Boolean.TRUE.equals(debug)) return true;\n    long traceId = HexCodec.lowerHexToUnsignedLong(hexTraceId);\n    // The absolute value of Long.MIN_VALUE is larger than a long, so Math.abs returns identity.\n    // This converts to MAX_VALUE to avoid always dropping when traceId == Long.MIN_VALUE\n    long t = traceId == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(traceId);\n    return t <= boundary();\n  }\n\n  @Override\n  public String toString() {\n    return \"CollectorSampler(\" + boundary() + \")\";\n  }\n\n  protected CollectorSampler() {}\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/main/java/zipkin2/collector/InMemoryCollectorMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class InMemoryCollectorMetrics implements CollectorMetrics {\n\n  private final ConcurrentHashMap<String, AtomicInteger> metrics;\n  private final String messages;\n  private final String messagesDropped;\n  private final String bytes;\n  private final String spans;\n  private final String spansDropped;\n\n  public InMemoryCollectorMetrics() {\n    this(new ConcurrentHashMap<>(), null);\n  }\n\n  InMemoryCollectorMetrics(ConcurrentHashMap<String, AtomicInteger> metrics, String transport) {\n    this.metrics = metrics;\n    this.messages = scope(\"messages\", transport);\n    this.messagesDropped = scope(\"messagesDropped\", transport);\n    this.bytes = scope(\"bytes\", transport);\n    this.spans = scope(\"spans\", transport);\n    this.spansDropped = scope(\"spansDropped\", transport);\n  }\n\n  @Override\n  public InMemoryCollectorMetrics forTransport(String transportType) {\n    if (transportType == null) throw new NullPointerException(\"transportType == null\");\n    return new InMemoryCollectorMetrics(metrics, transportType);\n  }\n\n  @Override\n  public void incrementMessages() {\n    increment(messages, 1);\n  }\n\n  public int messages() {\n    return get(messages);\n  }\n\n  @Override\n  public void incrementMessagesDropped() {\n    increment(messagesDropped, 1);\n  }\n\n  public int messagesDropped() {\n    return get(messagesDropped);\n  }\n\n  @Override\n  public void incrementBytes(int quantity) {\n    increment(bytes, quantity);\n  }\n\n  public int bytes() {\n    return get(bytes);\n  }\n\n  @Override\n  public void incrementSpans(int quantity) {\n    increment(spans, quantity);\n  }\n\n  public int spans() {\n    return get(spans);\n  }\n\n  @Override\n  public void incrementSpansDropped(int quantity) {\n    increment(spansDropped, quantity);\n  }\n\n  public int spansDropped() {\n    return get(spansDropped);\n  }\n\n  public void clear() {\n    metrics.clear();\n  }\n\n  private int get(String key) {\n    AtomicInteger atomic = metrics.get(key);\n    return atomic == null ? 0 : atomic.get();\n  }\n\n  private void increment(String key, int quantity) {\n    if (quantity == 0) return;\n    while (true) {\n      AtomicInteger metric = metrics.get(key);\n      if (metric == null) {\n        metric = metrics.putIfAbsent(key, new AtomicInteger(quantity));\n        if (metric == null) return; // won race creating the entry\n      }\n\n      while (true) {\n        int oldValue = metric.get();\n        int update = oldValue + quantity;\n        if (metric.compareAndSet(oldValue, update)) return; // won race updating\n      }\n    }\n  }\n\n  static String scope(String key, String transport) {\n    return key + (transport == null ? \"\" : \".\" + transport);\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/test/java/zipkin2/collector/CollectorSamplerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\n\nclass CollectorSamplerTest {\n\n  /**\n   * Math.abs(\"8000000000000000\") returns a negative, we coerse to \"7fffffffffffffff\" to avoid\n   * always dropping when trace_id == \"8000000000000000\"\n   */\n  @Test void mostNegativeNumberDefence() {\n    CollectorSampler sampler = CollectorSampler.create(0.1f);\n\n    assertThat(sampler.isSampled(\"8000000000000000\", false))\n        .isEqualTo(sampler.isSampled(\"7fffffffffffffff\", false));\n  }\n\n  @Test void debugWins() {\n    CollectorSampler sampler = CollectorSampler.create(0.0f);\n\n    assertThat(sampler.isSampled(\"8000000000000000\", true)).isTrue();\n  }\n\n  @Test void retain10Percent() {\n    float sampleRate = 0.1f;\n    CollectorSampler sampler = CollectorSampler.create(sampleRate);\n\n    assertThat(lotsOfSpans().filter(s -> sampler.isSampled(s.traceId(), false)).count())\n        .isCloseTo((long) (LOTS_OF_SPANS.length * sampleRate), withPercentage(3));\n  }\n\n  /**\n   * The collector needs to apply the same decision to incremental updates in a trace.\n   */\n  @Test void idempotent() {\n    CollectorSampler sampler1 = CollectorSampler.create(0.1f);\n    CollectorSampler sampler2 = CollectorSampler.create(0.1f);\n\n    assertThat(lotsOfSpans().filter(s -> sampler1.isSampled(s.traceId(), false)).toArray())\n        .containsExactly(\n            lotsOfSpans().filter(s -> sampler2.isSampled(s.traceId(), false)).toArray());\n  }\n\n  @Test void zeroMeansDropAllTraces() {\n    CollectorSampler sampler = CollectorSampler.create(0.0f);\n\n    assertThat(lotsOfSpans().filter(s -> sampler.isSampled(s.traceId(), false))).isEmpty();\n  }\n\n  @Test void oneMeansKeepAllTraces() {\n    CollectorSampler sampler = CollectorSampler.create(1.0f);\n\n    assertThat(lotsOfSpans().filter(s -> sampler.isSampled(s.traceId(), false)))\n        .hasSize(LOTS_OF_SPANS.length);\n  }\n\n  @Test void rateCantBeNegative() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> CollectorSampler.create(-1.0f));\n    assertThat(exception.getMessage()).contains(\"rate should be between 0 and 1: was -1.0\");\n  }\n\n  @Test void rateCantBeOverOne() {\n    Throwable exception = assertThrows(IllegalArgumentException.class, () -> CollectorSampler.create(1.1f));\n    assertThat(exception.getMessage()).contains(\"rate should be between 0 and 1: was 1.1\");\n  }\n\n  static Stream<Span> lotsOfSpans() {\n    return Stream.of(LOTS_OF_SPANS).parallel();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/core/src/test/java/zipkin2/collector/CollectorTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector;\n\nimport com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;\nimport java.util.concurrent.RejectedExecutionException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.event.Level;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.github.valfirst.slf4jtest.TestLoggerFactory.getLoggingEvents;\nimport static java.util.Arrays.asList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.Mockito.when;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.TRACE;\nimport static zipkin2.TestObjects.UTF_8;\n\n@ExtendWith(TestLoggerFactoryExtension.class)\nclass CollectorTest {\n  InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n  Callback<Void> callback = mock(Callback.class);\n  CollectorMetrics metrics = mock(CollectorMetrics.class);\n  Collector collector;\n  Logger testLogger = LoggerFactory.getLogger(CollectorTest.class);\n\n  @BeforeEach void setup() {\n    collector = spy(\n      new Collector.Builder(testLogger).metrics(metrics).storage(storage).build());\n    when(collector.idString(CLIENT_SPAN)).thenReturn(\"1\"); // to make expectations easier to read\n  }\n\n  @AfterEach void after() {\n    verifyNoMoreInteractions(metrics, callback);\n  }\n\n  @Test void unsampledSpansArentStored() {\n    collector = new Collector.Builder(LoggerFactory.getLogger(\"\"))\n      .sampler(CollectorSampler.create(0.0f))\n      .metrics(metrics)\n      .storage(storage)\n      .build();\n\n    collector.accept(TRACE, callback);\n\n    verify(callback).onSuccess(null);\n    assertThat(getLoggingEvents()).isEmpty();\n    verify(metrics).incrementSpans(4);\n    verify(metrics).incrementSpansDropped(4);\n    assertThat(storage.getTraces()).isEmpty();\n  }\n\n  @Test void errorDetectingFormat() {\n    collector.acceptSpans(new byte[] {'f', 'o', 'o'}, callback);\n\n    verify(callback).onError(any(RuntimeException.class));\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  @Test void acceptSpans_jsonV2() {\n    byte[] bytes = SpanBytesEncoder.JSON_V2.encodeList(TRACE);\n    collector.acceptSpans(bytes, callback);\n\n    verify(collector).acceptSpans(bytes, SpanBytesDecoder.JSON_V2, callback);\n\n    verify(callback).onSuccess(null);\n    assertThat(getLoggingEvents()).isEmpty();\n    verify(metrics).incrementSpans(4);\n    assertThat(storage.getTraces()).containsOnly(TRACE);\n  }\n\n  @Test void acceptSpans_decodingError() {\n    byte[] bytes = \"[\\\"='\".getBytes(UTF_8); // screwed up json\n    collector.acceptSpans(bytes, SpanBytesDecoder.JSON_V2, callback);\n\n    verify(callback).onError(any(IllegalArgumentException.class));\n    assertDebugLogIs(\"Malformed reading List<Span> from json\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  /** Tags in zipkin v2 model are stringly typed. */\n  @Test void acceptSpans_decodingError_nonStringValue() {\n    byte[] bytes = \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"tags\": {\n          \"error\": true\n        }\n      }\n      \"\"\".getBytes(UTF_8); // error tag has a bool instead of string value\n    collector.acceptSpans(bytes, SpanBytesDecoder.JSON_V2, callback);\n\n    verify(callback).onError(any(IllegalArgumentException.class));\n    assertDebugLogIs(\"Malformed reading List<Span> from json\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  @Test void accept_storageError() {\n    StorageComponent storage = mock(StorageComponent.class);\n    RuntimeException error = new RuntimeException(\"storage disabled\");\n    when(storage.spanConsumer()).thenThrow(error);\n    collector = new Collector.Builder(LoggerFactory.getLogger(\"\"))\n      .metrics(metrics)\n      .storage(storage)\n      .build();\n\n    collector.accept(TRACE, callback);\n\n    verify(callback).onSuccess(null); // error is async\n    assertDebugLogIs(\"Cannot store spans [1, 2, 2, ...] due to RuntimeException(storage disabled)\");\n    verify(metrics).incrementSpans(4);\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void acceptSpans_emptyMessageOk() {\n    byte[] bytes = new byte[] {'[', ']'};\n    collector.acceptSpans(bytes, callback);\n\n    verify(collector).acceptSpans(bytes, SpanBytesDecoder.JSON_V1, callback);\n\n    verify(callback).onSuccess(null);\n    assertThat(getLoggingEvents()).isEmpty();\n    assertThat(storage.getTraces()).isEmpty();\n  }\n\n  @Test void storeSpansCallback_toStringIncludesSpanIds() {\n    Span span2 = CLIENT_SPAN.toBuilder().id(\"3\").build();\n    when(collector.idString(span2)).thenReturn(\"3\");\n\n    assertThat(collector.new StoreSpans(asList(CLIENT_SPAN, span2)))\n      .hasToString(\"StoreSpans([1, 3])\");\n  }\n\n  @Test void storeSpansCallback_toStringIncludesSpanIds_noMoreThan3() {\n    assertThat(unprefixIdString(collector.new StoreSpans(TRACE).toString()))\n      .hasToString(\"StoreSpans([1, 1, 2, ...])\");\n  }\n\n  @Test void storeSpansCallback_onErrorWithNullMessage() {\n    RuntimeException error = new RuntimeException();\n\n    Callback<Void> callback = collector.new StoreSpans(TRACE);\n    callback.onError(error);\n\n    assertDebugLogIs(\"Cannot store spans [1, 1, 2, ...] due to RuntimeException()\");\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void storeSpansCallback_onErrorWithMessage() {\n    IllegalArgumentException error = new IllegalArgumentException(\"no beer\");\n    Callback<Void> callback = collector.new StoreSpans(TRACE);\n    callback.onError(error);\n\n    assertDebugLogIs(\"Cannot store spans [1, 1, 2, ...] due to IllegalArgumentException(no beer)\");\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void errorAcceptingSpans_onErrorRejectedExecution() {\n    RuntimeException error = new RejectedExecutionException(\"slow down\");\n    collector.handleStorageError(TRACE, error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\n      \"Cannot store spans [1, 1, 2, ...] due to RejectedExecutionException(slow down)\");\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void handleStorageError_onErrorWithNullMessage() {\n    RuntimeException error = new RuntimeException();\n    collector.handleStorageError(TRACE, error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Cannot store spans [1, 1, 2, ...] due to RuntimeException()\");\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void handleStorageError_onErrorWithMessage() {\n    RuntimeException error = new IllegalArgumentException(\"no beer\");\n    collector.handleStorageError(TRACE, error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Cannot store spans [1, 1, 2, ...] due to IllegalArgumentException(no beer)\");\n    verify(metrics).incrementSpansDropped(4);\n  }\n\n  @Test void handleDecodeError_onErrorWithNullMessage() {\n    RuntimeException error = new RuntimeException();\n    collector.handleDecodeError(error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Cannot decode spans due to RuntimeException()\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  @Test void handleDecodeError_onErrorWithMessage() {\n    IllegalArgumentException error = new IllegalArgumentException(\"no beer\");\n    collector.handleDecodeError(error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Cannot decode spans due to IllegalArgumentException(no beer)\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  @Test void handleDecodeError_doesntWrapMessageOnMalformedException() {\n    IllegalArgumentException error = new IllegalArgumentException(\"Malformed reading spans\");\n    collector.handleDecodeError(error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Malformed reading spans\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  @Test void handleDecodeError_doesntWrapMessageOnTruncatedException() {\n    IllegalArgumentException error = new IllegalArgumentException(\"Truncated reading spans\");\n    collector.handleDecodeError(error, callback);\n\n    verify(callback).onError(error);\n    assertDebugLogIs(\"Truncated reading spans\");\n    verify(metrics).incrementMessagesDropped();\n  }\n\n  private String unprefixIdString(String msg) {\n    return msg.replaceAll(\"7180c278b62e8f6a216a2aea45d08fc9/000000000000000\", \"\");\n  }\n\n  private void assertDebugLogIs(String message) {\n    assertThat(getLoggingEvents())\n      .hasSize(1)\n      .filteredOn(event -> event.getLevel().equals(Level.DEBUG))\n      .extracting(event -> unprefixIdString(event.getMessage()))\n      .containsOnly(message);\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/kafka/README.md",
    "content": "# collector-kafka\n\n## KafkaCollector\nThis collector is implemented as a Kafka consumer supporting Kafka brokers running\nversion 0.10.0.0 or later. It polls a Kafka [topic](#kafka-configuration) for messages that contain\na list of spans in json or TBinaryProtocol big-endian encoding. These\nspans are pushed to a span consumer.\n\nFor information about running this collector as a module in Zipkin server, see\nthe [Zipkin Server README](../../zipkin-server/README.md#kafka-collector).\n\nWhen using this collector as a library outside of Zipkin server,\n[zipkin2.collector.kafka.KafkaCollector.Builder](src/main/java/zipkin2/collector/kafka/KafkaCollector.java)\nincludes defaults that will operate against a Kafka topic name `zipkin`.\n\n## Encoding spans into Kafka messages\nThe message's binary data includes a list of spans. Supported encodings\nare the same as the http [POST /spans](https://zipkin.io/zipkin-api/#/paths/%252Fspans) body.\n\n### Json\nThe message's binary data is a list of spans in json. The first character must be '[' (decimal 91).\n\n`Codec.JSON.writeSpans(spans)` performs the correct json encoding.\n\nHere's an example, sending a list of a single span to the zipkin topic:\n\n```bash\n$ kafka-console-producer.sh --broker-list $ADVERTISED_HOST:9092 --topic zipkin\n[{\"traceId\":\"1\",\"name\":\"bang\",\"id\":\"2\",\"timestamp\":1470150004071068,\"duration\":1,\"localEndpoint\":{\"serviceName\":\"flintstones\"},\"tags\":{\"lc\":\"bamm-bamm\"}}]\n```\n\n### Thrift\nThe message's binary data includes a list header followed by N spans serialized in TBinaryProtocol\n\n`Codec.THRIFT.writeSpans(spans)` encodes spans in the following fashion:\n```\nwrite_byte(12) // type of the list elements: 12 == struct\nwrite_i32(count) // count of spans that will follow\nfor (int i = 0; i < count; i++) {\n  writeTBinaryProtocol(spans(i))\n}\n```\n\n### Legacy encoding\nOlder versions of zipkin accepted a single span per message, as opposed\nto a list per message. This practice is deprecated, but still supported.\n\n## Kafka configuration\n\nBelow are a few guidelines for the Kafka infrastructure used by this collector:\n* The collector does not explicitly create the `zipkin` topic itself. If your cluster has auto topic creation enabled then it will be created by Kafka automatically using the broker configured defaults. We recommend therefor creating the topic manually before starting the collector, using configuration parameters adapted for your Zipkin setup.\n* The collector will not fail if the `zipkin` topic does not exist, it will instead just wait for the topic to become available.\n* A size based retention makes more sense than the default time based (1 week), to safeguard against large bursts of span data.\n* The collector starts 1 instance of `KafkaConsumer` by default. We do recommend creating the `zipkin` topic with 6 or more partitions however, as it allows you to easily scale out the collector later by increasing the [KAFKA_STREAMS](../../zipkin-server/README.md#kafka-collector) parameter.\n* As Zipkin reporter sends batches of spans which do not rely on any kind of ordering guarantee (key=null), you can increase the number of partitions without affecting ordering. It does not make sense however to have more `KafkaConsumer` instances than partitions as the instances will just be idle and not consume anything.\n* Monitoring the consumer lag of the collector as well as the size of the topic will help you to decide if scaling up or down is needed.\n* Tuning this collector should happen in coordination with the storage backend. Parameters like `max.poll.records`, `fetch.max.bytes` can prevent the collector from overloading the storage backend, or if it's sized properly they could instead be used to increase ingestion rate.\n* A large and consistent consumer lag can indicate that the storage has difficulties with the ingestion rate and could be scaled up.\n\n## Logging\nZipkin by default suppresses all logging output from Kafka client operations as they can get quite verbose. Start Zipkin with `--logging.level.org.apache.kafka=INFO` or similar to override this during troubleshooting for example.\n"
  },
  {
    "path": "zipkin-collector/kafka/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector-kafka</artifactId>\n  <name>Collector: Kafka 0.10+</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n    <kafka.version>4.2.0</kafka.version>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.apache.kafka</groupId>\n      <artifactId>kafka-clients</artifactId>\n      <version>${kafka.version}</version>\n    </dependency>\n\n    <!-- For AdminClient -->\n    <dependency>\n      <groupId>com.fasterxml.jackson.core</groupId>\n      <artifactId>jackson-databind</artifactId>\n      <version>${jackson.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/kafka/src/main/java/zipkin2/collector/kafka/KafkaCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.kafka;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.common.KafkaException;\nimport org.apache.kafka.common.KafkaFuture;\nimport org.apache.kafka.common.config.ConfigException;\nimport org.apache.kafka.common.errors.InterruptException;\nimport org.apache.kafka.common.serialization.ByteArrayDeserializer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorComponent;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG;\nimport static org.apache.kafka.clients.consumer.ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;\nimport static org.apache.kafka.clients.consumer.ConsumerConfig.GROUP_ID_CONFIG;\nimport static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG;\nimport static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG;\n\n/**\n * This collector polls a Kafka topic for messages that contain TBinaryProtocol big-endian encoded\n * lists of spans. These spans are pushed to a {@link SpanConsumer#accept span consumer}.\n *\n * <p>This collector uses a Kafka 0.10+ consumer.\n */\npublic final class KafkaCollector extends CollectorComponent {\n  private static final Logger LOG = LoggerFactory.getLogger(KafkaCollector.class);\n\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /** Configuration including defaults needed to consume spans from a Kafka topic. */\n  public static final class Builder extends CollectorComponent.Builder {\n    final Properties properties = new Properties();\n    final Collector.Builder delegate = Collector.newBuilder(KafkaCollector.class);\n    CollectorMetrics metrics = CollectorMetrics.NOOP_METRICS;\n    String topic = \"zipkin\";\n    int streams = 1;\n\n    @Override\n    public Builder storage(StorageComponent storage) {\n      delegate.storage(storage);\n      return this;\n    }\n\n    @Override\n    public Builder sampler(CollectorSampler sampler) {\n      delegate.sampler(sampler);\n      return this;\n    }\n\n    @Override\n    public Builder metrics(CollectorMetrics metrics) {\n      if (metrics == null) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics.forTransport(\"kafka\");\n      delegate.metrics(this.metrics);\n      return this;\n    }\n\n    /**\n     * Topic zipkin spans will be consumed from. Defaults to \"zipkin\". Multiple topics may be\n     * specified if comma delimited.\n     */\n    public Builder topic(String topic) {\n      if (topic == null) throw new NullPointerException(\"topic == null\");\n      this.topic = topic;\n      return this;\n    }\n\n    /** The bootstrapServers connect string, ex. 127.0.0.1:9092. No default. */\n    public Builder bootstrapServers(String bootstrapServers) {\n      if (bootstrapServers == null) throw new NullPointerException(\"bootstrapServers == null\");\n      properties.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);\n      return this;\n    }\n\n    /** The consumer group this process is consuming on behalf of. Defaults to \"zipkin\" */\n    public Builder groupId(String groupId) {\n      if (groupId == null) throw new NullPointerException(\"groupId == null\");\n      properties.put(GROUP_ID_CONFIG, groupId);\n      return this;\n    }\n\n    /** Count of threads consuming the topic. Defaults to 1 */\n    public Builder streams(int streams) {\n      this.streams = streams;\n      return this;\n    }\n\n    /**\n     * By default, a consumer will be built from properties derived from builder defaults, as well\n     * as \"auto.offset.reset\" -> \"earliest\". Any properties set here will override the consumer\n     * config.\n     *\n     * <p>For example: Only consume spans since you connected by setting the below.\n     *\n     * <pre>{@code\n     * Map<String, String> overrides = new LinkedHashMap<>();\n     * overrides.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"latest\");\n     * builder.overrides(overrides);\n     * }</pre>\n     *\n     * @see org.apache.kafka.clients.consumer.ConsumerConfig\n     */\n    public final Builder overrides(Map<String, ?> overrides) {\n      if (overrides == null) throw new NullPointerException(\"overrides == null\");\n      properties.putAll(overrides);\n      return this;\n    }\n\n    @Override\n    public KafkaCollector build() {\n      return new KafkaCollector(this);\n    }\n\n    Builder() {\n      // Settings below correspond to \"New Consumer Configs\"\n      // https://kafka.apache.org/documentation/#newconsumerconfigs\n      properties.put(GROUP_ID_CONFIG, \"zipkin\");\n      properties.put(AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n      properties.put(KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName());\n      properties.put(VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName());\n    }\n  }\n\n  final LazyKafkaWorkers kafkaWorkers;\n  final Properties properties;\n  volatile AdminClient adminClient;\n\n  KafkaCollector(Builder builder) {\n    kafkaWorkers = new LazyKafkaWorkers(builder);\n    properties = builder.properties;\n  }\n\n  @Override\n  public KafkaCollector start() {\n    kafkaWorkers.start();\n    return this;\n  }\n\n  @Override\n  public CheckResult check() {\n    try {\n      CheckResult failure = kafkaWorkers.failure.get(); // check the kafka workers didn't quit\n      if (failure != null) return failure;\n      KafkaFuture<String> maybeClusterId = getAdminClient().describeCluster().clusterId();\n      maybeClusterId.get(1, TimeUnit.SECONDS);\n      return CheckResult.OK;\n    } catch (Throwable e) {\n      Call.propagateIfFatal(e);\n      return CheckResult.failed(e);\n    }\n  }\n\n  AdminClient getAdminClient() {\n    if (adminClient == null) {\n      synchronized (this) {\n        if (adminClient == null) {\n          adminClient = AdminClient.create(properties);\n        }\n      }\n    }\n    return adminClient;\n  }\n\n  @Override\n  public void close() {\n    kafkaWorkers.close();\n    if (adminClient != null) adminClient.close(Duration.ofSeconds(1));\n  }\n\n  @Override public final String toString() {\n    return \"KafkaCollector{\"\n      + \"bootstrapServers=\" + properties.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG)\n      + \", topic=\" + kafkaWorkers.builder.topic\n      + \"}\";\n  }\n\n  static final class LazyKafkaWorkers {\n    final int streams;\n    final Builder builder;\n    final AtomicReference<CheckResult> failure = new AtomicReference<>();\n    final CopyOnWriteArrayList<KafkaCollectorWorker> workers = new CopyOnWriteArrayList<>();\n    volatile ExecutorService pool;\n\n    LazyKafkaWorkers(Builder builder) {\n      this.streams = builder.streams;\n      this.builder = builder;\n    }\n\n    void start() {\n      if (pool == null) {\n        synchronized (this) {\n          if (pool == null) {\n            pool = compute();\n          }\n        }\n      }\n    }\n\n    void close() {\n      ExecutorService maybePool = pool;\n      if (maybePool == null) return;\n      for (KafkaCollectorWorker worker : workers) {\n        worker.stop();\n      }\n      maybePool.shutdown();\n      try {\n        if (!maybePool.awaitTermination(2, TimeUnit.SECONDS)) {\n          // Timeout exceeded: force shutdown\n          maybePool.shutdownNow();\n        }\n      } catch (InterruptedException e) {\n        // at least we tried\n      }\n    }\n\n    ExecutorService compute() {\n      ExecutorService pool =\n        streams == 1 ? Executors.newSingleThreadExecutor() : Executors.newFixedThreadPool(streams);\n\n      for (int i = 0; i < streams; i++) {\n        // TODO: bad idea to lazy reference properties from a mutable builder\n        // copy them here and then pass this to the KafkaCollectorWorker constructor instead\n        KafkaCollectorWorker worker = new KafkaCollectorWorker(builder);\n        workers.add(worker);\n        pool.execute(guardFailures(worker));\n      }\n\n      return pool;\n    }\n\n    Runnable guardFailures(final Runnable delegate) {\n      return () -> {\n        try {\n          delegate.run();\n        } catch (InterruptException e) {\n          // Interrupts are normal on shutdown, intentionally swallow\n        } catch (KafkaException e) {\n          KafkaException kafkaException = e;\n          if (kafkaException.getCause() instanceof ConfigException) kafkaException = (KafkaException) kafkaException.getCause();\n          LOG.error(\"Kafka worker exited with exception\", kafkaException);\n          failure.set(CheckResult.failed(kafkaException));\n        } catch (RuntimeException e) {\n          LOG.error(\"Kafka worker exited with exception\", e);\n          failure.set(CheckResult.failed(e));\n        } catch (Error e) {\n          LOG.error(\"Kafka worker exited with error\", e);\n          failure.set(CheckResult.failed(new RuntimeException(e)));\n        }\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/kafka/src/main/java/zipkin2/collector/kafka/KafkaCollectorWorker.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.kafka;\n\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.apache.kafka.clients.consumer.ConsumerRebalanceListener;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.clients.consumer.ConsumerRecords;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.common.TopicPartition;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\n\n/** Consumes spans from Kafka messages, ignoring malformed input */\nfinal class KafkaCollectorWorker implements Runnable {\n  static final Logger LOG = LoggerFactory.getLogger(KafkaCollectorWorker.class);\n  static final Callback<Void> NOOP =\n      new Callback<Void>() {\n        @Override\n        public void onSuccess(Void value) {}\n\n        @Override\n        public void onError(Throwable t) {}\n      };\n\n  final Properties properties;\n  final List<String> topics;\n  final Collector collector;\n  final CollectorMetrics metrics;\n  // added for integration tests only, see ITKafkaCollector\n  final AtomicReference<List<TopicPartition>> assignedPartitions =\n      new AtomicReference<>(List.of());\n  final AtomicBoolean running = new AtomicBoolean(true);\n\n  KafkaCollectorWorker(KafkaCollector.Builder builder) {\n    properties = builder.properties;\n    topics = Arrays.asList(builder.topic.split(\",\"));\n    collector = builder.delegate.build();\n    metrics = builder.metrics;\n  }\n\n  @Override\n  public void run() {\n    try (KafkaConsumer<byte[], byte[]> kafkaConsumer = new KafkaConsumer<>(properties)) {\n      kafkaConsumer.subscribe(\n        topics,\n        // added for integration tests only, see ITKafkaCollector\n        new ConsumerRebalanceListener() {\n          @Override\n          public void onPartitionsRevoked(Collection<TopicPartition> partitions) {\n            // technically we should remove only the revoked partitions but for test purposes it\n            // does not matter\n            assignedPartitions.set(List.of());\n          }\n\n          @Override\n          public void onPartitionsAssigned(Collection<TopicPartition> partitions) {\n            assignedPartitions.set(List.copyOf(partitions));\n          }\n        });\n      LOG.debug(\"Kafka consumer starting polling loop.\");\n      while (running.get()) {\n        final ConsumerRecords<byte[], byte[]> consumerRecords = kafkaConsumer.poll(Duration.of(1000, ChronoUnit.MILLIS));\n        LOG.debug(\"Kafka polling returned batch of {} messages.\", consumerRecords.count());\n        for (ConsumerRecord<byte[], byte[]> record : consumerRecords) {\n          final byte[] bytes = record.value();\n          metrics.incrementMessages();\n          metrics.incrementBytes(bytes.length);\n\n          if (bytes.length == 0) continue; // lenient on empty messages\n\n          if (bytes.length < 2) { // need two bytes to check if protobuf\n            metrics.incrementMessagesDropped();\n          } else {\n            // If we received legacy single-span encoding, decode it into a singleton list\n            if (!protobuf3(bytes) && bytes[0] <= 16 && bytes[0] != 12 /* thrift, but not list */) {\n              Span span;\n              try {\n                span = SpanBytesDecoder.THRIFT.decodeOne(bytes);\n              } catch (RuntimeException e) {\n                metrics.incrementMessagesDropped();\n                continue;\n              }\n              collector.accept(List.of(span), NOOP);\n            } else {\n              collector.acceptSpans(bytes, NOOP);\n            }\n          }\n        }\n      }\n    } catch (RuntimeException | Error e) {\n      LOG.warn(\"Unexpected error in polling loop spans\", e);\n      throw e;\n    } finally {\n      LOG.debug(\"Kafka consumer polling loop stopped. Kafka consumer closed.\");\n    }\n  }\n\n  /**\n   * Stop the polling loop\n   */\n  public void stop() {\n    running.set(false);\n  }\n\n  /* span key or trace ID key */\n  static boolean protobuf3(byte[] bytes) {\n    return bytes[0] == 10 && bytes[1] != 0; // varint follows and won't be zero\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/kafka/src/test/java/zipkin2/collector/kafka/ITKafkaCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.kafka;\n\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.KafkaException;\nimport org.apache.kafka.common.serialization.ByteArraySerializer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.TestObjects.UTF_8;\nimport static zipkin2.codec.SpanBytesEncoder.JSON_V2;\nimport static zipkin2.codec.SpanBytesEncoder.THRIFT;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Timeout(60)\n@Tag(\"docker\")\nclass ITKafkaCollector {\n  @RegisterExtension static KafkaExtension kafka = new KafkaExtension();\n\n  List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1]);\n\n  InMemoryCollectorMetrics metrics = new InMemoryCollectorMetrics();\n  InMemoryCollectorMetrics kafkaMetrics = metrics.forTransport(\"kafka\");\n\n  CopyOnWriteArraySet<Thread> threadsProvidingSpans = new CopyOnWriteArraySet<>();\n  LinkedBlockingQueue<List<Span>> receivedSpans = new LinkedBlockingQueue<>();\n  SpanConsumer consumer = (spans) -> {\n    threadsProvidingSpans.add(Thread.currentThread());\n    receivedSpans.add(spans);\n    return Call.create(null);\n  };\n  KafkaProducer<byte[], byte[]> producer;\n\n  @BeforeEach void setup() {\n    metrics.clear();\n    threadsProvidingSpans.clear();\n    Properties config = new Properties();\n    config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.bootstrapServer());\n    producer = new KafkaProducer<>(config, new ByteArraySerializer(), new ByteArraySerializer());\n  }\n\n  @AfterEach void tearDown() {\n    if (producer != null) producer.close();\n  }\n\n  @Test void checkPasses() {\n    try (KafkaCollector collector = builder(\"check_passes\").build()) {\n      assertThat(collector.check().ok()).isTrue();\n    }\n  }\n\n  /**\n   * Don't raise exception (crash process), rather fail status check! This allows the health check\n   * to report the cause.\n   */\n  @Test void check_failsOnInvalidBootstrapServers() throws Exception {\n\n    KafkaCollector.Builder builder =\n      builder(\"fail_invalid_bootstrap_servers\").bootstrapServers(\"1.1.1.1\");\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n\n      Thread.sleep(1000L); // wait for crash\n\n      assertThat(collector.check().error())\n        .isInstanceOf(KafkaException.class)\n        .hasMessage(\"Invalid url in bootstrap.servers: 1.1.1.1\");\n    }\n  }\n\n  /**\n   * If the Kafka broker(s) specified in the connection string are not available, the Kafka consumer\n   * library will attempt to reconnect indefinitely. The Kafka consumer will not throw an exception\n   * and does not expose the status of its connection to the Kafka broker(s) in its API.\n   * <p>\n   * An AdminClient API instance has been added to the connector to validate that connection with\n   * Kafka is available in every health check. This AdminClient reuses Consumer's properties to\n   * Connect to the cluster, and request a Cluster description to validate communication with\n   * Kafka.\n   */\n  @Test void reconnectsIndefinitelyAndReportsUnhealthyWhenKafkaUnavailable() throws Exception {\n    KafkaCollector.Builder builder =\n      builder(\"fail_invalid_bootstrap_servers\").bootstrapServers(\"localhost:\" + 9092);\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      Thread.sleep(TimeUnit.SECONDS.toMillis(1));\n      assertThat(collector.check().error()).isInstanceOf(TimeoutException.class);\n    }\n  }\n\n  /** Ensures legacy encoding works: a single TBinaryProtocol encoded span */\n  @Test void messageWithSingleThriftSpan() throws Exception {\n    KafkaCollector.Builder builder = builder(\"single_span\");\n\n    byte[] bytes = THRIFT.encode(CLIENT_SPAN);\n    produceSpans(bytes, builder.topic);\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsExactly(CLIENT_SPAN);\n    }\n\n    assertThat(kafkaMetrics.messages()).isEqualTo(1);\n    assertThat(kafkaMetrics.messagesDropped()).isZero();\n    assertThat(kafkaMetrics.bytes()).isEqualTo(bytes.length);\n    assertThat(kafkaMetrics.spans()).isEqualTo(1);\n    assertThat(kafkaMetrics.spansDropped()).isZero();\n  }\n\n  /** Ensures list encoding works: a TBinaryProtocol encoded list of spans */\n  @Test void messageWithMultipleSpans_thrift() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_thrift\"), THRIFT);\n  }\n\n  /** Ensures list encoding works: a json encoded list of spans */\n  @Test void messageWithMultipleSpans_json() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_json\"), SpanBytesEncoder.JSON_V1);\n  }\n\n  /** Ensures list encoding works: a version 2 json list of spans */\n  @Test void messageWithMultipleSpans_json2() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_json2\"), SpanBytesEncoder.JSON_V2);\n  }\n\n  /** Ensures list encoding works: proto3 ListOfSpans */\n  @Test void messageWithMultipleSpans_proto3() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_proto3\"), SpanBytesEncoder.PROTO3);\n  }\n\n  void messageWithMultipleSpans(KafkaCollector.Builder builder, SpanBytesEncoder encoder)\n    throws Exception {\n    byte[] message = encoder.encodeList(spans);\n\n    produceSpans(message, builder.topic);\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsAll(spans);\n    }\n\n    assertThat(kafkaMetrics.messages()).isEqualTo(1);\n    assertThat(kafkaMetrics.messagesDropped()).isZero();\n    assertThat(kafkaMetrics.bytes()).isEqualTo(message.length);\n    assertThat(kafkaMetrics.spans()).isEqualTo(spans.size());\n    assertThat(kafkaMetrics.spansDropped()).isZero();\n  }\n\n  /** Ensures malformed spans don't hang the collector */\n  @Test void skipsMalformedData() throws Exception {\n    KafkaCollector.Builder builder = builder(\"decoder_exception\");\n\n    byte[] malformed1 = \"[\\\"='\".getBytes(UTF_8); // screwed up json\n    byte[] malformed2 = \"malformed\".getBytes(UTF_8);\n    produceSpans(THRIFT.encodeList(spans), builder.topic);\n    produceSpans(new byte[0], builder.topic);\n    produceSpans(malformed1, builder.topic);\n    produceSpans(malformed2, builder.topic);\n    produceSpans(THRIFT.encodeList(spans), builder.topic);\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n      // the only way we could read this, is if the malformed spans were skipped.\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    }\n\n    assertThat(kafkaMetrics.messages()).isEqualTo(5);\n    assertThat(kafkaMetrics.messagesDropped()).isEqualTo(2); // only malformed, not empty\n    assertThat(kafkaMetrics.bytes())\n      .isEqualTo(THRIFT.encodeList(spans).length * 2 + malformed1.length + malformed2.length);\n    assertThat(kafkaMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(kafkaMetrics.spansDropped()).isZero();\n  }\n\n  /** Guards against errors that leak from storage, such as InvalidQueryException */\n  @Test void skipsOnSpanStorageException() throws Exception {\n    AtomicInteger counter = new AtomicInteger();\n    consumer = (input) -> new Call.Base<Void>() {\n      @Override protected Void doExecute() {\n        throw new AssertionError();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        if (counter.getAndIncrement() == 1) {\n          callback.onError(new RuntimeException(\"storage fell over\"));\n        } else {\n          receivedSpans.add(spans);\n          callback.onSuccess(null);\n        }\n      }\n\n      @Override public Call<Void> clone() {\n        throw new AssertionError();\n      }\n    };\n    final StorageComponent storage = buildStorage(consumer);\n    KafkaCollector.Builder builder = builder(\"storage_exception\").storage(storage);\n\n    produceSpans(THRIFT.encodeList(spans), builder.topic);\n    produceSpans(THRIFT.encodeList(spans), builder.topic); // tossed on error\n    produceSpans(THRIFT.encodeList(spans), builder.topic);\n\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n      // the only way we could read this, is if the malformed span was skipped.\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    }\n\n    assertThat(kafkaMetrics.messages()).isEqualTo(3);\n    assertThat(kafkaMetrics.messagesDropped()).isZero(); // storage failure isn't a message failure\n    assertThat(kafkaMetrics.bytes()).isEqualTo(THRIFT.encodeList(spans).length * 3);\n    assertThat(kafkaMetrics.spans()).isEqualTo(spans.size() * 3);\n    assertThat(kafkaMetrics.spansDropped()).isEqualTo(spans.size()); // only one dropped\n  }\n\n  @Test void messagesDistributedAcrossMultipleThreadsSuccessfully() throws Exception {\n    KafkaCollector.Builder builder = builder(\"multi_thread\", 2);\n\n    kafka.prepareTopics(builder.topic, 2);\n    warmUpTopic(builder.topic);\n\n    final byte[] traceBytes = JSON_V2.encodeList(spans);\n    try (KafkaCollector collector = builder.build()) {\n      collector.start();\n      waitForPartitionAssignments(collector);\n      produceSpans(traceBytes, builder.topic, 0);\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n      produceSpans(traceBytes, builder.topic, 1);\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    }\n\n    assertThat(threadsProvidingSpans).hasSize(2);\n\n    assertThat(kafkaMetrics.messages()).isEqualTo(3); // 2 + empty body for warmup\n    assertThat(kafkaMetrics.messagesDropped()).isZero();\n    assertThat(kafkaMetrics.bytes()).isEqualTo(traceBytes.length * 2);\n    assertThat(kafkaMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(kafkaMetrics.spansDropped()).isZero();\n  }\n\n  @Test void multipleTopicsCommaDelimited() {\n    try (KafkaCollector collector = builder(\"topic1,topic2\").build()) {\n      collector.start();\n\n      assertThat(collector.kafkaWorkers.workers.get(0).topics).containsExactly(\"topic1\", \"topic2\");\n    }\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    try (KafkaCollector collector = builder(\"muah\").build()) {\n      collector.start();\n\n      assertThat(collector).hasToString(\n        \"KafkaCollector{bootstrapServers=%s, topic=%s}\".formatted(kafka.bootstrapServer(),\n          \"muah\")\n      );\n    }\n  }\n\n  /**\n   * Producing this empty message triggers auto-creation of the topic and gets things \"warmed up\" on\n   * the broker before the consumers subscribe. Without this, the topic is auto-created when the\n   * first consumer subscribes but there appears to be a race condition where the existence of the\n   * topic is not known to the partition assignor when the consumer group goes through its initial\n   * re-balance. As a result, no partitions are assigned, there are no further changes to group\n   * membership to trigger another re-balance, and no messages are consumed. This initial message is\n   * not necessary if the test broker is re-created for each test, but that increases execution time\n   * for the suite by a factor of 10x (2-3s to ~25s on my local machine).\n   */\n  void warmUpTopic(String topic) {\n    produceSpans(new byte[0], topic);\n  }\n\n  /**\n   * Wait until all kafka consumers created by the collector have at least one partition assigned.\n   */\n  void waitForPartitionAssignments(KafkaCollector collector) throws Exception {\n    long consumersWithAssignments = 0;\n    while (consumersWithAssignments < collector.kafkaWorkers.streams) {\n      Thread.sleep(10);\n      consumersWithAssignments =\n        collector\n          .kafkaWorkers\n          .workers\n          .stream()\n          .filter(w -> !w.assignedPartitions.get().isEmpty())\n          .count();\n    }\n  }\n\n  void produceSpans(byte[] spans, String topic) {\n    produceSpans(spans, topic, 0);\n  }\n\n  void produceSpans(byte[] spans, String topic, Integer partition) {\n    producer.send(new ProducerRecord<>(topic, partition, null, spans));\n    producer.flush();\n  }\n\n  KafkaCollector.Builder builder(String topic) {\n    return builder(topic, 1);\n  }\n\n  KafkaCollector.Builder builder(String topic, int streams) {\n    return kafka.newCollectorBuilder(topic, streams)\n      .metrics(metrics)\n      .storage(buildStorage(consumer));\n  }\n\n  static StorageComponent buildStorage(final SpanConsumer spanConsumer) {\n    return new ForwardingStorageComponent() {\n      @Override protected StorageComponent delegate() {\n        throw new AssertionError();\n      }\n\n      @Override public SpanConsumer spanConsumer() {\n        return spanConsumer;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/kafka/src/test/java/zipkin2/collector/kafka/KafkaExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.kafka;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.concurrent.ExecutionException;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AdminClientConfig;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.common.errors.TopicExistsException;\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.opentest4j.TestAbortedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.InternetProtocol;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\nclass KafkaExtension implements BeforeAllCallback, AfterAllCallback {\n  static final Logger LOGGER = LoggerFactory.getLogger(KafkaExtension.class);\n  static final int KAFKA_PORT = 19092;\n\n  final KafkaContainer container = new KafkaContainer();\n\n  @Override public void beforeAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using bootstrapServer {}\", bootstrapServer());\n  }\n\n  void prepareTopics(String topics, int partitions) {\n    Properties config = new Properties();\n    config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer());\n\n    List<NewTopic> newTopics = new ArrayList<>();\n    for (String topic : topics.split(\",\")) {\n      if (\"\".equals(topic)) continue;\n      newTopics.add(new NewTopic(topic, partitions, (short) 1));\n    }\n\n    try (AdminClient adminClient = AdminClient.create(config)) {\n      adminClient.createTopics(newTopics).all().get();\n    } catch (InterruptedException | ExecutionException e) {\n      if (e.getCause() != null && e.getCause() instanceof TopicExistsException) return;\n      throw new TestAbortedException(\n        \"Topics could not be created \" + newTopics + \": \" + e.getMessage(), e);\n    }\n  }\n\n  String bootstrapServer() {\n    return container.getHost() + \":\" + container.getMappedPort(KAFKA_PORT);\n  }\n\n  KafkaCollector.Builder newCollectorBuilder(String topic, int streams) {\n    prepareTopics(topic, streams);\n    return KafkaCollector.builder().bootstrapServers(bootstrapServer())\n      .topic(topic)\n      .groupId(topic + \"_group\")\n      .streams(streams);\n  }\n\n  @Override public void afterAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n    container.stop();\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class KafkaContainer extends GenericContainer<KafkaContainer> {\n    KafkaContainer() {\n      super(parse(\"ghcr.io/openzipkin/zipkin-kafka:3.4.3\"));\n      waitStrategy = Wait.forHealthcheck();\n      // 19092 is for connections from the Docker host and needs to be used as a fixed port.\n      // TODO: someone who knows Kafka well, make ^^ comment better!\n      addFixedExposedPort(KAFKA_PORT, KAFKA_PORT, InternetProtocol.TCP);\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/kafka/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\norg.slf4j.simpleLogger.log.com.github.charithe.kafka=info\norg.slf4j.simpleLogger.log.zipkin2.collector.kafka=debug\n# uncomment to include kafka consumer configuration in test logs\n#logger.org.apache.kafka.clients.level=info\n"
  },
  {
    "path": "zipkin-collector/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <groupId>io.zipkin.zipkin2</groupId>\n  <artifactId>zipkin-collector-parent</artifactId>\n  <name>Collector</name>\n  <packaging>pom</packaging>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n  </properties>\n\n  <modules>\n    <module>core</module>\n    <module>activemq</module>\n    <module>kafka</module>\n    <module>rabbitmq</module>\n    <module>scribe</module>\n    <module>pulsar</module>\n  </modules>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <!-- Our json codec is shaded, but Intellij doesn't understand that when running tests. -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-tests</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-simple</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n      <version>${testcontainers.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/pulsar/README.md",
    "content": "# collector-pulsar\n\n## PulsarCollector\n\nThis collector is implemented as a Pulsar consumer supporting Pulsar brokers running\nversion 4.x or later, and the default subscription type is `Shared`, in Shared subscription type, \nmultiple consumers can attach to the same subscription and messages are delivered \nin a round-robin distribution across consumers.\n\nThis collector is implemented as a Pulsar consumer supporting Pulsar brokers running version 4.x or later. \nThe default `subscriptionType` is `Shared`, which allows multiple consumers to attach to the same subscription, \nwith messages delivered in a round-robin distribution across consumers, the default `subscriptionInitialPosition`\nis `Earliest`, you can modify the consumer settings as needed through the `consumerProps` parameter.\nAlso, the client settings can also be modified through the `clientProps` parameter.\n\nFor information about running this collector as a module in Zipkin server, see\nthe [Zipkin Server README](../../zipkin-server/README.md#pulsar-collector).\n\nWhen using this collector as a library outside of Zipkin server,\n[zipkin2.collector.pulsar.PulsarCollector.Builder](src/main/java/zipkin2/collector/pulsar/PulsarCollector.java)\nincludes defaults that will operate against a Pulsar topic name `zipkin`.\n\n## Encoding spans into Pulsar messages\n\nThe message's binary data includes a list of spans. Supported encodings\nare the same as the http [POST /spans](https://zipkin.io/zipkin-api/#/paths/%252Fspans) body.\n\n### Json\n\nThe message's binary data is a list of spans in json. The first character must be '[' (decimal 91).\n\n`Codec.JSON.writeSpans(spans)` performs the correct json encoding.\n\n### Thrift\n\nThe message's binary data includes a list header followed by N spans serialized in TBinaryProtocol\n\n`Codec.THRIFT.writeSpans(spans)` encodes spans in the following fashion:\n\n```\nwrite_byte(12) // type of the list elements: 12 == struct\nwrite_i32(count) // count of spans that will follow\nfor (int i = 0; i < count; i++) {\n  writeTBinaryProtocol(spans(i))\n}\n```\n\n### Legacy encoding\n\nOlder versions of zipkin accepted a single span per message, as opposed\nto a list per message. This practice is deprecated, but still supported.\n\n## Logging\n\nZipkin by default suppresses all logging output from Pulsar client operations as they can get quite verbose. Start\nZipkin\nwith `--logging.level.org.apache.pulsar=INFO` or similar to override this during troubleshooting for example.\n"
  },
  {
    "path": "zipkin-collector/pulsar/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector-pulsar</artifactId>\n  <name>Collector: Pulsar</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n    <pulsar-client.version>4.0.2</pulsar-client.version>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.apache.pulsar</groupId>\n      <artifactId>pulsar-client</artifactId>\n      <version>${pulsar-client.version}</version>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/pulsar/src/main/java/zipkin2/collector/pulsar/LazyPulsarInit.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.pulsar;\n\nimport org.apache.pulsar.client.api.PulsarClient;\nimport org.apache.pulsar.client.api.PulsarClientException;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\n\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\n\nclass LazyPulsarInit {\n\n  private final Collector collector;\n  private final CollectorMetrics metrics;\n  private final String topic;\n  private final int concurrency;\n  private final Map<String, Object> clientProps, consumerProps;\n  public volatile PulsarClient result;\n  final AtomicReference<CheckResult> failure = new AtomicReference<>();\n\n  LazyPulsarInit(PulsarCollector.Builder builder) {\n    this.collector = builder.delegate.build();\n    this.metrics = builder.metrics;\n    this.topic = builder.topic;\n    this.concurrency = builder.concurrency;\n    this.clientProps = builder.clientProps;\n    this.consumerProps = builder.consumerProps;\n  }\n\n  void init() {\n    if (result == null) {\n      synchronized (this) {\n        if (result == null) {\n          result = subscribe();\n        }\n      }\n    }\n  }\n\n  private PulsarClient subscribe() {\n    PulsarClient client;\n    try {\n      client = PulsarClient.builder()\n          .loadConf(clientProps)\n          .build();\n    } catch (Exception e) {\n      failure.set(CheckResult.failed(e));\n      throw new RuntimeException(\"Pulsar client creation failed. \" + e.getMessage(), e);\n    }\n\n    try {\n      for (int i = 0; i < concurrency; i++) {\n        PulsarSpanConsumer consumer = new PulsarSpanConsumer(topic, consumerProps, client, collector, metrics);\n        consumer.startConsumer();\n      }\n      return client;\n    } catch (Exception e) {\n      try {\n        client.close();\n      } catch (PulsarClientException ex) {\n        // Nobody cares me.\n      }\n      failure.set(CheckResult.failed(e));\n      throw new RuntimeException(\"Pulsar Client is unable to subscribe to the topic(\" + topic + \"), please check the service.\", e);\n    }\n  }\n\n  void close() throws PulsarClientException {\n    PulsarClient maybe = result;\n    if (maybe != null) {\n      result.close();\n      result = null;\n    }\n  }\n}"
  },
  {
    "path": "zipkin-collector/pulsar/src/main/java/zipkin2/collector/pulsar/PulsarCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.pulsar;\n\nimport io.opentelemetry.api.internal.StringUtils;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorComponent;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.StorageComponent;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/** This collector consumes encoded binary messages from a Pulsar topic. */\npublic final class PulsarCollector extends CollectorComponent {\n\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /** Configuration including defaults needed to consume spans from a Pulsar topic. */\n  public static final class Builder extends CollectorComponent.Builder {\n    final Collector.Builder delegate = Collector.newBuilder(PulsarCollector.class);\n    CollectorMetrics metrics = CollectorMetrics.NOOP_METRICS;\n    Map<String, Object> clientProps = new HashMap<>();\n    Map<String, Object> consumerProps = new HashMap<>();\n    String topic = \"zipkin\";\n    int concurrency = 1;\n\n    @Override\n    public Builder storage(StorageComponent storage) {\n      delegate.storage(storage);\n      return this;\n    }\n\n    @Override\n    public Builder metrics(CollectorMetrics metrics) {\n      if (Objects.isNull(metrics)) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics.forTransport(\"pulsar\");\n      this.delegate.metrics(this.metrics);\n      return this;\n    }\n\n    @Override\n    public Builder sampler(CollectorSampler sampler) {\n      this.delegate.sampler(sampler);\n      return this;\n    }\n\n    @Override\n    public PulsarCollector build() {\n      return new PulsarCollector(this);\n    }\n\n    /** Count of concurrent message consumers on the topic. Defaults to 1. */\n    public Builder concurrency(Integer concurrency) {\n      if (concurrency < 1) throw new IllegalArgumentException(\"concurrency < 1\");\n      this.concurrency = concurrency;\n      return this;\n    }\n\n    /** Queue zipkin spans will be consumed from. Defaults to \"zipkin\". */\n    public Builder topic(String topic) {\n      if (StringUtils.isNullOrEmpty(topic)) throw new NullPointerException(\"topic is null or empty\");\n      this.topic = topic;\n      return this;\n    }\n\n    /** The service URL for the Pulsar client ex. pulsar://my-broker:6650. No default. */\n    public Builder serviceUrl(String serviceUrl) {\n      if (StringUtils.isNullOrEmpty(serviceUrl)) throw new NullPointerException(\"serviceUrl is null or empty\");\n      clientProps.put(\"serviceUrl\", serviceUrl);\n      return this;\n    }\n\n    /** Specify the subscription name for this consumer. No default. */\n    public Builder subscriptionName(String subscriptionName) {\n      if (StringUtils.isNullOrEmpty(subscriptionName)) throw new NullPointerException(\"serviceUrl is null or empty\");\n      consumerProps.put(\"subscriptionName\", subscriptionName);\n      return this;\n    }\n\n    /**\n     * Any properties set here will override the previous Pulsar client configuration.\n     *\n     * @param clientPropsMap Map<String, Object>\n     * @return Builder\n     * @see org.apache.pulsar.client.api.ClientBuilder#loadConf(Map)\n     */\n    public Builder clientProps(Map<String, Object> clientPropsMap) {\n      if (clientPropsMap.isEmpty()) throw new NullPointerException(\"clientProps is empty\");\n      clientProps.putAll(clientPropsMap);\n      return this;\n    }\n\n    /**\n     * Any properties set here will override the previous Pulsar consumer configuration.\n     *\n     * @param consumerPropsMap Map<String, Object>\n     * @return Builder\n     * @see org.apache.pulsar.client.api.ConsumerBuilder#loadConf(Map)\n     */\n    public Builder consumerProps(Map<String, Object> consumerPropsMap) {\n      if (consumerPropsMap.isEmpty()) throw new NullPointerException(\"consumerProps is empty\");\n      consumerProps.putAll(consumerPropsMap);\n      return this;\n    }\n  }\n\n  final Map<String, Object> clientProps, consumerProps;\n  final String topic;\n  final LazyPulsarInit lazyPulsarInit;\n\n  PulsarCollector(Builder builder) {\n    clientProps = builder.clientProps;\n    consumerProps = builder.consumerProps;\n    this.topic = builder.topic;\n    this.lazyPulsarInit = new LazyPulsarInit(builder);\n  }\n\n  @Override\n  public PulsarCollector start() {\n    lazyPulsarInit.init();\n    return this;\n  }\n\n  @Override public void close() throws IOException {\n    lazyPulsarInit.close();\n  }\n\n  @Override public CheckResult check() {\n    try {\n      CheckResult failure = lazyPulsarInit.failure.get();\n      if (failure != null) return failure;\n      return CheckResult.OK;\n    } catch (Throwable th) {\n      Call.propagateIfFatal(th);\n      return CheckResult.failed(th);\n    }\n  }\n\n  @Override public String toString() {\n    return \"PulsarCollector{\" +\n        \"clientProps=\" + clientProps +\n        \", consumerProps=\" + consumerProps +\n        \", topic=\" + this.topic +\n        \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/pulsar/src/main/java/zipkin2/collector/pulsar/PulsarSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.pulsar;\n\nimport org.apache.pulsar.client.api.Consumer;\nimport org.apache.pulsar.client.api.Message;\nimport org.apache.pulsar.client.api.MessageListener;\nimport org.apache.pulsar.client.api.PulsarClient;\nimport org.apache.pulsar.client.api.PulsarClientException;\nimport org.apache.pulsar.client.api.SubscriptionInitialPosition;\nimport org.apache.pulsar.client.api.SubscriptionType;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Callback;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\n\nimport java.io.Closeable;\nimport java.util.Map;\n\npublic class PulsarSpanConsumer implements Closeable {\n  static final Callback<Void> NOOP = new Callback<>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n    }\n  };\n\n  private static final Logger LOG = LoggerFactory.getLogger(PulsarSpanConsumer.class);\n  private final String topic;\n  private final Map<String, Object> consumerProps;\n  private final PulsarClient client;\n  private final Collector collector;\n  private final CollectorMetrics metrics;\n  private Consumer<byte[]> consumer;\n\n  public PulsarSpanConsumer(String topic, Map<String, Object> consumerProps, PulsarClient client, Collector collector, CollectorMetrics metrics) {\n    this.topic = topic;\n    this.consumerProps = consumerProps;\n    this.client = client;\n    this.collector = collector;\n    this.metrics = metrics;\n  }\n\n  public void startConsumer() throws PulsarClientException {\n    consumer = client.newConsumer()\n        .topic(topic)\n        .subscriptionType(SubscriptionType.Shared)\n        .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)\n        .loadConf(consumerProps)\n        .messageListener(new ZipkinMessageListener<>(collector, metrics))\n        .subscribe();\n  }\n\n  @Override public void close() {\n    try {\n      if (consumer != null) {\n        consumer.close();\n        consumer = null;\n      }\n    } catch (PulsarClientException e) {\n      LOG.error(\"Failed to close Pulsar Consumer client.\", e);\n    }\n  }\n\n  /**\n   * A message listener implementation for processing messages in a Pulsar consumer,\n   * and it should not be overridden by loadConf as it ensures that zipkin could handle span correctly.\n   */\n  record ZipkinMessageListener<T>(Collector collector, CollectorMetrics metrics) implements MessageListener<T> {\n\n    @Override public void received(Consumer<T> consumer, Message<T> msg) {\n      try {\n        final byte[] serialized = msg.getData();\n        metrics.incrementMessages();\n        metrics.incrementBytes(serialized.length);\n\n        if (serialized.length == 0) return; // lenient on empty messages\n\n        collector.acceptSpans(serialized, NOOP);\n        consumer.acknowledgeAsync(msg);\n      } catch (Throwable th) {\n        metrics.incrementMessagesDropped();\n        LOG.error(\"Pulsar Span Consumer failed to process the message.\", th);\n        consumer.negativeAcknowledge(msg);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/pulsar/src/test/java/zipkin2/collector/pulsar/ITPulsarCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage zipkin2.collector.pulsar;\n\nimport org.apache.pulsar.client.api.Producer;\nimport org.apache.pulsar.client.api.PulsarClient;\nimport org.apache.pulsar.client.api.PulsarClientException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.TestObjects.UTF_8;\nimport static zipkin2.codec.SpanBytesEncoder.PROTO3;\nimport static zipkin2.codec.SpanBytesEncoder.THRIFT;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Timeout(60)\n@Tag(\"docker\")\npublic class ITPulsarCollector {\n\n  @RegisterExtension\n  static PulsarExtension pulsar = new PulsarExtension();\n  List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1]);\n\n  InMemoryCollectorMetrics metrics = new InMemoryCollectorMetrics();\n  InMemoryCollectorMetrics pulsarMetrics = metrics.forTransport(\"pulsar\");\n  CopyOnWriteArraySet<Thread> threadsProvidingSpans = new CopyOnWriteArraySet<>();\n  LinkedBlockingQueue<List<Span>> receivedSpans = new LinkedBlockingQueue<>();\n  SpanConsumer consumer;\n  PulsarClient pulsarClient;\n  PulsarCollector collector;\n  String testName;\n\n  @BeforeEach void start(TestInfo testInfo) throws PulsarClientException {\n    Optional<Method> testMethod = testInfo.getTestMethod();\n    if (testMethod.isPresent()) {\n      this.testName = testMethod.get().getName();\n    }\n    metrics.clear();\n    threadsProvidingSpans.clear();\n    receivedSpans.clear();\n    pulsarMetrics.clear();\n    consumer = (spans) -> {\n      threadsProvidingSpans.add(Thread.currentThread());\n      receivedSpans.add(spans);\n      return Call.create(null);\n    };\n    pulsarClient = PulsarClient.builder().serviceUrl(pulsar.serviceUrl()).build();\n    collector = builder().build().start();\n  }\n\n  PulsarCollector.Builder builder() {\n    return pulsar.newCollectorBuilder(testName)\n        .storage(buildStorage(consumer))\n        .metrics(metrics)\n        .subscriptionName(testName)\n        .topic(testName);\n  }\n\n  @AfterEach void tearDown() throws PulsarClientException {\n    pulsarClient.close();\n  }\n\n  @Test void checkPasses() {\n    assertThat(collector.check().ok()).isTrue();\n  }\n\n  @Test void startFailsWithInvalidServiceUrl() {\n    Throwable exception = assertThrows(RuntimeException.class, () -> {\n      collector = builder().serviceUrl(\"@zixin\").build();\n      collector.start();\n    });\n    assertThat(exception.getMessage()).contains(\"Pulsar client creation failed\");\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() throws IOException {\n    try (PulsarCollector collector = builder().build()) {\n      assertThat(collector).hasToString(String.format(\n          \"PulsarCollector{clientProps={serviceUrl=%s}, consumerProps={subscriptionName=%s}, topic=%s}\",\n          pulsar.serviceUrl(),\n          testName,\n          testName\n      ));\n    }\n  }\n\n  /** Ensures list encoding works: a json encoded list of spans */\n  @Test void messageWithMultipleSpans_json() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.JSON_V1);\n  }\n\n  /** Ensures list encoding works: a version 2 json list of spans */\n  @Test void messageWithMultipleSpans_json2() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.JSON_V2);\n  }\n\n  /** Ensures list encoding works: proto3 ListOfSpans */\n  @Test void messageWithMultipleSpans_proto3() throws Exception {\n    messageWithMultipleSpans(SpanBytesEncoder.PROTO3);\n  }\n\n  /** Ensures list encoding works: a TBinaryProtocol encoded list of spans */\n  @Test void messageWithMultipleSpans_thrift() throws Exception {\n    messageWithMultipleSpans(THRIFT);\n  }\n\n  /** Ensures malformed spans don't hang the collector */\n  @Test void skipsMalformedData() throws Exception {\n    byte[] malformed1 = \"[\\\"='\".getBytes(UTF_8); // screwed up json\n    byte[] malformed2 = \"malformed\".getBytes(UTF_8);\n    pushMessage(collector.topic, THRIFT.encodeList(spans));\n    pushMessage(collector.topic, new byte[0]);\n    pushMessage(collector.topic, malformed1);\n    pushMessage(collector.topic, malformed2);\n    pushMessage(collector.topic, THRIFT.encodeList(spans));\n\n    Thread.sleep(1000);\n\n    assertThat(pulsarMetrics.messages()).isEqualTo(5);\n    assertThat(pulsarMetrics.messagesDropped()).isEqualTo(2); // only malformed, not empty\n    assertThat(pulsarMetrics.bytes()).isEqualTo(\n        THRIFT.encodeList(spans).length * 2 + malformed1.length + malformed2.length);\n    assertThat(pulsarMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(pulsarMetrics.spansDropped()).isZero();\n  }\n\n  /** Guards against errors that leak from storage, such as InvalidQueryException */\n  @Test void skipsOnSpanStorageException() throws Exception {\n    collector.close();\n\n    AtomicInteger counter = new AtomicInteger();\n    consumer = (input) -> new Call.Base<>() {\n      @Override protected Void doExecute() {\n        throw new AssertionError();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        if (counter.getAndIncrement() == 1) {\n          callback.onError(new RuntimeException(\"storage fell over\"));\n        } else {\n          receivedSpans.add(spans);\n          callback.onSuccess(null);\n        }\n      }\n\n      @Override public Call<Void> clone() {\n        throw new AssertionError();\n      }\n    };\n\n    collector = builder().storage(buildStorage(consumer)).build().start();\n\n    pushMessage(collector.topic, PROTO3.encodeList(spans));\n    pushMessage(collector.topic, PROTO3.encodeList(spans)); // tossed on error\n    pushMessage(collector.topic, PROTO3.encodeList(spans));\n\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    // the only way we could read this, is if the malformed span was skipped.\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n\n    assertThat(pulsarMetrics.messages()).isEqualTo(3);\n    assertThat(pulsarMetrics.messagesDropped()).isZero(); // storage failure not message failure\n    assertThat(pulsarMetrics.bytes()).isEqualTo(PROTO3.encodeList(spans).length * 3);\n    assertThat(pulsarMetrics.spans()).isEqualTo(spans.size() * 3);\n    assertThat(pulsarMetrics.spansDropped()).isEqualTo(spans.size()); // only one dropped\n  }\n\n  @Test void messagesDistributedAcrossMultipleThreadsSuccessfully() throws Exception {\n    collector.close();\n\n    CountDownLatch latch = new CountDownLatch(2);\n    collector = builder().concurrency(2).storage(buildStorage((spans) -> {\n      latch.countDown();\n      try {\n        latch.await(); // await the other one as this proves 2 threads are in use\n      } catch (InterruptedException e) {\n        throw new AssertionError(e);\n      }\n      return consumer.accept(spans);\n    })).build().start();\n\n    pushMessage(collector.topic, new byte[]{}); // empty bodies don't go to storage\n    pushMessage(collector.topic, PROTO3.encodeList(spans));\n    pushMessage(collector.topic, PROTO3.encodeList(spans));\n\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    latch.countDown();\n    assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n\n    assertThat(threadsProvidingSpans).hasSize(2);\n\n    assertThat(pulsarMetrics.messages()).isEqualTo(3); // 2 + empty body for warmup\n    assertThat(pulsarMetrics.messagesDropped()).isZero();\n    assertThat(pulsarMetrics.bytes()).isEqualTo(PROTO3.encodeList(spans).length * 2);\n    assertThat(pulsarMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(pulsarMetrics.spansDropped()).isZero();\n  }\n\n\n  private void messageWithMultipleSpans(SpanBytesEncoder encoder) throws Exception {\n    byte[] message = encoder.encodeList(spans);\n    pushMessage(collector.topic, message);\n\n    assertThat(receivedSpans.take()).containsAll(spans);\n    assertThat(pulsarMetrics.messages()).isEqualTo(1);\n    assertThat(pulsarMetrics.messagesDropped()).isZero();\n    assertThat(pulsarMetrics.bytes()).isEqualTo(message.length);\n    assertThat(pulsarMetrics.spans()).isEqualTo(spans.size());\n    assertThat(pulsarMetrics.spansDropped()).isZero();\n  }\n\n  private void pushMessage(String topic, byte[] message) {\n    try (Producer<byte[]> producer = pulsarClient.newProducer().topic(topic).create()) {\n      producer.newMessage().value(message).send();\n    } catch (PulsarClientException e) {\n      throw new RuntimeException(\"Unable to send message to Pulsar\", e);\n    }\n  }\n\n\n  static StorageComponent buildStorage(final SpanConsumer spanConsumer) {\n    return new ForwardingStorageComponent() {\n      @Override protected StorageComponent delegate() {\n        throw new AssertionError();\n      }\n\n      @Override public SpanConsumer spanConsumer() {\n        return spanConsumer;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/pulsar/src/test/java/zipkin2/collector/pulsar/PulsarExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.pulsar;\n\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.HttpWaitStrategy;\n\nimport java.time.Duration;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\npublic class PulsarExtension implements BeforeAllCallback, AfterAllCallback {\n\n  static final Logger LOGGER = LoggerFactory.getLogger(PulsarExtension.class);\n  static final int BROKER_PORT = 6650;\n  static final int BROKER_HTTP_PORT = 8080;\n\n  final PulsarContainer container = new PulsarContainer();\n\n  @Override public void beforeAll(ExtensionContext context) throws Exception {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using serviceUrl {}\", serviceUrl());\n  }\n\n  String serviceUrl() {\n    return \"pulsar://\" + container.getHost() + \":\" + container.getMappedPort(BROKER_PORT);\n  }\n\n  @Override public void afterAll(ExtensionContext context) throws Exception {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.stop();\n  }\n\n  PulsarCollector.Builder newCollectorBuilder(String topic) {\n    return PulsarCollector.builder()\n        .topic(topic)\n        .subscriptionName(\"zipkin-subscription\")\n        .serviceUrl(serviceUrl());\n  }\n\n  static final class PulsarContainer extends GenericContainer<PulsarContainer> {\n    PulsarContainer() {\n      super(parse(\"ghcr.io/openzipkin/zipkin-pulsar:3.4.3\"));\n      withExposedPorts(BROKER_PORT, BROKER_HTTP_PORT);\n      String cmd = \"/pulsar/bin/apply-config-from-env.py /pulsar/conf/standalone.conf \" +\n          \"&& bin/pulsar standalone \" +\n          \"--no-functions-worker -nss\";\n      withEnv(\"PULSAR_MEM\", \"-Xms512m -Xmx512m -XX:MaxDirectMemorySize=1g\"); // limit memory usage\n      waitStrategy = new HttpWaitStrategy()\n          .forPort(BROKER_HTTP_PORT)\n          .forStatusCode(200)\n          .forPath(\"/admin/v2/clusters\")\n          .withStartupTimeout(Duration.ofSeconds(120));\n      withCommand(\"/bin/bash\", \"-c\", cmd);\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/pulsar/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\norg.slf4j.simpleLogger.log.com.github.charithe.pulsar=info\norg.slf4j.simpleLogger.log.zipkin2.collector.pulsar=debug\n# uncomment to include pulsar consumer configuration in test logs\n#logger.org.apache.pulsar.clients.level=info\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/README.md",
    "content": "# collector-rabbitmq\n\n## RabbitMQCollector\nThis collector consumes a RabbitMQ queue for messages that contain a list of spans.\nIts only dependencies besides Zipkin core are the `slf4j-api` and the [RabbitMQ Java Client](https://github.com/rabbitmq/rabbitmq-java-client).\n\n### Configuration\n\nThe following configuration can be set for the RabbitMQ Collector.\n\nProperty | Environment Variable | Description\n--- | --- | ---\n`zipkin.collector.rabbitmq.concurrency` | `RABBIT_CONCURRENCY` | Number of concurrent consumers. Defaults to `1`\n`zipkin.collector.rabbitmq.connection-timeout` | `RABBIT_CONNECTION_TIMEOUT` | Milliseconds to wait establishing a connection. Defaults to `60000` (1 minute)\n`zipkin.collector.rabbitmq.queue` | `RABBIT_QUEUE` | Queue from which to collect span messages. Defaults to `zipkin`\n`zipkin.collector.rabbitmq.uri` | `RABBIT_URI` | [RabbitMQ URI spec](https://www.rabbitmq.com/uri-spec.html)-compliant URI, ex. `amqp://user:pass@host:10000/vhost`\n\nIf the URI is set, the following properties will be ignored.\n\nProperty | Environment Variable | Description\n--- | --- | ---\n`zipkin.collector.rabbitmq.addresses` | `RABBIT_ADDRESSES` | Comma-separated list of RabbitMQ addresses, ex. `localhost:5672,localhost:5673`\n`zipkin.collector.rabbitmq.password` | `RABBIT_PASSWORD`| Password to use when connecting to RabbitMQ. Defaults to `guest`\n`zipkin.collector.rabbitmq.username` | `RABBIT_USER` | Username to use when connecting to RabbitMQ. Defaults to `guest`\n`zipkin.collector.rabbitmq.virtual-host` | `RABBIT_VIRTUAL_HOST` | RabbitMQ virtual host to use. Defaults to `/`\n`zipkin.collector.rabbitmq.use-ssl` | `RABBIT_USE_SSL` | Set to `true` to use SSL when connecting to RabbitMQ\n\n### Caveats\n\nThe configured queue will be idempotently declared as a durable queue.\n\nThis collector uses one connection to RabbitMQ, with the configured `concurrency` number of threads\neach using one channel to consume messages.\n\nConsumption is done with `autoAck` on, so messages that fail to process successfully are not retried.\n\n## Encoding spans into RabbitMQ messages\nThe message's body should be the bytes of an encoded list of spans.\n\n### JSON\nA list of Spans in JSON. The first character must be '[' (decimal 91).\n\n`SpanBytesEncoder.JSON_V2.encodeList(spans)` performs the correct JSON encoding.\n\n## Local testing\n\nThe following assumes you are running an instance of RabbitMQ locally on the default port (5672).\nYou can download and install RabbitMQ following [instructions available here](https://www.rabbitmq.com/download.html).\nWith the [RabbitMQ Management CLI](https://www.rabbitmq.com/management-cli.html) you can easily publish\none-off spans to RabbitMQ to be collected by this collector.\n\n1. Start RabbitMQ server\n2. Start Zipkin server\n```bash\n$ RABBIT_ADDRESSES=localhost java -jar zipkin.jar\n```\n3. Save an array of spans to a file like `sample-spans.json`\n```json\n[{\"traceId\":\"9032b04972e475c5\",\"id\":\"9032b04972e475c5\",\"kind\":\"SERVER\",\"name\":\"get\",\"timestamp\":1505990621526000,\"duration\":612898,\"localEndpoint\":{\"serviceName\":\"brave-webmvc-example\",\"ipv4\":\"192.168.1.113\"},\"remoteEndpoint\":{\"serviceName\":\"\",\"ipv4\":\"127.0.0.1\",\"port\":60149},\"tags\":{\"error\":\"500 Internal Server Error\",\"http.path\":\"/a\"}}]\n```\n4. Publish them using the CLI\n\nIf you have an AMQP package like `rabbitmq-c-utils` or `simple-amqp-client`:\n```bash\n$ amqp-publish -r zipkin < sample-spans.json\n```\n\nOr, if your rabbitmq host is running management extensions:\n```bash\n$ rabbitmqadmin publish exchange=amq.default routing_key=zipkin < sample-spans.json\n```\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector-rabbitmq</artifactId>\n  <name>Collector: RabbitMQ</name>\n  <description>Zipkin span collector for RabbitMQ transport</description>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n    <amqp-client.version>5.25.0</amqp-client.version>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>com.rabbitmq</groupId>\n      <artifactId>amqp-client</artifactId>\n      <version>${amqp-client.version}</version>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/src/main/java/zipkin2/collector/rabbitmq/RabbitMQCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.rabbitmq;\n\nimport com.rabbitmq.client.AMQP.BasicProperties;\nimport com.rabbitmq.client.Address;\nimport com.rabbitmq.client.Channel;\nimport com.rabbitmq.client.Connection;\nimport com.rabbitmq.client.ConnectionFactory;\nimport com.rabbitmq.client.DefaultConsumer;\nimport com.rabbitmq.client.Envelope;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicReference;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorComponent;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.StorageComponent;\n\n/** This collector consumes encoded binary messages from a RabbitMQ queue. */\npublic final class RabbitMQCollector extends CollectorComponent {\n  static final Callback<Void> NOOP = new Callback<Void>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n    }\n  };\n\n  public static Builder builder() {\n    return new Builder();\n  }\n\n  /** Configuration including defaults needed to consume spans from a RabbitMQ queue. */\n  public static final class Builder extends CollectorComponent.Builder {\n    Collector.Builder delegate = Collector.newBuilder(RabbitMQCollector.class);\n    CollectorMetrics metrics = CollectorMetrics.NOOP_METRICS;\n    String queue = \"zipkin\";\n    ConnectionFactory connectionFactory = new ConnectionFactory();\n    Address[] addresses;\n    int concurrency = 1;\n\n    @Override\n    public Builder storage(StorageComponent storage) {\n      this.delegate.storage(storage);\n      return this;\n    }\n\n    @Override\n    public Builder sampler(CollectorSampler sampler) {\n      this.delegate.sampler(sampler);\n      return this;\n    }\n\n    @Override\n    public Builder metrics(CollectorMetrics metrics) {\n      if (metrics == null) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics.forTransport(\"rabbitmq\");\n      this.delegate.metrics(this.metrics);\n      return this;\n    }\n\n    public Builder addresses(List<String> addresses) {\n      this.addresses = convertAddresses(addresses);\n      return this;\n    }\n\n    public Builder concurrency(int concurrency) {\n      this.concurrency = concurrency;\n      return this;\n    }\n\n    public Builder connectionFactory(ConnectionFactory connectionFactory) {\n      if (connectionFactory == null) throw new NullPointerException(\"connectionFactory == null\");\n      this.connectionFactory = connectionFactory;\n      return this;\n    }\n\n    /** Queue zipkin spans will be consumed from. Defaults to \"zipkin-spans\". */\n    public Builder queue(String queue) {\n      if (queue == null) throw new NullPointerException(\"queue == null\");\n      this.queue = queue;\n      return this;\n    }\n\n    @Override\n    public RabbitMQCollector build() {\n      return new RabbitMQCollector(this);\n    }\n  }\n\n  final String queue;\n  final LazyInit connection;\n\n  RabbitMQCollector(Builder builder) {\n    this.queue = builder.queue;\n    this.connection = new LazyInit(builder);\n  }\n\n  @Override\n  public RabbitMQCollector start() {\n    connection.get();\n    return this;\n  }\n\n  @Override\n  public CheckResult check() {\n    try {\n      start();\n      CheckResult failure = connection.failure.get();\n      if (failure != null) return failure;\n      return CheckResult.OK;\n    } catch (Throwable e) {\n      Call.propagateIfFatal(e);\n      return CheckResult.failed(e);\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    connection.close();\n  }\n\n  @Override public final String toString() {\n    return \"RabbitMQCollector{addresses=\"\n      + Arrays.toString(connection.builder.addresses)\n      + \", queue=\"\n      + queue\n      + \"}\";\n  }\n\n  /** Lazy creates a connection and a queue before starting consumers */\n  static final class LazyInit {\n    final Builder builder;\n    final AtomicReference<CheckResult> failure = new AtomicReference<>();\n    volatile Connection connection;\n\n    // TODO: bad idea to lazy reference properties from a mutable builder\n    // copy them here and then pass this to the KafkaCollectorWorker ctor instead\n    LazyInit(Builder builder) {\n      this.builder = builder;\n    }\n\n    Connection get() {\n      if (connection == null) {\n        synchronized (this) {\n          if (connection == null) {\n            connection = compute();\n          }\n        }\n      }\n      return connection;\n    }\n\n    void close() throws IOException {\n      Connection maybeConnection = connection;\n      if (maybeConnection != null) maybeConnection.close();\n    }\n\n    Connection compute() {\n      Connection connection;\n      try {\n        connection =\n          (builder.addresses == null)\n            ? builder.connectionFactory.newConnection()\n            : builder.connectionFactory.newConnection(builder.addresses);\n        declareQueueIfMissing(connection);\n      } catch (IOException e) {\n        throw new UncheckedIOException(\n          \"Unable to establish connection to RabbitMQ server: \" + e.getMessage(), e);\n      } catch (TimeoutException e) {\n        throw new RuntimeException(\n          \"Timeout establishing connection to RabbitMQ server: \" + e.getMessage(), e);\n      }\n      Collector collector = builder.delegate.build();\n      CollectorMetrics metrics = builder.metrics;\n\n      for (int i = 0; i < builder.concurrency; i++) {\n        String consumerTag = \"zipkin-rabbitmq.\" + i;\n        try {\n          // this sets up a channel for each consumer thread.\n          // We don't track channels, as the connection will close its channels implicitly\n          Channel channel = connection.createChannel();\n          RabbitMQSpanConsumer consumer = new RabbitMQSpanConsumer(channel, collector, metrics);\n          channel.basicConsume(builder.queue, true, consumerTag, consumer);\n        } catch (IOException e) {\n          throw new IllegalStateException(\"Failed to start RabbitMQ consumer \" + consumerTag, e);\n        }\n      }\n      return connection;\n    }\n\n    private void declareQueueIfMissing(Connection connection) throws IOException, TimeoutException {\n      Channel channel = connection.createChannel();\n      try {\n        // check if queue already exists\n        channel.queueDeclarePassive(builder.queue);\n        channel.close();\n      } catch (IOException maybeQueueDoesNotExist) {\n        Throwable cause = maybeQueueDoesNotExist.getCause();\n        if (cause != null && cause.getMessage().contains(\"NOT_FOUND\")) {\n          channel = connection.createChannel();\n          channel.queueDeclare(builder.queue, true, false, false, null);\n          channel.close();\n        } else {\n          throw maybeQueueDoesNotExist;\n        }\n      }\n    }\n  }\n\n  /**\n   * Consumes spans from messages on a RabbitMQ queue. Malformed messages will be discarded. Errors\n   * in the storage component will similarly be ignored, with no retry of the message.\n   */\n  static class RabbitMQSpanConsumer extends DefaultConsumer {\n    final Collector collector;\n    final CollectorMetrics metrics;\n\n    RabbitMQSpanConsumer(Channel channel, Collector collector, CollectorMetrics metrics) {\n      super(channel);\n      this.collector = collector;\n      this.metrics = metrics;\n    }\n\n    @Override\n    public void handleDelivery(String tag, Envelope envelope, BasicProperties props, byte[] body) {\n      metrics.incrementMessages();\n      metrics.incrementBytes(body.length);\n\n      if (body.length == 0) return; // lenient on empty messages\n\n      collector.acceptSpans(body, NOOP);\n    }\n  }\n\n  static Address[] convertAddresses(List<String> addresses) {\n    Address[] addressArray = new Address[addresses.size()];\n    for (int i = 0; i < addresses.size(); i++) {\n      String[] splitAddress = addresses.get(i).split(\":\", 100);\n      String host = splitAddress[0];\n      int port = -1;\n      try {\n        if (splitAddress.length == 2) port = Integer.parseInt(splitAddress[1]);\n      } catch (NumberFormatException ignore) {\n        // EmptyCatch ignored\n      }\n      addressArray[i] = (port > 0) ? new Address(host, port) : new Address(host);\n    }\n    return addressArray;\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/src/test/java/zipkin2/collector/rabbitmq/ITRabbitMQCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.rabbitmq;\n\nimport com.rabbitmq.client.Channel;\nimport com.rabbitmq.client.Connection;\nimport com.rabbitmq.client.ConnectionFactory;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.TestObjects.UTF_8;\nimport static zipkin2.codec.SpanBytesEncoder.THRIFT;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Timeout(60)\n@Tag(\"docker\")\nclass ITRabbitMQCollector {\n  @RegisterExtension static RabbitMQExtension rabbit = new RabbitMQExtension();\n\n  List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1]);\n\n  InMemoryCollectorMetrics metrics = new InMemoryCollectorMetrics();\n  InMemoryCollectorMetrics rabbitmqMetrics = metrics.forTransport(\"rabbitmq\");\n\n  CopyOnWriteArraySet<Thread> threadsProvidingSpans = new CopyOnWriteArraySet<>();\n  LinkedBlockingQueue<List<Span>> receivedSpans = new LinkedBlockingQueue<>();\n  SpanConsumer consumer = (spans) -> {\n    threadsProvidingSpans.add(Thread.currentThread());\n    receivedSpans.add(spans);\n    return Call.create(null);\n  };\n  Connection connection;\n\n  @BeforeEach void setup() throws Exception {\n    metrics.clear();\n    ConnectionFactory factory = new ConnectionFactory();\n    factory.setHost(rabbit.host());\n    factory.setPort(rabbit.port());\n    connection = factory.newConnection();\n  }\n\n  @AfterEach void tearDown() throws Exception {\n    if (connection != null) connection.close();\n  }\n\n  @Test void checkPasses() throws Exception {\n    try (RabbitMQCollector collector = builder(\"check_passes\").build()) {\n      assertThat(collector.check().ok()).isTrue();\n    }\n  }\n\n  /** Ensures list encoding works: a TBinaryProtocol encoded list of spans */\n  @Test void messageWithMultipleSpans_thrift() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_thrift\"), THRIFT);\n  }\n\n  /** Ensures list encoding works: a json encoded list of spans */\n  @Test void messageWithMultipleSpans_json() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_json\"), SpanBytesEncoder.JSON_V1);\n  }\n\n  /** Ensures list encoding works: a version 2 json list of spans */\n  @Test void messageWithMultipleSpans_json2() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_json2\"), SpanBytesEncoder.JSON_V2);\n  }\n\n  /** Ensures list encoding works: proto3 ListOfSpans */\n  @Test void messageWithMultipleSpans_proto3() throws Exception {\n    messageWithMultipleSpans(builder(\"multiple_spans_proto3\"), SpanBytesEncoder.PROTO3);\n  }\n\n  void messageWithMultipleSpans(RabbitMQCollector.Builder builder, SpanBytesEncoder encoder)\n    throws Exception {\n    byte[] message = encoder.encodeList(spans);\n\n    produceSpans(message, builder.queue);\n\n    try (RabbitMQCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsAll(spans);\n    }\n\n    assertThat(rabbitmqMetrics.messages()).isEqualTo(1);\n    assertThat(rabbitmqMetrics.messagesDropped()).isZero();\n    assertThat(rabbitmqMetrics.bytes()).isEqualTo(message.length);\n    assertThat(rabbitmqMetrics.spans()).isEqualTo(spans.size());\n    assertThat(rabbitmqMetrics.spansDropped()).isZero();\n  }\n\n  /** Ensures malformed spans don't hang the collector */\n  @Test void skipsMalformedData() throws Exception {\n    RabbitMQCollector.Builder builder = builder(\"decoder_exception\");\n\n    byte[] malformed1 = \"[\\\"='\".getBytes(UTF_8); // screwed up json\n    byte[] malformed2 = \"malformed\".getBytes(UTF_8);\n    produceSpans(THRIFT.encodeList(spans), builder.queue);\n    produceSpans(new byte[0], builder.queue);\n    produceSpans(malformed1, builder.queue);\n    produceSpans(malformed2, builder.queue);\n    produceSpans(THRIFT.encodeList(spans), builder.queue);\n\n    try (RabbitMQCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n      // the only way we could read this, is if the malformed spans were skipped.\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    }\n\n    assertThat(rabbitmqMetrics.messages()).isEqualTo(5);\n    assertThat(rabbitmqMetrics.messagesDropped()).isEqualTo(2); // only malformed, not empty\n    assertThat(rabbitmqMetrics.bytes())\n      .isEqualTo(THRIFT.encodeList(spans).length * 2 + malformed1.length + malformed2.length);\n    assertThat(rabbitmqMetrics.spans()).isEqualTo(spans.size() * 2);\n    assertThat(rabbitmqMetrics.spansDropped()).isZero();\n  }\n\n  @Test void startsWhenConfiguredQueueDoesntExist() throws Exception {\n    try (RabbitMQCollector collector = builder(\"ignored\").queue(\"zipkin-test2\").build()) {\n      assertThat(collector.check().ok()).isTrue();\n    }\n  }\n\n  /** Guards against errors that leak from storage, such as InvalidQueryException */\n  @Test void skipsOnSpanStorageException() throws Exception {\n    AtomicInteger counter = new AtomicInteger();\n    consumer = (input) -> new Call.Base<Void>() {\n      @Override protected Void doExecute() {\n        throw new AssertionError();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        if (counter.getAndIncrement() == 1) {\n          callback.onError(new RuntimeException(\"storage fell over\"));\n        } else {\n          receivedSpans.add(spans);\n          callback.onSuccess(null);\n        }\n      }\n\n      @Override public Call<Void> clone() {\n        throw new AssertionError();\n      }\n    };\n    final StorageComponent storage = buildStorage(consumer);\n    RabbitMQCollector.Builder builder = builder(\"storage_exception\").storage(storage);\n\n    produceSpans(THRIFT.encodeList(spans), builder.queue);\n    produceSpans(THRIFT.encodeList(spans), builder.queue); // tossed on error\n    produceSpans(THRIFT.encodeList(spans), builder.queue);\n\n    try (RabbitMQCollector collector = builder.build()) {\n      collector.start();\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n      // the only way we could read this, is if the malformed span was skipped.\n      assertThat(receivedSpans.take()).containsExactlyElementsOf(spans);\n    }\n\n    assertThat(rabbitmqMetrics.messages()).isEqualTo(3);\n    // storage failure isn't a message failure\n    assertThat(rabbitmqMetrics.messagesDropped()).isZero();\n    assertThat(rabbitmqMetrics.bytes()).isEqualTo(THRIFT.encodeList(spans).length * 3);\n    assertThat(rabbitmqMetrics.spans()).isEqualTo(spans.size() * 3);\n    assertThat(rabbitmqMetrics.spansDropped()).isEqualTo(spans.size()); // only one dropped\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() throws Exception {\n    try (RabbitMQCollector collector = builder(\"bugs bunny\").build()) {\n      collector.start();\n\n      assertThat(collector).hasToString(\n        \"RabbitMQCollector{addresses=[%s:%s], queue=%s}\".formatted(rabbit.host(),\n          rabbit.port(), \"bugs bunny\")\n      );\n    }\n  }\n\n  void produceSpans(byte[] spans, String queue) throws Exception {\n    Channel channel = null;\n    try {\n      channel = connection.createChannel();\n      channel.basicPublish(\"\", queue, null, spans);\n    } finally {\n      if (channel != null) channel.close();\n    }\n  }\n\n  RabbitMQCollector.Builder builder(String queue) {\n    return rabbit.newCollectorBuilder(queue)\n      .metrics(metrics)\n      .storage(buildStorage(consumer));\n  }\n\n  static StorageComponent buildStorage(final SpanConsumer spanConsumer) {\n    return new ForwardingStorageComponent() {\n      @Override protected StorageComponent delegate() {\n        throw new AssertionError();\n      }\n\n      @Override public SpanConsumer spanConsumer() {\n        return spanConsumer;\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/src/test/java/zipkin2/collector/rabbitmq/RabbitMQCollectorTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.rabbitmq;\n\nimport com.rabbitmq.client.ConnectionFactory;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass RabbitMQCollectorTest {\n\n  RabbitMQCollector collector;\n\n  @BeforeEach void before() {\n    ConnectionFactory connectionFactory = new ConnectionFactory();\n    connectionFactory.setConnectionTimeout(100);\n    // We can be pretty certain RabbitMQ isn't running on localhost port 80\n    collector = RabbitMQCollector.builder()\n        .connectionFactory(connectionFactory).addresses(List.of(\"localhost:80\")).build();\n  }\n\n  @Test void checkFalseWhenRabbitMQIsDown() {\n    CheckResult check = collector.check();\n    assertThat(check.ok()).isFalse();\n    assertThat(check.error()).isInstanceOf(UncheckedIOException.class);\n  }\n\n  @Test void startFailsWhenRabbitMQIsDown() {\n    // NOTE.. This is probably not good as it can crash on transient failure..\n    assertThatThrownBy(collector::start)\n        .isInstanceOf(UncheckedIOException.class)\n        .hasMessageStartingWith(\"Unable to establish connection to RabbitMQ server\");\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    assertThat(collector).hasToString(\n        \"RabbitMQCollector{addresses=[localhost:80], queue=zipkin}\"\n    );\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/src/test/java/zipkin2/collector/rabbitmq/RabbitMQExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.rabbitmq;\n\nimport java.time.Duration;\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.opentest4j.TestAbortedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.Container.ExecResult;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static java.util.Arrays.asList;\nimport static org.testcontainers.utility.DockerImageName.parse;\nimport static zipkin2.Call.propagateIfFatal;\n\nclass RabbitMQExtension implements BeforeAllCallback, AfterAllCallback {\n  static final Logger LOGGER = LoggerFactory.getLogger(RabbitMQExtension.class);\n  static final int RABBIT_PORT = 5672;\n\n  RabbitMQContainer container = new RabbitMQContainer();\n\n  @Override public void beforeAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using hostPort {}:{}\", host(), port());\n  }\n\n  @Override public void afterAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.stop();\n  }\n\n  RabbitMQCollector.Builder newCollectorBuilder(String queue) {\n    declareQueue(queue);\n    return RabbitMQCollector.builder().queue(queue).addresses(asList(host() + \":\" + port()));\n  }\n\n  void declareQueue(String queue) {\n    ExecResult result;\n    try {\n      result = container.execInContainer(\"amqp-declare-queue\", \"-q\", queue);\n    } catch (Throwable e) {\n      propagateIfFatal(e);\n      throw new TestAbortedException(\n        \"Couldn't declare queue \" + queue + \": \" + e.getMessage(), e);\n    }\n    if (result.getExitCode() != 0) {\n      throw new TestAbortedException(\"Couldn't declare queue \" + queue + \": \" + result);\n    }\n  }\n\n  String host() {\n    return container.getHost();\n  }\n\n  int port() {\n    return container.getMappedPort(RABBIT_PORT);\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class RabbitMQContainer extends GenericContainer<RabbitMQContainer> {\n    RabbitMQContainer() {\n      super(parse(\"ghcr.io/openzipkin/zipkin-rabbitmq:3.4.3\"));\n      withExposedPorts(RABBIT_PORT);\n      waitStrategy = Wait.forLogMessage(\".*Server startup complete.*\", 1);\n      withStartupTimeout(Duration.ofSeconds(60));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/rabbitmq/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# stop huge spam\norg.slf4j.simpleLogger.log.org.testcontainers.dockerclient=off\n"
  },
  {
    "path": "zipkin-collector/scribe/README.md",
    "content": "# collector-scribe\n\n## ScribeCollector\nThis collector accepts Scribe logs in a specified category. Each log\nentry is expected to contain a single span, which is TBinaryProtocol\nbig-endian, then base64 encoded. These spans are then pushed to storage.\n\n`zipkin2.collector.scribe.ScribeCollector.Builder` includes defaults that will\nlisten on port 9410, accept log entries in the category \"zipkin\"\n\n## Encoding\nThe scribe message is a TBinaryProtocol big-endian, then Base64 span.\nBase64 Basic and MIME schemes are supported.\n\nHere's what it looks like in pseudocode\n```\nserialized = writeTBinaryProtocol(span)\nencoded = base64(serialized)\n\nscribe.log(category = \"zipkin\", message = encoded)\n```\n"
  },
  {
    "path": "zipkin-collector/scribe/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-collector-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-collector-scribe</artifactId>\n  <name>Collector: Scribe (Legacy)</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n\n    <!-- error-prone doesn't like generated code -->\n    <errorprone.args>-XepDisableWarningsInGeneratedCode</errorprone.args>\n  </properties>\n\n  <!-- Avoid CVEs in armeria deps -->\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>io.netty</groupId>\n        <artifactId>netty-bom</artifactId>\n        <version>${netty.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-thrift0.18</artifactId>\n      <version>${armeria.version}</version>\n    </dependency>\n\n    <!-- for Generated annotations on Scribe types -->\n    <dependency>\n      <groupId>javax.annotation</groupId>\n      <artifactId>javax.annotation-api</artifactId>\n      <version>${javax-annotation-api.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-junit5</artifactId>\n      <version>${armeria.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.awaitility</groupId>\n      <artifactId>awaitility</artifactId>\n      <version>${awaitility.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/NettyScribeServer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport com.linecorp.armeria.common.CommonPools;\nimport com.linecorp.armeria.common.util.EventLoopGroups;\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.LengthFieldBasedFrameDecoder;\nimport java.net.InetSocketAddress;\n\nimport static zipkin2.Call.propagateIfFatal;\n\nfinal class NettyScribeServer {\n  final int port;\n  final ScribeSpanConsumer scribe;\n\n  volatile EventLoopGroup bossGroup;\n  volatile Channel channel;\n\n  NettyScribeServer(int port, ScribeSpanConsumer scribe) {\n    this.port = port;\n    this.scribe = scribe;\n  }\n\n  void start() {\n    bossGroup = EventLoopGroups.newEventLoopGroup(1);\n    EventLoopGroup workerGroup = CommonPools.workerGroup();\n\n    ServerBootstrap b = new ServerBootstrap();\n    try {\n      channel = b.group(bossGroup, workerGroup)\n        .channel(EventLoopGroups.serverChannelType(bossGroup))\n        .childHandler(new ChannelInitializer<SocketChannel>() {\n          @Override protected void initChannel(SocketChannel ch) {\n            ch.pipeline()\n              .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))\n              .addLast(new ScribeInboundHandler(scribe));\n          }\n        })\n        .bind(port)\n        .syncUninterruptibly()\n        .channel();\n    } catch (Throwable t) {\n      propagateIfFatal(t);\n      throw new RuntimeException(\"Could not start scribe server.\", t);\n    }\n  }\n\n  @SuppressWarnings(\"FutureReturnValueIgnored\")\n  void close() {\n    if (channel == null) return;\n    // TODO: chain these futures, and probably block a bit\n    // https://line-armeria.slack.com/archives/C1NGPBUH2/p1591167918430500\n    channel.close();\n    bossGroup.shutdownGracefully();\n  }\n\n  boolean isRunning() {\n    return channel != null && channel.isActive();\n  }\n\n  int port() {\n    if (channel == null) return 0;\n    return ((InetSocketAddress) channel.localAddress()).getPort();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/ScribeCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport zipkin2.CheckResult;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorComponent;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\n/**\n * This collector accepts Scribe logs in a specified category. Each log entry is expected to contain\n * a single span, which is TBinaryProtocol big-endian, then base64 encoded. These spans are chained\n * to an {@link SpanConsumer#accept asynchronous span consumer}.\n */\npublic final class ScribeCollector extends CollectorComponent {\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  /** Configuration including defaults needed to receive spans from a Scribe category. */\n  public static final class Builder extends CollectorComponent.Builder {\n    Collector.Builder delegate = Collector.newBuilder(ScribeCollector.class);\n    CollectorMetrics metrics = CollectorMetrics.NOOP_METRICS;\n    String category = \"zipkin\";\n    int port = 9410;\n\n    @Override public Builder storage(StorageComponent storage) {\n      delegate.storage(storage);\n      return this;\n    }\n\n    @Override public Builder metrics(CollectorMetrics metrics) {\n      if (metrics == null) throw new NullPointerException(\"metrics == null\");\n      this.metrics = metrics.forTransport(\"scribe\");\n      delegate.metrics(this.metrics);\n      return this;\n    }\n\n    @Override public Builder sampler(CollectorSampler sampler) {\n      delegate.sampler(sampler);\n      return this;\n    }\n\n    /** Category zipkin spans will be consumed from. Defaults to \"zipkin\" */\n    public Builder category(String category) {\n      if (category == null) throw new NullPointerException(\"category == null\");\n      this.category = category;\n      return this;\n    }\n\n    /** The port to listen on. Defaults to 9410 */\n    public Builder port(int port) {\n      this.port = port;\n      return this;\n    }\n\n    @Override public ScribeCollector build() {\n      return new ScribeCollector(this);\n    }\n  }\n\n  final NettyScribeServer server;\n\n  ScribeCollector(Builder builder) {\n    server = new NettyScribeServer(builder.port,\n      new ScribeSpanConsumer(builder.delegate.build(), builder.metrics, builder.category));\n  }\n\n  /** Will throw an exception if the {@link Builder#port(int) port} is already in use. */\n  @Override public ScribeCollector start() {\n    server.start();\n    return this;\n  }\n\n  @Override public CheckResult check() {\n    if (!server.isRunning()) {\n      return CheckResult.failed(new IllegalStateException(\"server not running\"));\n    }\n    return CheckResult.OK;\n  }\n\n  /** Returns zero until {@link #start()} was called. */\n  public int port() {\n    return server.port();\n  }\n\n  @Override public final String toString() {\n    return \"ScribeCollector{port=\" + port() + \", category=\" + server.scribe.category + \"}\";\n  }\n\n  @Override public void close() {\n    server.close();\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/ScribeInboundHandler.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.util.Exceptions;\nimport com.linecorp.armeria.common.util.SafeCloseable;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.ServiceRequestContextBuilder;\nimport com.linecorp.armeria.server.thrift.THttpService;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.channel.EventLoop;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport static zipkin2.Call.propagateIfFatal;\n\n@SuppressWarnings(\"FutureReturnValueIgnored\")\n// TODO: errorprone wants us to check futures before returning, but what would be a sensible check?\n// Say it is somehow canceled, would we take action? Would callback.onError() be redundant?\nfinal class ScribeInboundHandler extends ChannelInboundHandlerAdapter {\n\n  static final Logger logger = LoggerFactory.getLogger(ScribeInboundHandler.class);\n\n  // Headers mostly copied from https://github.com/apache/thrift/blob/master/lib/javame/src/org/apache/thrift/transport/THttpClient.java#L130\n  static final RequestHeaders THRIFT_HEADERS = RequestHeaders.builder(\n    HttpMethod.POST, \"/internal/zipkin-thriftrpc\")\n    .set(HttpHeaderNames.CONTENT_TYPE, \"application/x-thrift\")\n    .set(HttpHeaderNames.ACCEPT, \"application/x-thrift\")\n    .set(HttpHeaderNames.USER_AGENT, \"Zipkin/ScribeInboundHandler\")\n    .build();\n\n  final THttpService scribeService;\n\n  ScribeInboundHandler(ScribeSpanConsumer scribe) {\n    scribeService = THttpService.of(scribe);\n  }\n\n  Map<Integer, ByteBuf> pendingResponses = new HashMap<>();\n  int nextResponseIndex = 0;\n  int previouslySentResponseIndex = -1;\n\n  @Override public void channelRead(ChannelHandlerContext ctx, Object payload) {\n    assert payload instanceof ByteBuf;\n    HttpRequest request = HttpRequest.of(THRIFT_HEADERS, HttpData.wrap((ByteBuf) payload));\n    ServiceRequestContextBuilder requestContextBuilder = ServiceRequestContext.builder(request)\n      .service(scribeService)\n      .alloc(ctx.alloc());\n\n    if (ctx.executor() instanceof EventLoop) {\n      requestContextBuilder.eventLoop((EventLoop) ctx.executor());\n    }\n\n    ServiceRequestContext requestContext = requestContextBuilder.build();\n\n    final HttpResponse response;\n    try (SafeCloseable unused = requestContext.push()) {\n      response = HttpResponse.of(scribeService.serve(requestContext, request));\n    } catch (Throwable t) {\n      propagateIfFatal(t);\n      exceptionCaught(ctx, t);\n      return;\n    }\n\n    int responseIndex = nextResponseIndex++;\n\n    response.aggregateWithPooledObjects(ctx.executor(), ctx.alloc()).handle((msg, t) -> {\n      if (t != null) {\n        exceptionCaught(ctx, t);\n        return null;\n      }\n\n      try (HttpData content = msg.content()) {\n        ByteBuf returned = ctx.alloc().buffer(content.length() + 4);\n        returned.writeInt(content.length());\n        returned.writeBytes(content.byteBuf());\n        if (responseIndex == previouslySentResponseIndex + 1) {\n          ctx.writeAndFlush(returned);\n          previouslySentResponseIndex++;\n\n          flushResponses(ctx);\n        } else {\n          pendingResponses.put(responseIndex, returned);\n        }\n      }\n\n      return null;\n    });\n  }\n\n  @Override public void channelInactive(ChannelHandlerContext ctx) {\n    release();\n  }\n\n  @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n    Exceptions.logIfUnexpected(logger, ctx.channel(), cause);\n\n    release();\n    closeOnFlush(ctx.channel());\n  }\n\n  void flushResponses(ChannelHandlerContext ctx) {\n    while (!pendingResponses.isEmpty()) {\n      ByteBuf response = pendingResponses.remove(previouslySentResponseIndex + 1);\n      if (response == null) {\n        return;\n      }\n\n      ctx.writeAndFlush(response);\n      previouslySentResponseIndex++;\n    }\n  }\n\n  void release() {\n    pendingResponses.values().forEach(ByteBuf::release);\n    pendingResponses.clear();\n  }\n\n  /**\n   * Closes the specified channel after all queued write requests are flushed.\n   */\n  static void closeOnFlush(Channel ch) {\n    if (ch.isActive()) {\n      ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/ScribeSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport com.linecorp.armeria.common.CommonPools;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.List;\nimport org.apache.thrift.async.AsyncMethodCallback;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.scribe.generated.LogEntry;\nimport zipkin2.collector.scribe.generated.ResultCode;\nimport zipkin2.collector.scribe.generated.Scribe;\n\nfinal class ScribeSpanConsumer implements Scribe.AsyncIface {\n  final Collector collector;\n  final CollectorMetrics metrics;\n  final String category;\n\n  ScribeSpanConsumer(Collector collector, CollectorMetrics metrics, String category) {\n    this.collector = collector;\n    this.metrics = metrics;\n    this.category = category;\n  }\n\n  @Override\n  public void Log(List<LogEntry> messages, AsyncMethodCallback<ResultCode> resultHandler) {\n    metrics.incrementMessages();\n    List<Span> spans = new ArrayList<>();\n    int byteCount = 0;\n    try {\n      for (LogEntry logEntry : messages) {\n        if (!category.equals(logEntry.category)) continue;\n        byte[] bytes = logEntry.message.getBytes(StandardCharsets.ISO_8859_1);\n        bytes = Base64.getMimeDecoder().decode(bytes); // finagle-zipkin uses mime encoding\n        byteCount += bytes.length;\n        spans.add(SpanBytesDecoder.THRIFT.decodeOne(bytes));\n      }\n    } catch (RuntimeException e) {\n      metrics.incrementMessagesDropped();\n      resultHandler.onError(e);\n      return;\n    } finally {\n      metrics.incrementBytes(byteCount);\n    }\n\n    collector.accept(spans, new Callback<Void>() {\n      @Override public void onSuccess(Void value) {\n        resultHandler.onComplete(ResultCode.OK);\n      }\n\n      @Override public void onError(Throwable t) {\n        Exception error = t instanceof Exception e ? e : new RuntimeException(t);\n        resultHandler.onError(error);\n      }\n    // Collectors may not be asynchronous so switch to blocking executor here.\n    }, CommonPools.blockingTaskExecutor());\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/generated/LogEntry.java",
    "content": "/**\n * Autogenerated by Thrift Compiler (0.12.0)\n *\n * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING\n *  @generated\n */\npackage zipkin2.collector.scribe.generated;\n\n@SuppressWarnings({\"cast\", \"rawtypes\", \"serial\", \"unchecked\", \"unused\"})\n@javax.annotation.Generated(value = \"Autogenerated by Thrift Compiler (0.12.0)\", date = \"2019-05-07\")\npublic class LogEntry implements org.apache.thrift.TBase<LogEntry, LogEntry._Fields>, java.io.Serializable, Cloneable, Comparable<LogEntry> {\n  private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct(\"LogEntry\");\n\n  private static final org.apache.thrift.protocol.TField CATEGORY_FIELD_DESC = new org.apache.thrift.protocol.TField(\"category\", org.apache.thrift.protocol.TType.STRING, (short)1);\n  private static final org.apache.thrift.protocol.TField MESSAGE_FIELD_DESC = new org.apache.thrift.protocol.TField(\"message\", org.apache.thrift.protocol.TType.STRING, (short)2);\n\n  private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new LogEntryStandardSchemeFactory();\n  private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new LogEntryTupleSchemeFactory();\n\n  public @org.apache.thrift.annotation.Nullable java.lang.String category; // required\n  public @org.apache.thrift.annotation.Nullable java.lang.String message; // required\n\n  /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */\n  public enum _Fields implements org.apache.thrift.TFieldIdEnum {\n    CATEGORY((short)1, \"category\"),\n    MESSAGE((short)2, \"message\");\n\n    private static final java.util.Map<java.lang.String, _Fields> byName = new java.util.HashMap<java.lang.String, _Fields>();\n\n    static {\n      for (_Fields field : java.util.EnumSet.allOf(_Fields.class)) {\n        byName.put(field.getFieldName(), field);\n      }\n    }\n\n    /**\n     * Find the _Fields constant that matches fieldId, or null if its not found.\n     */\n    @org.apache.thrift.annotation.Nullable\n    public static _Fields findByThriftId(int fieldId) {\n      switch(fieldId) {\n        case 1: // CATEGORY\n          return CATEGORY;\n        case 2: // MESSAGE\n          return MESSAGE;\n        default:\n          return null;\n      }\n    }\n\n    /**\n     * Find the _Fields constant that matches fieldId, throwing an exception\n     * if it is not found.\n     */\n    public static _Fields findByThriftIdOrThrow(int fieldId) {\n      _Fields fields = findByThriftId(fieldId);\n      if (fields == null) throw new java.lang.IllegalArgumentException(\"Field \" + fieldId + \" doesn't exist!\");\n      return fields;\n    }\n\n    /**\n     * Find the _Fields constant that matches name, or null if its not found.\n     */\n    @org.apache.thrift.annotation.Nullable\n    public static _Fields findByName(java.lang.String name) {\n      return byName.get(name);\n    }\n\n    private final short _thriftId;\n    private final java.lang.String _fieldName;\n\n    _Fields(short thriftId, java.lang.String fieldName) {\n      _thriftId = thriftId;\n      _fieldName = fieldName;\n    }\n\n    public short getThriftFieldId() {\n      return _thriftId;\n    }\n\n    public java.lang.String getFieldName() {\n      return _fieldName;\n    }\n  }\n\n  // isset id assignments\n  public static final java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap;\n  static {\n    java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new java.util.EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class);\n    tmpMap.put(_Fields.CATEGORY, new org.apache.thrift.meta_data.FieldMetaData(\"category\", org.apache.thrift.TFieldRequirementType.DEFAULT, \n        new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING)));\n    tmpMap.put(_Fields.MESSAGE, new org.apache.thrift.meta_data.FieldMetaData(\"message\", org.apache.thrift.TFieldRequirementType.DEFAULT, \n        new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING)));\n    metaDataMap = java.util.Collections.unmodifiableMap(tmpMap);\n    org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(LogEntry.class, metaDataMap);\n  }\n\n  public LogEntry() {\n  }\n\n  public LogEntry(\n    java.lang.String category,\n    java.lang.String message)\n  {\n    this();\n    this.category = category;\n    this.message = message;\n  }\n\n  /**\n   * Performs a deep copy on <i>other</i>.\n   */\n  public LogEntry(LogEntry other) {\n    if (other.isSetCategory()) {\n      this.category = other.category;\n    }\n    if (other.isSetMessage()) {\n      this.message = other.message;\n    }\n  }\n\n  public LogEntry deepCopy() {\n    return new LogEntry(this);\n  }\n\n  @Override\n  public void clear() {\n    this.category = null;\n    this.message = null;\n  }\n\n  @org.apache.thrift.annotation.Nullable\n  public java.lang.String getCategory() {\n    return this.category;\n  }\n\n  public LogEntry setCategory(@org.apache.thrift.annotation.Nullable java.lang.String category) {\n    this.category = category;\n    return this;\n  }\n\n  public void unsetCategory() {\n    this.category = null;\n  }\n\n  /** Returns true if field category is set (has been assigned a value) and false otherwise */\n  public boolean isSetCategory() {\n    return this.category != null;\n  }\n\n  public void setCategoryIsSet(boolean value) {\n    if (!value) {\n      this.category = null;\n    }\n  }\n\n  @org.apache.thrift.annotation.Nullable\n  public java.lang.String getMessage() {\n    return this.message;\n  }\n\n  public LogEntry setMessage(@org.apache.thrift.annotation.Nullable java.lang.String message) {\n    this.message = message;\n    return this;\n  }\n\n  public void unsetMessage() {\n    this.message = null;\n  }\n\n  /** Returns true if field message is set (has been assigned a value) and false otherwise */\n  public boolean isSetMessage() {\n    return this.message != null;\n  }\n\n  public void setMessageIsSet(boolean value) {\n    if (!value) {\n      this.message = null;\n    }\n  }\n\n  public void setFieldValue(_Fields field, @org.apache.thrift.annotation.Nullable java.lang.Object value) {\n    switch (field) {\n    case CATEGORY:\n      if (value == null) {\n        unsetCategory();\n      } else {\n        setCategory((java.lang.String)value);\n      }\n      break;\n\n    case MESSAGE:\n      if (value == null) {\n        unsetMessage();\n      } else {\n        setMessage((java.lang.String)value);\n      }\n      break;\n\n    }\n  }\n\n  @org.apache.thrift.annotation.Nullable\n  public java.lang.Object getFieldValue(_Fields field) {\n    switch (field) {\n    case CATEGORY:\n      return getCategory();\n\n    case MESSAGE:\n      return getMessage();\n\n    }\n    throw new java.lang.IllegalStateException();\n  }\n\n  /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */\n  public boolean isSet(_Fields field) {\n    if (field == null) {\n      throw new java.lang.IllegalArgumentException();\n    }\n\n    switch (field) {\n    case CATEGORY:\n      return isSetCategory();\n    case MESSAGE:\n      return isSetMessage();\n    }\n    throw new java.lang.IllegalStateException();\n  }\n\n  @Override\n  public boolean equals(java.lang.Object that) {\n    if (that == null)\n      return false;\n    if (that instanceof LogEntry entry)\n      return this.equals(entry);\n    return false;\n  }\n\n  public boolean equals(LogEntry that) {\n    if (that == null)\n      return false;\n    if (this == that)\n      return true;\n\n    boolean this_present_category = true && this.isSetCategory();\n    boolean that_present_category = true && that.isSetCategory();\n    if (this_present_category || that_present_category) {\n      if (!(this_present_category && that_present_category))\n        return false;\n      if (!this.category.equals(that.category))\n        return false;\n    }\n\n    boolean this_present_message = true && this.isSetMessage();\n    boolean that_present_message = true && that.isSetMessage();\n    if (this_present_message || that_present_message) {\n      if (!(this_present_message && that_present_message))\n        return false;\n      if (!this.message.equals(that.message))\n        return false;\n    }\n\n    return true;\n  }\n\n  @Override\n  public int hashCode() {\n    int hashCode = 1;\n\n    hashCode = hashCode * 8191 + ((isSetCategory()) ? 131071 : 524287);\n    if (isSetCategory())\n      hashCode = hashCode * 8191 + category.hashCode();\n\n    hashCode = hashCode * 8191 + ((isSetMessage()) ? 131071 : 524287);\n    if (isSetMessage())\n      hashCode = hashCode * 8191 + message.hashCode();\n\n    return hashCode;\n  }\n\n  @Override\n  public int compareTo(LogEntry other) {\n    if (!getClass().equals(other.getClass())) {\n      return getClass().getName().compareTo(other.getClass().getName());\n    }\n\n    int lastComparison = 0;\n\n    lastComparison = java.lang.Boolean.valueOf(isSetCategory()).compareTo(other.isSetCategory());\n    if (lastComparison != 0) {\n      return lastComparison;\n    }\n    if (isSetCategory()) {\n      lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.category, other.category);\n      if (lastComparison != 0) {\n        return lastComparison;\n      }\n    }\n    lastComparison = java.lang.Boolean.valueOf(isSetMessage()).compareTo(other.isSetMessage());\n    if (lastComparison != 0) {\n      return lastComparison;\n    }\n    if (isSetMessage()) {\n      lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.message, other.message);\n      if (lastComparison != 0) {\n        return lastComparison;\n      }\n    }\n    return 0;\n  }\n\n  @org.apache.thrift.annotation.Nullable\n  public _Fields fieldForId(int fieldId) {\n    return _Fields.findByThriftId(fieldId);\n  }\n\n  public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException {\n    scheme(iprot).read(iprot, this);\n  }\n\n  public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException {\n    scheme(oprot).write(oprot, this);\n  }\n\n  @Override\n  public java.lang.String toString() {\n    java.lang.StringBuilder sb = new java.lang.StringBuilder(\"LogEntry(\");\n    boolean first = true;\n\n    sb.append(\"category:\");\n    if (this.category == null) {\n      sb.append(\"null\");\n    } else {\n      sb.append(this.category);\n    }\n    first = false;\n    if (!first) sb.append(\", \");\n    sb.append(\"message:\");\n    if (this.message == null) {\n      sb.append(\"null\");\n    } else {\n      sb.append(this.message);\n    }\n    first = false;\n    sb.append(\")\");\n    return sb.toString();\n  }\n\n  public void validate() throws org.apache.thrift.TException {\n    // check for required fields\n    // check for sub-struct validity\n  }\n\n  private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {\n    try {\n      write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out)));\n    } catch (org.apache.thrift.TException te) {\n      throw new java.io.IOException(te);\n    }\n  }\n\n  private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException {\n    try {\n      read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in)));\n    } catch (org.apache.thrift.TException te) {\n      throw new java.io.IOException(te);\n    }\n  }\n\n  private static class LogEntryStandardSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n    public LogEntryStandardScheme getScheme() {\n      return new LogEntryStandardScheme();\n    }\n  }\n\n  private static class LogEntryStandardScheme extends org.apache.thrift.scheme.StandardScheme<LogEntry> {\n\n    public void read(org.apache.thrift.protocol.TProtocol iprot, LogEntry struct) throws org.apache.thrift.TException {\n      org.apache.thrift.protocol.TField schemeField;\n      iprot.readStructBegin();\n      while (true)\n      {\n        schemeField = iprot.readFieldBegin();\n        if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { \n          break;\n        }\n        switch (schemeField.id) {\n          case 1: // CATEGORY\n            if (schemeField.type == org.apache.thrift.protocol.TType.STRING) {\n              struct.category = iprot.readString();\n              struct.setCategoryIsSet(true);\n            } else { \n              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n            }\n            break;\n          case 2: // MESSAGE\n            if (schemeField.type == org.apache.thrift.protocol.TType.STRING) {\n              struct.message = iprot.readString();\n              struct.setMessageIsSet(true);\n            } else { \n              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n            }\n            break;\n          default:\n            org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n        }\n        iprot.readFieldEnd();\n      }\n      iprot.readStructEnd();\n\n      // check for required fields of primitive type, which can't be checked in the validate method\n      struct.validate();\n    }\n\n    public void write(org.apache.thrift.protocol.TProtocol oprot, LogEntry struct) throws org.apache.thrift.TException {\n      struct.validate();\n\n      oprot.writeStructBegin(STRUCT_DESC);\n      if (struct.category != null) {\n        oprot.writeFieldBegin(CATEGORY_FIELD_DESC);\n        oprot.writeString(struct.category);\n        oprot.writeFieldEnd();\n      }\n      if (struct.message != null) {\n        oprot.writeFieldBegin(MESSAGE_FIELD_DESC);\n        oprot.writeString(struct.message);\n        oprot.writeFieldEnd();\n      }\n      oprot.writeFieldStop();\n      oprot.writeStructEnd();\n    }\n\n  }\n\n  private static class LogEntryTupleSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n    public LogEntryTupleScheme getScheme() {\n      return new LogEntryTupleScheme();\n    }\n  }\n\n  private static class LogEntryTupleScheme extends org.apache.thrift.scheme.TupleScheme<LogEntry> {\n\n    @Override\n    public void write(org.apache.thrift.protocol.TProtocol prot, LogEntry struct) throws org.apache.thrift.TException {\n      org.apache.thrift.protocol.TTupleProtocol oprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n      java.util.BitSet optionals = new java.util.BitSet();\n      if (struct.isSetCategory()) {\n        optionals.set(0);\n      }\n      if (struct.isSetMessage()) {\n        optionals.set(1);\n      }\n      oprot.writeBitSet(optionals, 2);\n      if (struct.isSetCategory()) {\n        oprot.writeString(struct.category);\n      }\n      if (struct.isSetMessage()) {\n        oprot.writeString(struct.message);\n      }\n    }\n\n    @Override\n    public void read(org.apache.thrift.protocol.TProtocol prot, LogEntry struct) throws org.apache.thrift.TException {\n      org.apache.thrift.protocol.TTupleProtocol iprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n      java.util.BitSet incoming = iprot.readBitSet(2);\n      if (incoming.get(0)) {\n        struct.category = iprot.readString();\n        struct.setCategoryIsSet(true);\n      }\n      if (incoming.get(1)) {\n        struct.message = iprot.readString();\n        struct.setMessageIsSet(true);\n      }\n    }\n  }\n\n  private static <S extends org.apache.thrift.scheme.IScheme> S scheme(org.apache.thrift.protocol.TProtocol proto) {\n    return (org.apache.thrift.scheme.StandardScheme.class.equals(proto.getScheme()) ? STANDARD_SCHEME_FACTORY : TUPLE_SCHEME_FACTORY).getScheme();\n  }\n}\n\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/generated/ResultCode.java",
    "content": "/**\n * Autogenerated by Thrift Compiler (0.12.0)\n *\n * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING\n *  @generated\n */\npackage zipkin2.collector.scribe.generated;\n\n\n@javax.annotation.Generated(value = \"Autogenerated by Thrift Compiler (0.12.0)\", date = \"2019-05-07\")\npublic enum ResultCode implements org.apache.thrift.TEnum {\n  OK(0),\n  TRY_LATER(1);\n\n  private final int value;\n\n  private ResultCode(int value) {\n    this.value = value;\n  }\n\n  /**\n   * Get the integer value of this enum value, as defined in the Thrift IDL.\n   */\n  public int getValue() {\n    return value;\n  }\n\n  /**\n   * Find a the enum type by its integer value, as defined in the Thrift IDL.\n   * @return null if the value is not found.\n   */\n  @org.apache.thrift.annotation.Nullable\n  public static ResultCode findByValue(int value) { \n    switch (value) {\n      case 0:\n        return OK;\n      case 1:\n        return TRY_LATER;\n      default:\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/main/java/zipkin2/collector/scribe/generated/Scribe.java",
    "content": "/**\n * Autogenerated by Thrift Compiler (0.12.0)\n *\n * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING\n *  @generated\n */\npackage zipkin2.collector.scribe.generated;\n\n@SuppressWarnings({\"cast\", \"rawtypes\", \"serial\", \"unchecked\", \"unused\"})\n@javax.annotation.Generated(value = \"Autogenerated by Thrift Compiler (0.12.0)\", date = \"2019-05-07\")\npublic class Scribe {\n\n  public interface Iface {\n\n    public ResultCode Log(java.util.List<LogEntry> messages) throws org.apache.thrift.TException;\n\n  }\n\n  public interface AsyncIface {\n\n    public void Log(java.util.List<LogEntry> messages, org.apache.thrift.async.AsyncMethodCallback<ResultCode> resultHandler) throws org.apache.thrift.TException;\n\n  }\n\n  public static class Client extends org.apache.thrift.TServiceClient implements Iface {\n    public static class Factory implements org.apache.thrift.TServiceClientFactory<Client> {\n      public Factory() {}\n      public Client getClient(org.apache.thrift.protocol.TProtocol prot) {\n        return new Client(prot);\n      }\n      public Client getClient(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {\n        return new Client(iprot, oprot);\n      }\n    }\n\n    public Client(org.apache.thrift.protocol.TProtocol prot)\n    {\n      super(prot, prot);\n    }\n\n    public Client(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {\n      super(iprot, oprot);\n    }\n\n    public ResultCode Log(java.util.List<LogEntry> messages) throws org.apache.thrift.TException\n    {\n      send_Log(messages);\n      return recv_Log();\n    }\n\n    public void send_Log(java.util.List<LogEntry> messages) throws org.apache.thrift.TException\n    {\n      Log_args args = new Log_args();\n      args.setMessages(messages);\n      sendBase(\"Log\", args);\n    }\n\n    public ResultCode recv_Log() throws org.apache.thrift.TException\n    {\n      Log_result result = new Log_result();\n      receiveBase(result, \"Log\");\n      if (result.isSetSuccess()) {\n        return result.success;\n      }\n      throw new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, \"Log failed: unknown result\");\n    }\n\n  }\n  public static class AsyncClient extends org.apache.thrift.async.TAsyncClient implements AsyncIface {\n    public static class Factory implements org.apache.thrift.async.TAsyncClientFactory<AsyncClient> {\n      private org.apache.thrift.async.TAsyncClientManager clientManager;\n      private org.apache.thrift.protocol.TProtocolFactory protocolFactory;\n      public Factory(org.apache.thrift.async.TAsyncClientManager clientManager, org.apache.thrift.protocol.TProtocolFactory protocolFactory) {\n        this.clientManager = clientManager;\n        this.protocolFactory = protocolFactory;\n      }\n      public AsyncClient getAsyncClient(org.apache.thrift.transport.TNonblockingTransport transport) {\n        return new AsyncClient(protocolFactory, clientManager, transport);\n      }\n    }\n\n    public AsyncClient(org.apache.thrift.protocol.TProtocolFactory protocolFactory, org.apache.thrift.async.TAsyncClientManager clientManager, org.apache.thrift.transport.TNonblockingTransport transport) {\n      super(protocolFactory, clientManager, transport);\n    }\n\n    public void Log(java.util.List<LogEntry> messages, org.apache.thrift.async.AsyncMethodCallback<ResultCode> resultHandler) throws org.apache.thrift.TException {\n      checkReady();\n      Log_call method_call = new Log_call(messages, resultHandler, this, ___protocolFactory, ___transport);\n      this.___currentMethod = method_call;\n      ___manager.call(method_call);\n    }\n\n    public static class Log_call extends org.apache.thrift.async.TAsyncMethodCall<ResultCode> {\n      private java.util.List<LogEntry> messages;\n      public Log_call(java.util.List<LogEntry> messages, org.apache.thrift.async.AsyncMethodCallback<ResultCode> resultHandler, org.apache.thrift.async.TAsyncClient client, org.apache.thrift.protocol.TProtocolFactory protocolFactory, org.apache.thrift.transport.TNonblockingTransport transport) throws org.apache.thrift.TException {\n        super(client, protocolFactory, transport, resultHandler, false);\n        this.messages = messages;\n      }\n\n      public void write_args(org.apache.thrift.protocol.TProtocol prot) throws org.apache.thrift.TException {\n        prot.writeMessageBegin(new org.apache.thrift.protocol.TMessage(\"Log\", org.apache.thrift.protocol.TMessageType.CALL, 0));\n        Log_args args = new Log_args();\n        args.setMessages(messages);\n        args.write(prot);\n        prot.writeMessageEnd();\n      }\n\n      public ResultCode getResult() throws org.apache.thrift.TException {\n        if (getState() != org.apache.thrift.async.TAsyncMethodCall.State.RESPONSE_READ) {\n          throw new java.lang.IllegalStateException(\"Method call not finished!\");\n        }\n        org.apache.thrift.transport.TMemoryInputTransport memoryTransport = new org.apache.thrift.transport.TMemoryInputTransport(getFrameBuffer().array());\n        org.apache.thrift.protocol.TProtocol prot = client.getProtocolFactory().getProtocol(memoryTransport);\n        return (new Client(prot)).recv_Log();\n      }\n    }\n\n  }\n\n  public static class Processor<I extends Iface> extends org.apache.thrift.TBaseProcessor<I> implements org.apache.thrift.TProcessor {\n    private static final org.slf4j.Logger _LOGGER = org.slf4j.LoggerFactory.getLogger(Processor.class.getName());\n    public Processor(I iface) {\n      super(iface, getProcessMap(new java.util.HashMap<java.lang.String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>>()));\n    }\n\n    protected Processor(I iface, java.util.Map<java.lang.String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> processMap) {\n      super(iface, getProcessMap(processMap));\n    }\n\n    private static <I extends Iface> java.util.Map<java.lang.String,  org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> getProcessMap(java.util.Map<java.lang.String, org.apache.thrift.ProcessFunction<I, ? extends  org.apache.thrift.TBase>> processMap) {\n      processMap.put(\"Log\", new Log());\n      return processMap;\n    }\n\n    public static class Log<I extends Iface> extends org.apache.thrift.ProcessFunction<I, Log_args> {\n      public Log() {\n        super(\"Log\");\n      }\n\n      public Log_args getEmptyArgsInstance() {\n        return new Log_args();\n      }\n\n      protected boolean isOneway() {\n        return false;\n      }\n\n      @Override\n      protected boolean rethrowUnhandledExceptions() {\n        return false;\n      }\n\n      public Log_result getResult(I iface, Log_args args) throws org.apache.thrift.TException {\n        Log_result result = new Log_result();\n        result.success = iface.Log(args.messages);\n        return result;\n      }\n    }\n\n  }\n\n  public static class AsyncProcessor<I extends AsyncIface> extends org.apache.thrift.TBaseAsyncProcessor<I> {\n    private static final org.slf4j.Logger _LOGGER = org.slf4j.LoggerFactory.getLogger(AsyncProcessor.class.getName());\n    public AsyncProcessor(I iface) {\n      super(iface, getProcessMap(new java.util.HashMap<java.lang.String, org.apache.thrift.AsyncProcessFunction<I, ? extends org.apache.thrift.TBase, ?>>()));\n    }\n\n    protected AsyncProcessor(I iface, java.util.Map<java.lang.String,  org.apache.thrift.AsyncProcessFunction<I, ? extends  org.apache.thrift.TBase, ?>> processMap) {\n      super(iface, getProcessMap(processMap));\n    }\n\n    private static <I extends AsyncIface> java.util.Map<java.lang.String,  org.apache.thrift.AsyncProcessFunction<I, ? extends  org.apache.thrift.TBase,?>> getProcessMap(java.util.Map<java.lang.String,  org.apache.thrift.AsyncProcessFunction<I, ? extends  org.apache.thrift.TBase, ?>> processMap) {\n      processMap.put(\"Log\", new Log());\n      return processMap;\n    }\n\n    public static class Log<I extends AsyncIface> extends org.apache.thrift.AsyncProcessFunction<I, Log_args, ResultCode> {\n      public Log() {\n        super(\"Log\");\n      }\n\n      public Log_args getEmptyArgsInstance() {\n        return new Log_args();\n      }\n\n      public org.apache.thrift.async.AsyncMethodCallback<ResultCode> getResultHandler(final org.apache.thrift.server.AbstractNonblockingServer.AsyncFrameBuffer fb, final int seqid) {\n        final org.apache.thrift.AsyncProcessFunction fcall = this;\n        return new org.apache.thrift.async.AsyncMethodCallback<ResultCode>() { \n          public void onComplete(ResultCode o) {\n            Log_result result = new Log_result();\n            result.success = o;\n            try {\n              fcall.sendResponse(fb, result, org.apache.thrift.protocol.TMessageType.REPLY,seqid);\n            } catch (org.apache.thrift.transport.TTransportException e) {\n              _LOGGER.error(\"TTransportException writing to internal frame buffer\", e);\n              fb.close();\n            } catch (java.lang.Exception e) {\n              _LOGGER.error(\"Exception writing to internal frame buffer\", e);\n              onError(e);\n            }\n          }\n          public void onError(java.lang.Exception e) {\n            byte msgType = org.apache.thrift.protocol.TMessageType.REPLY;\n            org.apache.thrift.TSerializable msg;\n            Log_result result = new Log_result();\n            if (e instanceof org.apache.thrift.transport.TTransportException) {\n              _LOGGER.error(\"TTransportException inside handler\", e);\n              fb.close();\n              return;\n            } else if (e instanceof org.apache.thrift.TApplicationException exception) {\n              _LOGGER.error(\"TApplicationException inside handler\", e);\n              msgType = org.apache.thrift.protocol.TMessageType.EXCEPTION;\n              msg = exception;\n            } else {\n              _LOGGER.error(\"Exception inside handler\", e);\n              msgType = org.apache.thrift.protocol.TMessageType.EXCEPTION;\n              msg = new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.INTERNAL_ERROR, e.getMessage());\n            }\n            try {\n              fcall.sendResponse(fb,msg,msgType,seqid);\n            } catch (java.lang.Exception ex) {\n              _LOGGER.error(\"Exception writing to internal frame buffer\", ex);\n              fb.close();\n            }\n          }\n        };\n      }\n\n      protected boolean isOneway() {\n        return false;\n      }\n\n      public void start(I iface, Log_args args, org.apache.thrift.async.AsyncMethodCallback<ResultCode> resultHandler) throws org.apache.thrift.TException {\n        iface.Log(args.messages,resultHandler);\n      }\n    }\n\n  }\n\n  public static class Log_args implements org.apache.thrift.TBase<Log_args, Log_args._Fields>, java.io.Serializable, Cloneable, Comparable<Log_args>   {\n    private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct(\"Log_args\");\n\n    private static final org.apache.thrift.protocol.TField MESSAGES_FIELD_DESC = new org.apache.thrift.protocol.TField(\"messages\", org.apache.thrift.protocol.TType.LIST, (short)1);\n\n    private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new Log_argsStandardSchemeFactory();\n    private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new Log_argsTupleSchemeFactory();\n\n    public @org.apache.thrift.annotation.Nullable java.util.List<LogEntry> messages; // required\n\n    /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */\n    public enum _Fields implements org.apache.thrift.TFieldIdEnum {\n      MESSAGES((short)1, \"messages\");\n\n      private static final java.util.Map<java.lang.String, _Fields> byName = new java.util.HashMap<java.lang.String, _Fields>();\n\n      static {\n        for (_Fields field : java.util.EnumSet.allOf(_Fields.class)) {\n          byName.put(field.getFieldName(), field);\n        }\n      }\n\n      /**\n       * Find the _Fields constant that matches fieldId, or null if its not found.\n       */\n      @org.apache.thrift.annotation.Nullable\n      public static _Fields findByThriftId(int fieldId) {\n        switch(fieldId) {\n          case 1: // MESSAGES\n            return MESSAGES;\n          default:\n            return null;\n        }\n      }\n\n      /**\n       * Find the _Fields constant that matches fieldId, throwing an exception\n       * if it is not found.\n       */\n      public static _Fields findByThriftIdOrThrow(int fieldId) {\n        _Fields fields = findByThriftId(fieldId);\n        if (fields == null) throw new java.lang.IllegalArgumentException(\"Field \" + fieldId + \" doesn't exist!\");\n        return fields;\n      }\n\n      /**\n       * Find the _Fields constant that matches name, or null if its not found.\n       */\n      @org.apache.thrift.annotation.Nullable\n      public static _Fields findByName(java.lang.String name) {\n        return byName.get(name);\n      }\n\n      private final short _thriftId;\n      private final java.lang.String _fieldName;\n\n      _Fields(short thriftId, java.lang.String fieldName) {\n        _thriftId = thriftId;\n        _fieldName = fieldName;\n      }\n\n      public short getThriftFieldId() {\n        return _thriftId;\n      }\n\n      public java.lang.String getFieldName() {\n        return _fieldName;\n      }\n    }\n\n    // isset id assignments\n    public static final java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap;\n    static {\n      java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new java.util.EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class);\n      tmpMap.put(_Fields.MESSAGES, new org.apache.thrift.meta_data.FieldMetaData(\"messages\", org.apache.thrift.TFieldRequirementType.DEFAULT, \n          new org.apache.thrift.meta_data.ListMetaData(org.apache.thrift.protocol.TType.LIST, \n              new org.apache.thrift.meta_data.StructMetaData(org.apache.thrift.protocol.TType.STRUCT, LogEntry.class))));\n      metaDataMap = java.util.Collections.unmodifiableMap(tmpMap);\n      org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(Log_args.class, metaDataMap);\n    }\n\n    public Log_args() {\n    }\n\n    public Log_args(\n      java.util.List<LogEntry> messages)\n    {\n      this();\n      this.messages = messages;\n    }\n\n    /**\n     * Performs a deep copy on <i>other</i>.\n     */\n    public Log_args(Log_args other) {\n      if (other.isSetMessages()) {\n        java.util.List<LogEntry> __this__messages = new java.util.ArrayList<LogEntry>(other.messages.size());\n        for (LogEntry other_element : other.messages) {\n          __this__messages.add(new LogEntry(other_element));\n        }\n        this.messages = __this__messages;\n      }\n    }\n\n    public Log_args deepCopy() {\n      return new Log_args(this);\n    }\n\n    @Override\n    public void clear() {\n      this.messages = null;\n    }\n\n    public int getMessagesSize() {\n      return (this.messages == null) ? 0 : this.messages.size();\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public java.util.Iterator<LogEntry> getMessagesIterator() {\n      return (this.messages == null) ? null : this.messages.iterator();\n    }\n\n    public void addToMessages(LogEntry elem) {\n      if (this.messages == null) {\n        this.messages = new java.util.ArrayList<LogEntry>();\n      }\n      this.messages.add(elem);\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public java.util.List<LogEntry> getMessages() {\n      return this.messages;\n    }\n\n    public Log_args setMessages(@org.apache.thrift.annotation.Nullable java.util.List<LogEntry> messages) {\n      this.messages = messages;\n      return this;\n    }\n\n    public void unsetMessages() {\n      this.messages = null;\n    }\n\n    /** Returns true if field messages is set (has been assigned a value) and false otherwise */\n    public boolean isSetMessages() {\n      return this.messages != null;\n    }\n\n    public void setMessagesIsSet(boolean value) {\n      if (!value) {\n        this.messages = null;\n      }\n    }\n\n    public void setFieldValue(_Fields field, @org.apache.thrift.annotation.Nullable java.lang.Object value) {\n      switch (field) {\n      case MESSAGES:\n        if (value == null) {\n          unsetMessages();\n        } else {\n          setMessages((java.util.List<LogEntry>)value);\n        }\n        break;\n\n      }\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public java.lang.Object getFieldValue(_Fields field) {\n      switch (field) {\n      case MESSAGES:\n        return getMessages();\n\n      }\n      throw new java.lang.IllegalStateException();\n    }\n\n    /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */\n    public boolean isSet(_Fields field) {\n      if (field == null) {\n        throw new java.lang.IllegalArgumentException();\n      }\n\n      switch (field) {\n      case MESSAGES:\n        return isSetMessages();\n      }\n      throw new java.lang.IllegalStateException();\n    }\n\n    @Override\n    public boolean equals(java.lang.Object that) {\n      if (that == null)\n        return false;\n      if (that instanceof Log_args log_args)\n        return this.equals(log_args);\n      return false;\n    }\n\n    public boolean equals(Log_args that) {\n      if (that == null)\n        return false;\n      if (this == that)\n        return true;\n\n      boolean this_present_messages = true && this.isSetMessages();\n      boolean that_present_messages = true && that.isSetMessages();\n      if (this_present_messages || that_present_messages) {\n        if (!(this_present_messages && that_present_messages))\n          return false;\n        if (!this.messages.equals(that.messages))\n          return false;\n      }\n\n      return true;\n    }\n\n    @Override\n    public int hashCode() {\n      int hashCode = 1;\n\n      hashCode = hashCode * 8191 + ((isSetMessages()) ? 131071 : 524287);\n      if (isSetMessages())\n        hashCode = hashCode * 8191 + messages.hashCode();\n\n      return hashCode;\n    }\n\n    @Override\n    public int compareTo(Log_args other) {\n      if (!getClass().equals(other.getClass())) {\n        return getClass().getName().compareTo(other.getClass().getName());\n      }\n\n      int lastComparison = 0;\n\n      lastComparison = java.lang.Boolean.valueOf(isSetMessages()).compareTo(other.isSetMessages());\n      if (lastComparison != 0) {\n        return lastComparison;\n      }\n      if (isSetMessages()) {\n        lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.messages, other.messages);\n        if (lastComparison != 0) {\n          return lastComparison;\n        }\n      }\n      return 0;\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public _Fields fieldForId(int fieldId) {\n      return _Fields.findByThriftId(fieldId);\n    }\n\n    public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException {\n      scheme(iprot).read(iprot, this);\n    }\n\n    public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException {\n      scheme(oprot).write(oprot, this);\n    }\n\n    @Override\n    public java.lang.String toString() {\n      java.lang.StringBuilder sb = new java.lang.StringBuilder(\"Log_args(\");\n      boolean first = true;\n\n      sb.append(\"messages:\");\n      if (this.messages == null) {\n        sb.append(\"null\");\n      } else {\n        sb.append(this.messages);\n      }\n      first = false;\n      sb.append(\")\");\n      return sb.toString();\n    }\n\n    public void validate() throws org.apache.thrift.TException {\n      // check for required fields\n      // check for sub-struct validity\n    }\n\n    private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {\n      try {\n        write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out)));\n      } catch (org.apache.thrift.TException te) {\n        throw new java.io.IOException(te);\n      }\n    }\n\n    private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException {\n      try {\n        read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in)));\n      } catch (org.apache.thrift.TException te) {\n        throw new java.io.IOException(te);\n      }\n    }\n\n    private static class Log_argsStandardSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n      public Log_argsStandardScheme getScheme() {\n        return new Log_argsStandardScheme();\n      }\n    }\n\n    private static class Log_argsStandardScheme extends org.apache.thrift.scheme.StandardScheme<Log_args> {\n\n      public void read(org.apache.thrift.protocol.TProtocol iprot, Log_args struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TField schemeField;\n        iprot.readStructBegin();\n        while (true)\n        {\n          schemeField = iprot.readFieldBegin();\n          if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { \n            break;\n          }\n          switch (schemeField.id) {\n            case 1: // MESSAGES\n              if (schemeField.type == org.apache.thrift.protocol.TType.LIST) {\n                {\n                  org.apache.thrift.protocol.TList _list0 = iprot.readListBegin();\n                  struct.messages = new java.util.ArrayList<LogEntry>(_list0.size);\n                  @org.apache.thrift.annotation.Nullable LogEntry _elem1;\n                  for (int _i2 = 0; _i2 < _list0.size; ++_i2)\n                  {\n                    _elem1 = new LogEntry();\n                    _elem1.read(iprot);\n                    struct.messages.add(_elem1);\n                  }\n                  iprot.readListEnd();\n                }\n                struct.setMessagesIsSet(true);\n              } else { \n                org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n              }\n              break;\n            default:\n              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n          }\n          iprot.readFieldEnd();\n        }\n        iprot.readStructEnd();\n\n        // check for required fields of primitive type, which can't be checked in the validate method\n        struct.validate();\n      }\n\n      public void write(org.apache.thrift.protocol.TProtocol oprot, Log_args struct) throws org.apache.thrift.TException {\n        struct.validate();\n\n        oprot.writeStructBegin(STRUCT_DESC);\n        if (struct.messages != null) {\n          oprot.writeFieldBegin(MESSAGES_FIELD_DESC);\n          {\n            oprot.writeListBegin(new org.apache.thrift.protocol.TList(org.apache.thrift.protocol.TType.STRUCT, struct.messages.size()));\n            for (LogEntry _iter3 : struct.messages)\n            {\n              _iter3.write(oprot);\n            }\n            oprot.writeListEnd();\n          }\n          oprot.writeFieldEnd();\n        }\n        oprot.writeFieldStop();\n        oprot.writeStructEnd();\n      }\n\n    }\n\n    private static class Log_argsTupleSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n      public Log_argsTupleScheme getScheme() {\n        return new Log_argsTupleScheme();\n      }\n    }\n\n    private static class Log_argsTupleScheme extends org.apache.thrift.scheme.TupleScheme<Log_args> {\n\n      @Override\n      public void write(org.apache.thrift.protocol.TProtocol prot, Log_args struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TTupleProtocol oprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n        java.util.BitSet optionals = new java.util.BitSet();\n        if (struct.isSetMessages()) {\n          optionals.set(0);\n        }\n        oprot.writeBitSet(optionals, 1);\n        if (struct.isSetMessages()) {\n          {\n            oprot.writeI32(struct.messages.size());\n            for (LogEntry _iter4 : struct.messages)\n            {\n              _iter4.write(oprot);\n            }\n          }\n        }\n      }\n\n      @Override\n      public void read(org.apache.thrift.protocol.TProtocol prot, Log_args struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TTupleProtocol iprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n        java.util.BitSet incoming = iprot.readBitSet(1);\n        if (incoming.get(0)) {\n          {\n            org.apache.thrift.protocol.TList _list5 = new org.apache.thrift.protocol.TList(org.apache.thrift.protocol.TType.STRUCT, iprot.readI32());\n            struct.messages = new java.util.ArrayList<LogEntry>(_list5.size);\n            @org.apache.thrift.annotation.Nullable LogEntry _elem6;\n            for (int _i7 = 0; _i7 < _list5.size; ++_i7)\n            {\n              _elem6 = new LogEntry();\n              _elem6.read(iprot);\n              struct.messages.add(_elem6);\n            }\n          }\n          struct.setMessagesIsSet(true);\n        }\n      }\n    }\n\n    private static <S extends org.apache.thrift.scheme.IScheme> S scheme(org.apache.thrift.protocol.TProtocol proto) {\n      return (org.apache.thrift.scheme.StandardScheme.class.equals(proto.getScheme()) ? STANDARD_SCHEME_FACTORY : TUPLE_SCHEME_FACTORY).getScheme();\n    }\n  }\n\n  public static class Log_result implements org.apache.thrift.TBase<Log_result, Log_result._Fields>, java.io.Serializable, Cloneable, Comparable<Log_result>   {\n    private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct(\"Log_result\");\n\n    private static final org.apache.thrift.protocol.TField SUCCESS_FIELD_DESC = new org.apache.thrift.protocol.TField(\"success\", org.apache.thrift.protocol.TType.I32, (short)0);\n\n    private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new Log_resultStandardSchemeFactory();\n    private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new Log_resultTupleSchemeFactory();\n\n    /**\n     * \n     * @see ResultCode\n     */\n    public @org.apache.thrift.annotation.Nullable ResultCode success; // required\n\n    /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */\n    public enum _Fields implements org.apache.thrift.TFieldIdEnum {\n      /**\n       * \n       * @see ResultCode\n       */\n      SUCCESS((short)0, \"success\");\n\n      private static final java.util.Map<java.lang.String, _Fields> byName = new java.util.HashMap<java.lang.String, _Fields>();\n\n      static {\n        for (_Fields field : java.util.EnumSet.allOf(_Fields.class)) {\n          byName.put(field.getFieldName(), field);\n        }\n      }\n\n      /**\n       * Find the _Fields constant that matches fieldId, or null if its not found.\n       */\n      @org.apache.thrift.annotation.Nullable\n      public static _Fields findByThriftId(int fieldId) {\n        switch(fieldId) {\n          case 0: // SUCCESS\n            return SUCCESS;\n          default:\n            return null;\n        }\n      }\n\n      /**\n       * Find the _Fields constant that matches fieldId, throwing an exception\n       * if it is not found.\n       */\n      public static _Fields findByThriftIdOrThrow(int fieldId) {\n        _Fields fields = findByThriftId(fieldId);\n        if (fields == null) throw new java.lang.IllegalArgumentException(\"Field \" + fieldId + \" doesn't exist!\");\n        return fields;\n      }\n\n      /**\n       * Find the _Fields constant that matches name, or null if its not found.\n       */\n      @org.apache.thrift.annotation.Nullable\n      public static _Fields findByName(java.lang.String name) {\n        return byName.get(name);\n      }\n\n      private final short _thriftId;\n      private final java.lang.String _fieldName;\n\n      _Fields(short thriftId, java.lang.String fieldName) {\n        _thriftId = thriftId;\n        _fieldName = fieldName;\n      }\n\n      public short getThriftFieldId() {\n        return _thriftId;\n      }\n\n      public java.lang.String getFieldName() {\n        return _fieldName;\n      }\n    }\n\n    // isset id assignments\n    public static final java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap;\n    static {\n      java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new java.util.EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class);\n      tmpMap.put(_Fields.SUCCESS, new org.apache.thrift.meta_data.FieldMetaData(\"success\", org.apache.thrift.TFieldRequirementType.DEFAULT, \n          new org.apache.thrift.meta_data.EnumMetaData(org.apache.thrift.protocol.TType.ENUM, ResultCode.class)));\n      metaDataMap = java.util.Collections.unmodifiableMap(tmpMap);\n      org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(Log_result.class, metaDataMap);\n    }\n\n    public Log_result() {\n    }\n\n    public Log_result(\n      ResultCode success)\n    {\n      this();\n      this.success = success;\n    }\n\n    /**\n     * Performs a deep copy on <i>other</i>.\n     */\n    public Log_result(Log_result other) {\n      if (other.isSetSuccess()) {\n        this.success = other.success;\n      }\n    }\n\n    public Log_result deepCopy() {\n      return new Log_result(this);\n    }\n\n    @Override\n    public void clear() {\n      this.success = null;\n    }\n\n    /**\n     * \n     * @see ResultCode\n     */\n    @org.apache.thrift.annotation.Nullable\n    public ResultCode getSuccess() {\n      return this.success;\n    }\n\n    /**\n     * \n     * @see ResultCode\n     */\n    public Log_result setSuccess(@org.apache.thrift.annotation.Nullable ResultCode success) {\n      this.success = success;\n      return this;\n    }\n\n    public void unsetSuccess() {\n      this.success = null;\n    }\n\n    /** Returns true if field success is set (has been assigned a value) and false otherwise */\n    public boolean isSetSuccess() {\n      return this.success != null;\n    }\n\n    public void setSuccessIsSet(boolean value) {\n      if (!value) {\n        this.success = null;\n      }\n    }\n\n    public void setFieldValue(_Fields field, @org.apache.thrift.annotation.Nullable java.lang.Object value) {\n      switch (field) {\n      case SUCCESS:\n        if (value == null) {\n          unsetSuccess();\n        } else {\n          setSuccess((ResultCode)value);\n        }\n        break;\n\n      }\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public java.lang.Object getFieldValue(_Fields field) {\n      switch (field) {\n      case SUCCESS:\n        return getSuccess();\n\n      }\n      throw new java.lang.IllegalStateException();\n    }\n\n    /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */\n    public boolean isSet(_Fields field) {\n      if (field == null) {\n        throw new java.lang.IllegalArgumentException();\n      }\n\n      switch (field) {\n      case SUCCESS:\n        return isSetSuccess();\n      }\n      throw new java.lang.IllegalStateException();\n    }\n\n    @Override\n    public boolean equals(java.lang.Object that) {\n      if (that == null)\n        return false;\n      if (that instanceof Log_result log_result)\n        return this.equals(log_result);\n      return false;\n    }\n\n    public boolean equals(Log_result that) {\n      if (that == null)\n        return false;\n      if (this == that)\n        return true;\n\n      boolean this_present_success = true && this.isSetSuccess();\n      boolean that_present_success = true && that.isSetSuccess();\n      if (this_present_success || that_present_success) {\n        if (!(this_present_success && that_present_success))\n          return false;\n        if (!this.success.equals(that.success))\n          return false;\n      }\n\n      return true;\n    }\n\n    @Override\n    public int hashCode() {\n      int hashCode = 1;\n\n      hashCode = hashCode * 8191 + ((isSetSuccess()) ? 131071 : 524287);\n      if (isSetSuccess())\n        hashCode = hashCode * 8191 + success.getValue();\n\n      return hashCode;\n    }\n\n    @Override\n    public int compareTo(Log_result other) {\n      if (!getClass().equals(other.getClass())) {\n        return getClass().getName().compareTo(other.getClass().getName());\n      }\n\n      int lastComparison = 0;\n\n      lastComparison = java.lang.Boolean.valueOf(isSetSuccess()).compareTo(other.isSetSuccess());\n      if (lastComparison != 0) {\n        return lastComparison;\n      }\n      if (isSetSuccess()) {\n        lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.success, other.success);\n        if (lastComparison != 0) {\n          return lastComparison;\n        }\n      }\n      return 0;\n    }\n\n    @org.apache.thrift.annotation.Nullable\n    public _Fields fieldForId(int fieldId) {\n      return _Fields.findByThriftId(fieldId);\n    }\n\n    public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException {\n      scheme(iprot).read(iprot, this);\n    }\n\n    public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException {\n      scheme(oprot).write(oprot, this);\n      }\n\n    @Override\n    public java.lang.String toString() {\n      java.lang.StringBuilder sb = new java.lang.StringBuilder(\"Log_result(\");\n      boolean first = true;\n\n      sb.append(\"success:\");\n      if (this.success == null) {\n        sb.append(\"null\");\n      } else {\n        sb.append(this.success);\n      }\n      first = false;\n      sb.append(\")\");\n      return sb.toString();\n    }\n\n    public void validate() throws org.apache.thrift.TException {\n      // check for required fields\n      // check for sub-struct validity\n    }\n\n    private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {\n      try {\n        write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out)));\n      } catch (org.apache.thrift.TException te) {\n        throw new java.io.IOException(te);\n      }\n    }\n\n    private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException {\n      try {\n        read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in)));\n      } catch (org.apache.thrift.TException te) {\n        throw new java.io.IOException(te);\n      }\n    }\n\n    private static class Log_resultStandardSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n      public Log_resultStandardScheme getScheme() {\n        return new Log_resultStandardScheme();\n      }\n    }\n\n    private static class Log_resultStandardScheme extends org.apache.thrift.scheme.StandardScheme<Log_result> {\n\n      public void read(org.apache.thrift.protocol.TProtocol iprot, Log_result struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TField schemeField;\n        iprot.readStructBegin();\n        while (true)\n        {\n          schemeField = iprot.readFieldBegin();\n          if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { \n            break;\n          }\n          switch (schemeField.id) {\n            case 0: // SUCCESS\n              if (schemeField.type == org.apache.thrift.protocol.TType.I32) {\n                struct.success = ResultCode.findByValue(iprot.readI32());\n                struct.setSuccessIsSet(true);\n              } else { \n                org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n              }\n              break;\n            default:\n              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);\n          }\n          iprot.readFieldEnd();\n        }\n        iprot.readStructEnd();\n\n        // check for required fields of primitive type, which can't be checked in the validate method\n        struct.validate();\n      }\n\n      public void write(org.apache.thrift.protocol.TProtocol oprot, Log_result struct) throws org.apache.thrift.TException {\n        struct.validate();\n\n        oprot.writeStructBegin(STRUCT_DESC);\n        if (struct.success != null) {\n          oprot.writeFieldBegin(SUCCESS_FIELD_DESC);\n          oprot.writeI32(struct.success.getValue());\n          oprot.writeFieldEnd();\n        }\n        oprot.writeFieldStop();\n        oprot.writeStructEnd();\n      }\n\n    }\n\n    private static class Log_resultTupleSchemeFactory implements org.apache.thrift.scheme.SchemeFactory {\n      public Log_resultTupleScheme getScheme() {\n        return new Log_resultTupleScheme();\n      }\n    }\n\n    private static class Log_resultTupleScheme extends org.apache.thrift.scheme.TupleScheme<Log_result> {\n\n      @Override\n      public void write(org.apache.thrift.protocol.TProtocol prot, Log_result struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TTupleProtocol oprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n        java.util.BitSet optionals = new java.util.BitSet();\n        if (struct.isSetSuccess()) {\n          optionals.set(0);\n        }\n        oprot.writeBitSet(optionals, 1);\n        if (struct.isSetSuccess()) {\n          oprot.writeI32(struct.success.getValue());\n        }\n      }\n\n      @Override\n      public void read(org.apache.thrift.protocol.TProtocol prot, Log_result struct) throws org.apache.thrift.TException {\n        org.apache.thrift.protocol.TTupleProtocol iprot = (org.apache.thrift.protocol.TTupleProtocol) prot;\n        java.util.BitSet incoming = iprot.readBitSet(1);\n        if (incoming.get(0)) {\n          struct.success = ResultCode.findByValue(iprot.readI32());\n          struct.setSuccessIsSet(true);\n        }\n      }\n    }\n\n    private static <S extends org.apache.thrift.scheme.IScheme> S scheme(org.apache.thrift.protocol.TProtocol proto) {\n      return (org.apache.thrift.scheme.StandardScheme.class.equals(proto.getScheme()) ? STANDARD_SCHEME_FACTORY : TUPLE_SCHEME_FACTORY).getScheme();\n    }\n  }\n\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/test/java/zipkin2/collector/scribe/ITScribeCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport com.linecorp.armeria.common.CommonPools;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.thrift.protocol.TBinaryProtocol;\nimport org.apache.thrift.protocol.TProtocol;\nimport org.apache.thrift.transport.TSocket;\nimport org.apache.thrift.transport.TTransport;\nimport org.apache.thrift.transport.layered.TFramedTransport;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.scribe.generated.LogEntry;\nimport zipkin2.collector.scribe.generated.ResultCode;\nimport zipkin2.collector.scribe.generated.Scribe;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nclass ITScribeCollector {\n  static Collector collector;\n  static CollectorMetrics metrics;\n  static NettyScribeServer server;\n\n  @BeforeAll static void startServer() {\n    collector = mock(Collector.class);\n    doAnswer(invocation -> {\n      Callback<Void> callback = invocation.getArgument(1);\n      callback.onSuccess(null);\n      return null;\n    }).when(collector).accept(any(), any(), any());\n\n    metrics = mock(CollectorMetrics.class);\n\n    server = new NettyScribeServer(0, new ScribeSpanConsumer(collector, metrics, \"zipkin\"));\n    server.start();\n  }\n\n  @AfterAll static void stopServer() {\n    server.close();\n  }\n\n  @Test void normal() throws Exception {\n    // Java version of this sample code\n    // https://github.com/facebookarchive/scribe/wiki/Logging-Messages\n    TTransport transport = new TFramedTransport(new TSocket(\"localhost\", server.port()));\n    TProtocol protocol = new TBinaryProtocol(transport, false, false);\n    Scribe.Iface client = new Scribe.Client(protocol);\n\n    List<LogEntry> entries = TestObjects.TRACE.stream()\n      .map(ITScribeCollector::logEntry)\n      .collect(Collectors.toList());\n\n    transport.open();\n    try {\n      ResultCode code = client.Log(entries);\n      assertThat(code).isEqualTo(ResultCode.OK);\n\n      code = client.Log(entries);\n      assertThat(code).isEqualTo(ResultCode.OK);\n    } finally {\n      transport.close();\n    }\n\n    verify(collector, times(2)).accept(eq(TestObjects.TRACE), any(),\n      eq(CommonPools.blockingTaskExecutor()));\n    verify(metrics, times(2)).incrementMessages();\n  }\n\n  static LogEntry logEntry(Span span) {\n    return new LogEntry()\n      .setCategory(\"zipkin\")\n      .setMessage(Base64.getMimeEncoder().encodeToString(SpanBytesEncoder.THRIFT.encode(span)));\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/test/java/zipkin2/collector/scribe/ScribeCollectorTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass ScribeCollectorTest {\n  InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n\n  @Test void check_failsWhenNotStarted() {\n    try (ScribeCollector scribe = ScribeCollector.newBuilder().storage(storage).port(0).build()) {\n\n      CheckResult result = scribe.check();\n      assertThat(result.ok()).isFalse();\n      assertThat(result.error()).isInstanceOf(IllegalStateException.class);\n\n      scribe.start();\n      assertThat(scribe.check().ok()).isTrue();\n    }\n  }\n\n  @Test void anonymousPort() {\n    try (ScribeCollector scribe = ScribeCollector.newBuilder().storage(storage).port(0).build()) {\n\n      assertThat(scribe.port()).isZero();\n\n      scribe.start();\n      assertThat(scribe.port()).isNotZero();\n    }\n  }\n\n  @Test void start_failsWhenCantBindPort() {\n    ScribeCollector.Builder builder = ScribeCollector.newBuilder().storage(storage).port(0);\n\n    try (ScribeCollector first = builder.build().start()) {\n      try (ScribeCollector samePort = builder.port(first.port()).build()) {\n        assertThatThrownBy(samePort::start)\n          .isInstanceOf(RuntimeException.class)\n          .hasMessage(\"Could not start scribe server.\");\n      }\n    }\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    try (ScribeCollector scribe = ScribeCollector.newBuilder().storage(storage).port(0).build()) {\n\n      assertThat(scribe.start())\n        .hasToString(\"ScribeCollector{port=\" + scribe.port() + \", category=zipkin}\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/test/java/zipkin2/collector/scribe/ScribeSpanConsumerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.thrift.async.AsyncMethodCallback;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.collector.scribe.generated.LogEntry;\nimport zipkin2.collector.scribe.generated.ResultCode;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\n\nclass ScribeSpanConsumerTest {\n  // scope to scribe as we aren't creating the consumer with the builder.\n  InMemoryCollectorMetrics scribeMetrics = new InMemoryCollectorMetrics().forTransport(\"scribe\");\n\n  InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n  SpanConsumer consumer = storage.spanConsumer();\n\n  static class CaptureAsyncMethodCallback implements AsyncMethodCallback<ResultCode> {\n\n    ResultCode resultCode;\n    Exception error;\n\n    CountDownLatch latch = new CountDownLatch(1);\n\n    @Override public void onComplete(ResultCode resultCode) {\n      this.resultCode = resultCode;\n      latch.countDown();\n    }\n\n    @Override public void onError(Exception error) {\n      this.error = error;\n      latch.countDown();\n    }\n  }\n\n  static String reallyLongAnnotation;\n\n  static {\n    char[] as = new char[2048];\n    Arrays.fill(as, 'a');\n    reallyLongAnnotation = new String(as);\n  }\n\n  Endpoint zipkinQuery =\n    Endpoint.newBuilder().serviceName(\"zipkin-query\").ip(\"127.0.0.1\").port(9411).build();\n  Endpoint zipkinQuery0 = zipkinQuery.toBuilder().port(null).build();\n\n  V1Span v1 = V1Span.newBuilder()\n    .traceId(-6054243957716233329L)\n    .name(\"getTracesByIds\")\n    .id(-3615651937927048332L)\n    .parentId(-6054243957716233329L)\n    .addAnnotation(1442493420635000L, \"sr\", zipkinQuery)\n    .addAnnotation(1442493420747000L, reallyLongAnnotation, zipkinQuery)\n    .addAnnotation(\n      1442493422583586L,\n      \"Gc(9,0.PSScavenge,2015-09-17 12:37:02 +0000,304.milliseconds+762.microseconds)\",\n      zipkinQuery)\n    .addAnnotation(1442493422680000L, \"ss\", zipkinQuery)\n    .addBinaryAnnotation(\"srv/finagle.version\", \"6.28.0\", zipkinQuery0)\n    .addBinaryAnnotation(\"sa\", zipkinQuery)\n    .addBinaryAnnotation(\"ca\", zipkinQuery.toBuilder().port(63840).build())\n    .debug(false)\n    .build();\n\n  Span v2 = V1SpanConverter.create().convert(v1).get(0);\n  byte[] bytes = SpanBytesEncoder.THRIFT.encode(v2);\n  String encodedSpan = new String(Base64.getEncoder().encode(bytes), UTF_8);\n\n  @Test void entriesWithSpansAreConsumed() throws Exception {\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(\"zipkin\", consumer);\n\n    LogEntry entry = new LogEntry();\n    entry.category = \"zipkin\";\n    entry.message = encodedSpan;\n\n    expectSuccess(scribe, entry);\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(storage.getTraces()).containsExactly(List.of(v2)));\n\n    assertThat(scribeMetrics.messages()).isEqualTo(1);\n    assertThat(scribeMetrics.messagesDropped()).isZero();\n    assertThat(scribeMetrics.bytes()).isEqualTo(bytes.length);\n    assertThat(scribeMetrics.spans()).isEqualTo(1);\n    assertThat(scribeMetrics.spansDropped()).isZero();\n  }\n\n  @Test void entriesWithoutSpansAreSkipped() throws Exception {\n    SpanConsumer consumer = (callback) -> {\n      throw new AssertionError(); // as we shouldn't get here.\n    };\n\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(\"zipkin\", consumer);\n\n    LogEntry entry = new LogEntry();\n    entry.category = \"notzipkin\";\n    entry.message = \"hello world\";\n\n    expectSuccess(scribe, entry);\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(scribeMetrics.messages()).isEqualTo(1));\n    assertThat(scribeMetrics.messagesDropped()).isZero();\n    assertThat(scribeMetrics.bytes()).isZero();\n    assertThat(scribeMetrics.spans()).isZero();\n    assertThat(scribeMetrics.spansDropped()).isZero();\n  }\n\n  private void expectSuccess(ScribeSpanConsumer scribe, LogEntry entry) throws Exception {\n    CaptureAsyncMethodCallback callback = new CaptureAsyncMethodCallback();\n    scribe.Log(List.of(entry), callback);\n    callback.latch.await(10, TimeUnit.SECONDS);\n    assertThat(callback.resultCode).isEqualTo(ResultCode.OK);\n  }\n\n  @Test void malformedDataIsDropped() {\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(\"zipkin\", consumer);\n\n    LogEntry entry = new LogEntry();\n    entry.category = \"zipkin\";\n    entry.message = \"notbase64\";\n\n    CaptureAsyncMethodCallback callback = new CaptureAsyncMethodCallback();\n    scribe.Log(List.of(entry), callback);\n    assertThat(callback.error).isInstanceOf(IllegalArgumentException.class);\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(scribeMetrics.messages()).isEqualTo(1));\n    assertThat(scribeMetrics.messagesDropped()).isEqualTo(1);\n    assertThat(scribeMetrics.bytes()).isZero();\n    assertThat(scribeMetrics.spans()).isZero();\n    assertThat(scribeMetrics.spansDropped()).isZero();\n  }\n\n  @Test void consumerExceptionBeforeCallbackDoesntSetFutureException() {\n    consumer = (input) -> {\n      throw new NullPointerException(\"endpoint was null\");\n    };\n\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(\"zipkin\", consumer);\n\n    LogEntry entry = new LogEntry();\n    entry.category = \"zipkin\";\n    entry.message = encodedSpan;\n\n    CaptureAsyncMethodCallback callback = new CaptureAsyncMethodCallback();\n    scribe.Log(List.of(entry), callback);\n\n    // Storage related exceptions are not propagated to the caller. Only marshalling ones are.\n    assertThat(callback.error).isNull();\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(scribeMetrics.messages()).isEqualTo(1));\n    assertThat(scribeMetrics.messagesDropped()).isZero();\n    assertThat(scribeMetrics.bytes()).isEqualTo(bytes.length);\n    assertThat(scribeMetrics.spans()).isEqualTo(1);\n    assertThat(scribeMetrics.spansDropped()).isEqualTo(1);\n  }\n\n  /**\n   * Callbacks are performed asynchronously. If they throw, it hints that we are chaining futures\n   * when we shouldn't\n   */\n  @Test void callbackExceptionDoesntThrow() throws Exception {\n    consumer = (input) -> new Call.Base<Void>() {\n      @Override protected Void doExecute() {\n        throw new AssertionError();\n      }\n\n      @Override protected void doEnqueue(Callback<Void> callback) {\n        callback.onError(new NullPointerException());\n      }\n\n      @Override public Call<Void> clone() {\n        throw new AssertionError();\n      }\n    };\n\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(\"zipkin\", consumer);\n\n    LogEntry entry = new LogEntry();\n    entry.category = \"zipkin\";\n    entry.message = encodedSpan;\n\n    expectSuccess(scribe, entry);\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(scribeMetrics.messages()).isEqualTo(1));\n    assertThat(scribeMetrics.messagesDropped()).isZero();\n    assertThat(scribeMetrics.bytes()).isEqualTo(bytes.length);\n    assertThat(scribeMetrics.spans()).isEqualTo(1);\n    assertThat(scribeMetrics.spansDropped()).isEqualTo(1);\n  }\n\n  /** Finagle's zipkin tracer breaks on a column width with a trailing newline */\n  @Test void decodesSpanGeneratedByFinagle() throws Exception {\n    LogEntry entry = new LogEntry();\n    entry.category = \"zipkin\";\n    entry.message = \"\"\"\n      CgABq/sBMnzE048LAAMAAAAOZ2V0VHJhY2VzQnlJZHMKAATN0p+4EGfTdAoABav7ATJ8xNOPDwAGDAAAAAQKAAEABR/wq+2DeAsAAgAAAAJzcgwAAwgAAX8AAAEGAAIkwwsAAwAAAAx6aXBraW4tcXVlcnkAAAoAAQAFH/Cr7zj4CwACAAAIAGFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n      YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhDAADCAABfwAAAQYAAiTDCwADAAAADHppcGtpbi1xdWVyeQAACgABAAUf8KwLPyILAAIAAABOR2MoOSwwLlBTU2NhdmVuZ2UsMjAxNS0wOS0xNyAxMjozNzowMiArMDAwMCwzMDQubWlsbGlzZWNvbmRzKzc2Mi5taWNyb3NlY29uZHMpDAADCAABfwAAAQYAAiTDCwADAAAADHppcGtpbi1xdWVyeQAIAAQABKZ6AAoAAQAFH/CsDLfACwACAAAAAnNzDAADCAABfwAAAQYAAiTDCwADAAAADHppcGtpbi1xdWVyeQAADwAIDAAAAAULAAEAAAATc3J2L2ZpbmFnbGUudmVyc2lvbgsAAgAAAAY2LjI4LjAIAAMAAAAGDAAECAABfwAAAQYAAgAACwADAAAADHppcGtpbi1xdWVyeQAACwABAAAAD3Nydi9tdXgvZW5hYmxlZAsAAgAAAAEBCAADAAAAAAwABAgAAX8AAAEGAAIAAAsAAwAAAAx6aXBraW4tcXVlcnkAAAsAAQAAAAJzYQsAAgAAAAEBCAADAAAAAAwABAgAAX8AAAEGAAIkwwsAAwAAAAx6aXBraW4tcXVlcnkAAAsAAQAAAAJjYQsAAgAAAAEBCAADAAAAAAwABAgAAX8AAAEGAAL5YAsAAwAAAAx6aXBraW4tcXVlcnkAAAsAAQAAAAZudW1JZHMLAAIAAAAEAAAAAQgAAwAAAAMMAAQIAAF/AAABBgACJMMLAAMAAAAMemlwa2luLXF1ZXJ5AAACAAkAAA==\n      \"\"\";\n\n    ScribeSpanConsumer scribe = newScribeSpanConsumer(entry.category, consumer);\n\n    expectSuccess(scribe, entry);\n\n    // Storage finishes after callback so wait for it.\n    await().untilAsserted(() -> assertThat(storage.getTraces()).containsExactly(List.of(v2)));\n\n    assertThat(scribeMetrics.messages()).isEqualTo(1);\n    assertThat(scribeMetrics.messagesDropped()).isZero();\n    assertThat(scribeMetrics.bytes())\n      .isEqualTo(Base64.getMimeDecoder().decode(entry.message).length);\n    assertThat(scribeMetrics.spans()).isEqualTo(1);\n    assertThat(scribeMetrics.spansDropped()).isZero();\n  }\n\n  ScribeSpanConsumer newScribeSpanConsumer(String category, SpanConsumer spanConsumer) {\n    ScribeCollector.Builder builder = ScribeCollector.newBuilder()\n      .category(category)\n      .metrics(scribeMetrics)\n      .storage(new ForwardingStorageComponent() {\n        @Override protected StorageComponent delegate() {\n          throw new AssertionError();\n        }\n\n        @Override public SpanConsumer spanConsumer() {\n          return spanConsumer;\n        }\n      });\n    return new ScribeSpanConsumer(\n      builder.delegate.build(),\n      builder.metrics,\n      builder.category);\n  }\n}\n"
  },
  {
    "path": "zipkin-collector/scribe/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\norg.slf4j.simpleLogger.log.zipkin2.collector.scribe=debug\n"
  },
  {
    "path": "zipkin-junit5/README.md",
    "content": "# zipkin-junit5\n\nThis contains `ZipkinExtension`, a JUnit5 extension to spin-up a Zipkin server during tests.\n\nZipkinExtension aims to emulate a http collector. For example, it presents\nthe v1 and v2 POST apis [Zipkin Api](http://openzipkin.github.io/zipkin-api/#/), and\nsupports features like gzip compression.\n\nUsage\n------\n\nFor example, you can write micro-integration tests like so:\n\n```java\n@RegisterExtension\npublic ZipkinExtension zipkin = new ZipkinExtension();\n\n// Pretend we have a traced system under test\nTracedService service = new TracedService(zipkin.httpUrl(), ReportingMode.FLUSH_EVERY);\n\n@Test\npublic void skipsReportingWhenNotSampled() throws IOException {\n  zipkin.storeSpans(asList(rootSpan));\n\n  // send a request to the instrumented server, telling it not to sample.\n  client.addHeader(\"X-B3-TraceId\", rootSpan.traceId)\n        .addHeader(\"X-B3-SpanId\", rootSpan.id)\n        .addHeader(\"X-B3-Sampled\", 0).get(service.httpUrl());\n\n  // check that zipkin didn't receive any new data in that trace\n  assertThat(zipkin.getTraces()).containsOnly(asList(rootSpan));\n}\n```\n\nYou can also simulate failures.\n\nFor example, if you want to ensure your instrumentation doesn't retry on http 400.\n\n```java\n@Test\npublic void doesntAttemptToRetryOn400() throws IOException {\n  zipkin.enqueueFailure(sendErrorResponse(400, \"Invalid Format\"));\n\n  reporter.record(span);\n  reporter.flush();\n\n  // check that we didn't retry on 400\n  assertThat(zipkin.httpRequestCount()).isEqualTo(1);\n}\n```\n\nBesides `httpRequestCount()`, there are two other counters that can\nhelp you assert instrumentation is doing what you think:\n\n* `collectorMetrics()` - How many spans or bytes were collected on the http transport.\n\nThese counters can validate aspects such if you are grouping spans by id\nbefore reporting them to the server.\n"
  },
  {
    "path": "zipkin-junit5/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <groupId>io.zipkin.zipkin2</groupId>\n  <artifactId>zipkin-junit5</artifactId>\n  <name>Zipkin JUnit5</name>\n  <description>JUnit5 Extension to spin-up a Zipkin server during tests</description>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>com.squareup.okhttp3</groupId>\n      <!-- Intentionally not the alpha mockwebserver3 until it can coexist with\n           okhttp 3, avoiding:\n           java.lang.NoSuchMethodError: 'void okhttp3.internal.concurrent.TaskRunner.<init>(okhttp3.internal.concurrent.TaskRunner$Backend, java.util.logging.Logger, int, kotlin.jvm.internal.DefaultConstructorMarker)' -->\n      <artifactId>mockwebserver</artifactId>\n      <version>${okhttp.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>junit</groupId>\n          <artifactId>junit</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <!-- Override mockwebserver's junit only to squash CVE version -->\n    <dependency>\n      <groupId>junit</groupId>\n      <artifactId>junit</artifactId>\n      <version>4.13.2</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-simple</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <!-- Our json codec is shaded, but Intellij doesn't understand that when running tests. -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-tests</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-junit5/src/main/java/zipkin2/junit5/HttpFailure.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.junit5;\n\nimport okhttp3.mockwebserver.MockResponse;\n\nimport static okhttp3.mockwebserver.SocketPolicy.DISCONNECT_DURING_REQUEST_BODY;\n\n/**\n * Instrumentation that use {@code POST} endpoints need to survive failures. Besides simply not\n * starting the zipkin server, you can enqueue failures like this to test edge cases. For example,\n * that you log a failure when a 400 code is returned.\n */\npublic final class HttpFailure {\n\n  /** Ex a network partition occurs in the middle of the POST request */\n  public static HttpFailure disconnectDuringBody() {\n    return new HttpFailure(new MockResponse().setSocketPolicy(DISCONNECT_DURING_REQUEST_BODY));\n  }\n\n  /** Ex code 400 when the server cannot read the spans */\n  public static HttpFailure sendErrorResponse(int code, String body) {\n    return new HttpFailure(new MockResponse().setResponseCode(code).setBody(body));\n  }\n\n  /** Not exposed publicly in order to not leak okhttp3 types. */\n  final MockResponse response;\n\n  HttpFailure(MockResponse response) {\n    this.response = response;\n  }\n}\n"
  },
  {
    "path": "zipkin-junit5/src/main/java/zipkin2/junit5/ZipkinDispatcher.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.junit5;\n\nimport java.io.IOException;\nimport okhttp3.HttpUrl;\nimport okhttp3.mockwebserver.Dispatcher;\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\nimport okio.Buffer;\nimport okio.GzipSource;\nimport zipkin2.Callback;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.storage.StorageComponent;\n\nfinal class ZipkinDispatcher extends Dispatcher {\n  private final Collector consumer;\n  private final CollectorMetrics metrics;\n  private final MockWebServer server;\n\n  ZipkinDispatcher(StorageComponent storage, CollectorMetrics metrics, MockWebServer server) {\n    this.consumer = Collector.newBuilder(getClass()).storage(storage).metrics(metrics).build();\n    this.metrics = metrics;\n    this.server = server;\n  }\n\n  @Override\n  public MockResponse dispatch(RecordedRequest request) {\n    HttpUrl url = server.url(request.getPath());\n    if (request.getMethod().equals(\"POST\")) {\n      String type = request.getHeader(\"Content-Type\");\n      if (url.encodedPath().equals(\"/api/v1/spans\")) {\n        SpanBytesDecoder decoder =\n          type != null && type.contains(\"/x-thrift\")\n            ? SpanBytesDecoder.THRIFT\n            : SpanBytesDecoder.JSON_V1;\n        return acceptSpans(request, decoder);\n      } else if (url.encodedPath().equals(\"/api/v2/spans\")) {\n        SpanBytesDecoder decoder =\n          type != null && type.contains(\"/x-protobuf\")\n            ? SpanBytesDecoder.PROTO3\n            : SpanBytesDecoder.JSON_V2;\n        return acceptSpans(request, decoder);\n      }\n    } else { // unsupported method\n      return new MockResponse().setResponseCode(405);\n    }\n    return new MockResponse().setResponseCode(404);\n  }\n\n  MockResponse acceptSpans(RecordedRequest request, SpanBytesDecoder decoder) {\n    byte[] body = request.getBody().readByteArray();\n    metrics.incrementMessages();\n    String encoding = request.getHeader(\"Content-Encoding\");\n    if (encoding != null && encoding.contains(\"gzip\")) {\n      try {\n        Buffer result = new Buffer();\n        GzipSource source = new GzipSource(new Buffer().write(body));\n        while (source.read(result, Integer.MAX_VALUE) != -1) ;\n        body = result.readByteArray();\n      } catch (IOException e) {\n        metrics.incrementMessagesDropped();\n        return new MockResponse().setResponseCode(400).setBody(\"Cannot gunzip spans\");\n      }\n    }\n    metrics.incrementBytes(body.length);\n\n    final MockResponse result = new MockResponse();\n    if (body.length == 0) return result.setResponseCode(202); // lenient on empty\n\n    consumer.acceptSpans(body, decoder, new Callback<Void>() {\n      @Override public void onSuccess(Void value) {\n        result.setResponseCode(202);\n      }\n\n      @Override public void onError(Throwable t) {\n        String message = t.getMessage();\n        result.setBody(message).setResponseCode(message.startsWith(\"Cannot store\") ? 500 : 400);\n      }\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-junit5/src/main/java/zipkin2/junit5/ZipkinExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.junit5;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport okhttp3.mockwebserver.Dispatcher;\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\nimport org.junit.jupiter.api.extension.AfterEachCallback;\nimport org.junit.jupiter.api.extension.BeforeEachCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.collector.InMemoryCollectorMetrics;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static okhttp3.mockwebserver.SocketPolicy.KEEP_OPEN;\n\n/**\n * Starts up a local Zipkin server, listening for http requests on {@link #httpUrl}.\n *\n * <p>This can be used to test instrumentation. For example, you can POST spans directly to this\n * server.\n *\n * <p>See http://openzipkin.github.io/zipkin-api/#/\n */\npublic final class ZipkinExtension implements BeforeEachCallback, AfterEachCallback {\n  private final InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n  private final InMemoryCollectorMetrics metrics = new InMemoryCollectorMetrics();\n  private final MockWebServer server = new MockWebServer();\n  private final BlockingQueue<MockResponse> failureQueue = new LinkedBlockingQueue<>();\n  private final AtomicInteger receivedSpanBytes = new AtomicInteger();\n\n  public ZipkinExtension() {\n    final ZipkinDispatcher successDispatch = new ZipkinDispatcher(storage, metrics, server);\n    Dispatcher dispatcher = new Dispatcher() {\n      @Override public MockResponse dispatch(RecordedRequest request) {\n        MockResponse maybeFailure = failureQueue.poll();\n        if (maybeFailure != null) return maybeFailure;\n        MockResponse result = successDispatch.dispatch(request);\n        if (request.getMethod().equals(\"POST\")) {\n          receivedSpanBytes.addAndGet((int) request.getBodySize());\n        }\n        return result;\n      }\n\n      @Override public MockResponse peek() {\n        MockResponse maybeFailure = failureQueue.peek();\n        if (maybeFailure != null) return maybeFailure.clone();\n        return new MockResponse().setSocketPolicy(KEEP_OPEN);\n      }\n    };\n    server.setDispatcher(dispatcher);\n  }\n\n  /** Use this to connect. The zipkin v1 interface will be under \"/api/v1\" */\n  public String httpUrl() {\n    return \"http://%s:%s\".formatted(server.getHostName(), server.getPort());\n  }\n\n  /** Use this to see how many requests you've sent to any zipkin http endpoint. */\n  public int httpRequestCount() {\n    return server.getRequestCount();\n  }\n\n  /** Use this to see how many spans or serialized bytes were collected on the http endpoint. */\n  public InMemoryCollectorMetrics collectorMetrics() {\n    return metrics;\n  }\n\n  /**\n   * Stores the given spans directly, to setup preconditions for a test.\n   *\n   * <p>For example, if you are testing what happens when instrumentation adds a child to a trace,\n   * you'd add the parent here.\n   */\n  public ZipkinExtension storeSpans(List<Span> spans) {\n    try {\n      storage.accept(spans).execute();\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n    return this;\n  }\n\n  /**\n   * Adds a one-time failure to the http endpoint.\n   *\n   * <p>Ex. If you want to test that you don't repeatedly send bad data, you could send a 400 back.\n   *\n   * <pre>{@code\n   * zipkin.enqueueFailure(sendErrorResponse(400, \"bad format\"));\n   * }</pre>\n   *\n   * @param failure type of failure the next call to the http endpoint responds with\n   */\n  public ZipkinExtension enqueueFailure(HttpFailure failure) {\n    failureQueue.add(failure.response);\n    return this;\n  }\n\n  /** Retrieves all traces this zipkin server has received. */\n  public List<List<Span>> getTraces() {\n    return storage.spanStore().getTraces();\n  }\n\n  /** Retrieves a trace by ID which Zipkin server has received, or null if not present. */\n  @Nullable public List<Span> getTrace(String traceId) {\n    List<Span> result;\n    try {\n      result = storage.traces().getTrace(traceId).execute();\n    } catch (IOException e) {\n      throw new AssertionError(\"I/O exception in in-memory storage\", e);\n    }\n    // Note: this is a different behavior than Traces.getTrace() which is not nullable!\n    return result.isEmpty() ? null : result;\n  }\n\n  /** Retrieves all service links between traces this zipkin server has received. */\n  public List<DependencyLink> getDependencies() {\n    return storage.spanStore().getDependencies();\n  }\n\n  /**\n   * Used to manually start the server.\n   *\n   * @param httpPort choose 0 to select an available port\n   */\n  public void start(int httpPort) throws IOException {\n    server.start(httpPort);\n  }\n\n  /**\n   * Used to manually stop the server.\n   */\n  public void shutdown() throws IOException {\n    server.shutdown();\n  }\n\n  @Override public void beforeEach(ExtensionContext extensionContext) {\n  }\n\n  @Override public void afterEach(ExtensionContext extensionContext) {\n  }\n}\n"
  },
  {
    "path": "zipkin-junit5/src/test/java/zipkin2/junit5/ZipkinExtensionTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.junit5;\n\nimport java.io.IOException;\nimport java.util.List;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport okio.Buffer;\nimport okio.ByteString;\nimport okio.GzipSink;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\n\npublic class ZipkinExtensionTest {\n\n  @RegisterExtension public ZipkinExtension zipkin = new ZipkinExtension();\n\n  List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1]);\n  OkHttpClient client = new OkHttpClient();\n\n  @Test void getTraces_storedViaPost() throws IOException {\n    // write the span to the zipkin using http\n    assertPostSpansV1Success(CLIENT_SPAN);\n\n    // read the traces directly\n    assertThat(zipkin.getTraces()).containsOnly(List.of(CLIENT_SPAN));\n  }\n\n  @Test void getTraces_storedViaPostVersion2_json() throws IOException {\n    getTraces_storedViaPostVersion2(\"application/json\", SpanBytesEncoder.JSON_V2);\n  }\n\n  @Test void getTraces_storedViaPostVersion2_proto3() throws IOException {\n    getTraces_storedViaPostVersion2(\"application/x-protobuf\", SpanBytesEncoder.PROTO3);\n  }\n\n  void getTraces_storedViaPostVersion2(String mediaType, SpanBytesEncoder encoder)\n    throws IOException {\n\n    byte[] message = encoder.encodeList(spans);\n\n    // write the span to the zipkin using http api v2\n    try (Response response = client.newCall(\n      new Request.Builder().url(zipkin.httpUrl() + \"/api/v2/spans\")\n        .post(RequestBody.create(MediaType.parse(mediaType), message))\n        .build()).execute()) {\n      assertThat(response.code()).isEqualTo(202);\n    }\n\n    // read the traces directly\n    assertThat(zipkin.getTraces()).containsOnly(List.of(spans.get(0)), List.of(spans.get(1)));\n  }\n\n  /** The rule is here to help debugging. Even partial spans should be returned */\n  @Test void getTraces_whenMissingTimestamps() throws IOException {\n    Span span = Span.newBuilder().traceId(\"1\").id(\"1\").name(\"foo\").build();\n    // write the span to the zipkin using http\n    assertPostSpansV1Success(span);\n\n    // read the traces directly\n    assertThat(zipkin.getTraces()).containsOnly(List.of(span));\n  }\n\n  /** The raw query can show affects like redundant rows in the data store. */\n  @Test void storeSpans_readbackRaw() {\n    Span missingDuration = LOTS_OF_SPANS[0].toBuilder().duration(null).build();\n    Span withDuration = LOTS_OF_SPANS[0];\n\n    // write the span to zipkin directly\n    zipkin.storeSpans(List.of(missingDuration));\n    zipkin.storeSpans(List.of(withDuration));\n\n    assertThat(zipkin.getTrace(missingDuration.traceId())).containsExactly(missingDuration,\n      withDuration);\n  }\n\n  @Test void httpRequestCountIncrements() throws IOException {\n    assertPostSpansV1Success(spans);\n    assertPostSpansV1Success(spans);\n\n    assertThat(zipkin.httpRequestCount()).isEqualTo(2);\n  }\n\n  /**\n   * Normally, a span can be reported twice: for client and server. However, there are bugs that\n   * happened where several updates went to the same span id. {@link ZipkinExtension#collectorMetrics}\n   * can be used to help ensure a span isn't reported more times than expected.\n   */\n  @Test void collectorMetrics_spans() throws IOException {\n    assertPostSpansV1Success(LOTS_OF_SPANS[0]);\n    assertPostSpansV1Success(LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);\n    assertThat(zipkin.collectorMetrics().spans()).isEqualTo(3);\n  }\n\n  @Test void postSpans_disconnectDuringBody() throws IOException {\n    zipkin.enqueueFailure(HttpFailure.disconnectDuringBody());\n\n    try (Response response = postSpansV1(spans)) {\n      failBecauseExceptionWasNotThrown(IOException.class);\n    } catch (IOException expected) { // not always a ConnectException!\n    }\n\n    // Zipkin didn't store the spans, as they shouldn't have been readable, due to disconnect\n    assertThat(zipkin.getTraces()).isEmpty();\n\n    // create a new connection pool to avoid flakey tests\n    client = new OkHttpClient();\n\n    // The failure shouldn't affect later requests\n    assertPostSpansV1Success(spans);\n  }\n\n  @Test void postSpans_sendErrorResponse400() throws IOException {\n    zipkin.enqueueFailure(HttpFailure.sendErrorResponse(400, \"Invalid Format\"));\n\n    try (Response response = postSpansV1(spans)) {\n      assertThat(response.code()).isEqualTo(400);\n      assertThat(response.body().string()).isEqualTo(\"Invalid Format\");\n    }\n\n    // Zipkin didn't store the spans, as they shouldn't have been readable, due to the error\n    assertThat(zipkin.getTraces()).isEmpty();\n\n    // The failure shouldn't affect later requests\n    assertPostSpansV1Success(spans);\n  }\n\n  @Test void gzippedSpans() throws IOException {\n    byte[] spansInJson = SpanBytesEncoder.JSON_V1.encodeList(spans);\n\n    ByteString gzippedJson;\n    try (Buffer sink = new Buffer(); Buffer source = new Buffer()) {\n      source.write(spansInJson);\n      GzipSink gzipSink = new GzipSink(sink);\n      gzipSink.write(source, spansInJson.length);\n      gzipSink.close();\n      gzippedJson = sink.readByteString();\n    }\n\n    try (Response response = client.newCall(\n      new Request.Builder().url(zipkin.httpUrl() + \"/api/v1/spans\")\n        .addHeader(\"Content-Encoding\", \"gzip\")\n        .post(RequestBody.create(MediaType.parse(\"application/json\"), gzippedJson))\n        .build()).execute()) {\n      assertThat(response.code()).isEqualTo(202);\n    }\n\n    assertThat(zipkin.collectorMetrics().bytes()).isEqualTo(spansInJson.length);\n  }\n\n  Response postSpansV1(List<Span> spans) throws IOException {\n    byte[] spansInJson = SpanBytesEncoder.JSON_V1.encodeList(spans);\n    return client.newCall(new Request.Builder().url(zipkin.httpUrl() + \"/api/v1/spans\")\n      .post(RequestBody.create(MediaType.parse(\"application/json\"), spansInJson))\n      .build()).execute();\n  }\n\n  void assertPostSpansV1Success(Span... spans) throws IOException {\n    assertPostSpansV1Success(List.of(spans));\n  }\n\n  void assertPostSpansV1Success(List<Span> spans) throws IOException {\n    try (Response response = postSpansV1(spans)) {\n      assertThat(response.code()).isEqualTo(202);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-junit5/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n"
  },
  {
    "path": "zipkin-lens/.eslintignore",
    "content": "src/translations\n"
  },
  {
    "path": "zipkin-lens/.eslintrc.js",
    "content": "module.exports = {\n  parser: '@typescript-eslint/parser', // Specifies the ESLint parser\n  extends: [\n    'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react\n    'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin\n    'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier\n    'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.\n  ],\n  parserOptions: {\n    ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features\n    sourceType: 'module', // Allows for the use of imports\n    ecmaFeatures: {\n      jsx: true, // Allows for the parsing of JSX\n    },\n  },\n  rules: {\n    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs\n    // e.g. \"@typescript-eslint/explicit-function-return-type\": \"off\",\n    '@typescript-eslint/ban-ts-ignore': 'off',\n    '@typescript-eslint/no-explicit-any': 'off',\n    '@typescript-eslint/explicit-function-return-type': 'off',\n    '@typescript-eslint/no-non-null-assertion': 'off',\n    'react/display-name': 'off',\n    \"react/prop-types\": \"off\"\n  },\n  settings: {\n    react: {\n      version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use\n    },\n  },\n};\n"
  },
  {
    "path": "zipkin-lens/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "zipkin-lens/.linguirc",
    "content": "{\n  \"locales\": [\n    \"en\",\n    \"es\",\n    \"fr\",\n    \"zh-cn\"\n  ],\n  \"sourceLocale\": \"en\",\n  \"catalogs\": [\n    {\n      \"path\": \"src/translations/{locale}/messages\",\n      \"include\": [\n        \"src/\"\n      ],\n      \"exclude\": [\n        \"*/node_modules/*\"\n      ]\n    }\n  ],\n  \"format\": \"po\",\n  \"compileNamespace\": \"ts\"\n}\n"
  },
  {
    "path": "zipkin-lens/.npmrc",
    "content": "# This is needed while dependencies rely on different versions of react \nlegacy-peer-deps=true\n"
  },
  {
    "path": "zipkin-lens/.prettierrc.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nmodule.exports = {\n  singleQuote: true,\n  trailingComma: 'all',\n};\n"
  },
  {
    "path": "zipkin-lens/README.md",
    "content": "# Zipkin Lens\n\nZipkin-lens is the UI for [Zipkin](https://github.com/openzipkin/zipkin). It is a modern replacement of the [classic](https://github.com/openzipkin-attic/zipkin-classic) UI which has proved its merit since the beginning of the Zipkin project.\n\nHere are a couple example screenshots:\n\n<img width=\"1920\" alt=\"Search Screen\" src=\"https://user-images.githubusercontent.com/64215/49579677-4602de00-f990-11e8-81b7-dd782ce91227.png\">\n<img width=\"1920\" alt=\"Trace Detail Screen\" src=\"https://user-images.githubusercontent.com/64215/49579684-4d29ec00-f990-11e8-8799-5c53a503413e.png\">\n\n## Quick start\n\nIn the project directory, you can run:\n\n### `npm start`\n\nRuns the app in the development mode.<br />\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n\nThe page will reload if you make edits.<br />\nYou will also see any lint errors in the console.\n\nBy default, API requests are proxied to `http://localhost:9411`. You can change this target using\nthe `API_BASE` environment variable, e.g., `API_BASE=http://tracing.company.com npm start`.\n\n### `npm test`\n\nLaunches the test runner in the interactive watch mode.<br />\nSee the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.\n\nTo get a coverage report as well, run `npm test -- --coverage`.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.<br />\nIt correctly bundles React in production mode and optimizes the build for the best performance.\n\n## Build tips\n\n### Maven build\n\nThis project is not published to NPM, rather Maven, as the primary consumer is [zipkin-server](../zipkin-server).\n`../mvnw clean install` installs and builds via NPM. The resulting assets, such as index.html, are\nplaced into the \"zipkin-lens\" directory of zipkin-lens.jar. This is published on release as\n[io.zipkin:zipkin-lens](https://central.sonatype.com/search?q=io.zipkin%3Azipkin-lens).\n\n### Use the production node version\n\nThe production UI is built with Maven. To use the same version, issue this command:\n\n```bash\nnvm use $(../mvnw help:evaluate -Dexpression=node.version -q -DforceStdout)\n```\n\nNow, it is less likely a pull request will fail when `npm test` succeeds locally.\n\n## Localization\n\nWe use [LinguiJS](https://lingui.js.org/) for localization of the UI. Translations for strings are\nfound in the JSON files under [here](src/translations). The Javascript files in the directory are\ncompiled from the JSON files. We're always excited to have help maintaining these translations - if\nyou see a string in the UI that is not translated or mistranslated, please feel free to send a PR to\nthe JSON file to fix it. If you can, please run `yarn run compile` to also compile the translation\ninto the output. If it's tedious to set up an environment for it, though, don't worry we'll take care\nof it.\n\n### Adding a new locale\n\nTo add a new translated locale, first edit [.linguirc](.linguirc) and add the locale to the\n`locales` section. Next, run `yarn run extract` to extract a new file under `src/translations` for\nthe locale. Translate as many strings in the JSON file as you can. Then run `yarn run compile` to\ncompile the strings.\n\nFinally, edit [App.tsx](src/components/App/App.tsx) and\n[LanguageSelector.tsx](src/components/App/LanguageSelector.tsx) to import the new translation and\nadd an entry to the language selector respectively.\n\n## Dev Tools\n\nAs the app is a SPA using React, it can be difficult to debug issues using native browser tools because\nthe HTML does not map directly to any source code. It is recommended to install these extensions if you\nneed to debug the page (they will only work with the local dev server, not a production build).\n\n### React Developer Tools\n\nChrome: https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi\nFirefox: https://addons.mozilla.org/en-US/firefox/addon/react-devtools/\n\n### Redux DevTools\n\nChrome: https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd\nFirefox: https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/\n\n## Running behind a reverse proxy\nSince version `2.20`, Zipkin Lens supports running under an arbitrary context root. As a result,\nit can be proxied under a different path than `/zipkin` such as `/admin/zipkin`.\n\nAs an example, here is the configuration for Apache HTTP Server acting as a reverse proxy\nfor a Zipkin instance running on the same host:\n\n```\nLoadModule proxy_module lib/httpd/modules/mod_proxy.so\nLoadModule proxy_http_module lib/httpd/modules/mod_proxy_http.so\n\nProxyPass \"/admin/zipkin\"  \"http://localhost:9411/zipkin\"\nProxyPassReverse \"/admin/zipkin\"  \"http://localhost:9411/zipkin\"\n```\n\nFor the reverse proxy configuration to work, Zipkin needs to be started with the `zipkin.ui.basepath`\nparameter pointing to the proxy path:\n\n```bash\njava -jar zipkin.jar --zipkin.ui.basepath=/admin/zipkin\n```\n\nor via docker\n```bash\ndocker run -e ZIPKIN_UI_BASEPATH=/admin/zipkin -p 9411:9411 openzipkin/zipkin\n```\n\n## Authentication / Authorization\n\nZipkin Lens can be secured by running it behind an authenticating proxy like [Apache HTTPD](https://httpd.apache.org/docs/current/howto/auth.html), [Nginx](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) or similar.\n\n## Acknowledgements\nZipkin Lens design includes facets of old and new designs. Here are\nsome notable new aspects borrowed or adapted from others.\n\n### Overall Design\nZipkin Lens was originally an internal UI at LINE called IMON. One driving feature was to\nallow site-specific tags like \"Phase\" and \"Instance Id\" to be choices for trace queries.\nThe UI was originally matching color schemes of other tools like Grafana and fit in with\nthe Observability ecosystem of LINE engineering. IMON was created by Igarashi Takuma and\nHuy Do who later contributed it as the initial version of Zipkin Lens.\n\n### Trace Mini Map\nThe original Zipkin UI trace detail screen had a Zoom feature which is\nhelpful for looking at nuance in a large trace by expanding the\ntimeline around a group of spans. However, it did not help with\nnavigating to these spans. We discussed the idea of a mini-map,\nsimilar to what video games use to quickly scroll to a place of\ninterest in a game. This is especially important in messaging spans\nwhere there can be a large time gap separating clusters of spans. The\ninitial mini-map implementation in Lens is very similar to work in\nJaeger UI, as a mini-map occurs over the trace and lets you zoom to a\ntimeframe similar to Chrome or FireFox debug tools. The implementation\nwas different as it is implemented with SVG, which is easier to debug\nthan Canvas.\n\n### Span Detail pop-under\nThe original Zipkin UI trace detail screen would pop-out span details\nwhen clicking on a span. This had two problems: One was that the\npop-out blocked your position in the trace, so after you close the\npop-out you need to find it again. Another problem was that you cannot\nview two span details at the same time. Lens pops these details under\na selected span. Although the data is the same as the old Zipkin UI,\nthe gesture is the same as in Jaeger's UI and inspired by them.\n\n### Vizceral Dependencies graph\nThe original Zipkin UI dependencies screen used a D3 library which\nconnected services with curved arrows. These arrows varied in\nthickness depending on the traffic. When you clicked an arrow, details\nwould pop out. The initial version of Lens uses the same library as\nHaystack Ui to present service dependencies: Netflix Vizceral.\n"
  },
  {
    "path": "zipkin-lens/index.html",
    "content": "<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<!DOCTYPE html>\n<html>\n  <head>\n    <base href=\"/zipkin/\" >\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <title>Zipkin</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "zipkin-lens/javadoc/README.md",
    "content": "Maven central publication requires a JavaDoc jar. This exists to satisfy that.\n"
  },
  {
    "path": "zipkin-lens/package.json",
    "content": "{\n  \"name\": \"zipkin-lens\",\n  \"version\": \"0.0.1\",\n  \"homepage\": \".\",\n  \"private\": true,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/openzipkin/zipkin\"\n  },\n  \"author\": \"https://gitter.im/openzipkin/zipkin\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"@date-io/moment\": \"^1.3.7\",\n    \"@fortawesome/fontawesome-svg-core\": \"^1.2.25\",\n    \"@fortawesome/free-brands-svg-icons\": \"^5.11.2\",\n    \"@fortawesome/free-solid-svg-icons\": \"^5.11.2\",\n    \"@fortawesome/react-fontawesome\": \"^0.1.7\",\n    \"@material-ui/core\": \"^4.1.1\",\n    \"@material-ui/data-grid\": \"4.0.0-alpha.24\",\n    \"@material-ui/icons\": \"^4.9.1\",\n    \"@material-ui/lab\": \"^4.0.0-alpha.50\",\n    \"@material-ui/pickers\": \"^3.1.1\",\n    \"@material-ui/styles\": \"^4.1.1\",\n    \"@reduxjs/toolkit\": \"^1.3.4\",\n    \"@testing-library/react\": \"^10.4.9\",\n    \"@testing-library/react-hooks\": \"^3.2.1\",\n    \"@testing-library/user-event\": \"^10.4.1\",\n    \"@types/classnames\": \"^2.2.10\",\n    \"@types/enzyme\": \"^3.10.5\",\n    \"@types/enzyme-adapter-react-16\": \"^1.0.6\",\n    \"@types/fetch-mock\": \"^7.3.8\",\n    \"@types/node\": \"^13.13.0\",\n    \"@types/react\": \"^16.9.23\",\n    \"@types/react-dom\": \"^16.9.5\",\n    \"@types/react-redux\": \"^7.1.7\",\n    \"@types/react-router-dom\": \"^5.1.3\",\n    \"@types/recharts\": \"^2.0.0\",\n    \"@types/redux-mock-store\": \"^1.0.2\",\n    \"@types/shortid\": \"0.0.29\",\n    \"@types/styled-components\": \"^5.1.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.34.0\",\n    \"@typescript-eslint/parser\": \"^2.23.0\",\n    \"classnames\": \"^2.2.6\",\n    \"enzyme\": \"^3.7.0\",\n    \"enzyme-adapter-react-16\": \"^1.7.0\",\n    \"eslint\": \"^6.8.0\",\n    \"eslint-config-airbnb-typescript\": \"^7.0.0\",\n    \"eslint-config-prettier\": \"^6.10.0\",\n    \"eslint-plugin-import\": \"^2.16.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.2.1\",\n    \"eslint-plugin-prettier\": \"^3.1.2\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^2.5.0\",\n    \"events\": \"^3.3.0\",\n    \"flatted\": \"^3.4.1\",\n    \"fetch-mock\": \"^9.11.0\",\n    \"history\": \"^4.10.1\",\n    \"http-proxy-middleware\": \"^2.0.7\",\n    \"i18next\": \"^23.8.2\",\n    \"i18next-browser-languagedetector\": \"^7.2.0\",\n    \"lodash\": \"^4.17.20\",\n    \"minimatch\": \"^10.2.4\",\n    \"moment\": \"^2.24.0\",\n    \"node-fetch\": \"^2.6.1\",\n    \"prettier\": \"^2.0.4\",\n    \"prop-types\": \"^15.6.2\",\n    \"react\": \"^16.13.0\",\n    \"react-dom\": \"^16.13.0\",\n    \"react-i18next\": \"^14.0.5\",\n    \"react-redux\": \"^7.1.0\",\n    \"react-router-dom\": \"^5.0.1\",\n    \"react-use\": \"^14.2.0\",\n    \"react-virtualized-auto-sizer\": \"1.0.7\",\n    \"react-window\": \"^1.8.8\",\n    \"recharts\": \"^2.3.2\",\n    \"redux\": \"^4.0.0\",\n    \"redux-mock-store\": \"^1.5.4\",\n    \"redux-thunk\": \"^2.3.0\",\n    \"rollup\": \"^4.59.0\",\n    \"shortid\": \"^2.2.14\",\n    \"styled-components\": \"^5.1.1\",\n    \"vitest\": \"^1.6.1\",\n    \"vizceral-react\": \"^4.6.5\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"start\": \"vite\",\n    \"test\": \"vitest\",\n    \"build\": \"tsc&& vite build\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">1%\",\n      \"not ie 11\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"@babel/preset-react\": \"^7.18.6\",\n    \"@babel/preset-typescript\": \"^7.18.6\",\n    \"@types/react-virtualized-auto-sizer\": \"^1.0.1\",\n    \"@types/react-window\": \"^1.8.5\",\n    \"happy-dom\": \"^8.2.6\",\n    \"identity-obj-proxy\": \"^3.0.0\",\n    \"typescript\": \"4.9.4\",\n    \"vite\": \"^4.1.1\",\n    \"vite-plugin-eslint\": \"^1.8.1\"\n  }\n}\n"
  },
  {
    "path": "zipkin-lens/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-lens</artifactId>\n  <name>Zipkin Lens</name>\n  <description>Repackages Zipkin Lens into a jar, so we can use it in zipkin-server</description>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n    <npm.skipTests>false</npm.skipTests>\n\n    <!-- Update occasionally based on LTS, but don't overrun what's in Alpine:\n         https://nodejs.org/en/download/package-manager\n         https://pkgs.alpinelinux.org/packages?name=nodejs&branch=edge -->\n    <node.version>22.13.1</node.version>\n    <exec-maven-plugin.version>3.5.0</exec-maven-plugin.version>\n    <frontend-maven-plugin.version>1.15.1</frontend-maven-plugin.version>\n    <maven-clean-plugin.version>3.4.0</maven-clean-plugin.version>\n    <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>\n  </properties>\n\n  <dependencies>\n    <!-- no dependencies as this is just javascript -->\n  </dependencies>\n\n  <build>\n    <pluginManagement>\n      <plugins>\n        <plugin>\n          <groupId>org.codehaus.mojo</groupId>\n          <artifactId>exec-maven-plugin</artifactId>\n          <version>${exec-maven-plugin.version}</version>\n        </plugin>\n        <plugin>\n          <groupId>com.github.eirslett</groupId>\n          <artifactId>frontend-maven-plugin</artifactId>\n          <version>${frontend-maven-plugin.version}</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-clean-plugin</artifactId>\n          <version>${maven-clean-plugin.version}</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-resources-plugin</artifactId>\n          <version>${maven-resources-plugin.version}</version>\n        </plugin>\n      </plugins>\n    </pluginManagement>\n\n    <plugins>\n      <plugin>\n        <artifactId>maven-clean-plugin</artifactId>\n        <executions>\n          <execution>\n            <id>remove existing NPM build</id>\n            <phase>compile</phase>\n            <goals>\n              <goal>clean</goal>\n            </goals>\n            <configuration>\n              <excludeDefaultDirectories>true</excludeDefaultDirectories>\n              <filesets>\n                <fileset>\n                  <directory>build</directory>\n                </fileset>\n              </filesets>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <groupId>com.github.eirslett</groupId>\n        <artifactId>frontend-maven-plugin</artifactId>\n        <configuration>\n          <installDirectory>target</installDirectory>\n          <nodeVersion>v${node.version}</nodeVersion>\n          <environmentVariables>\n            <!--\n            create-react-app runs tests in watch mode unless this is defined. We define it here for running Maven locally.\n            -->\n            <CI>true</CI>\n          </environmentVariables>\n        </configuration>\n        <executions>\n          <execution>\n            <id>install node and npm</id>\n            <goals>\n              <goal>install-node-and-npm</goal>\n            </goals>\n          </execution>\n          <execution>\n            <id>npm install</id>\n            <goals>\n              <goal>npm</goal>\n            </goals>\n            <configuration>\n              <arguments>install</arguments>\n            </configuration>\n          </execution>\n          <execution>\n            <id>npm lint</id>\n            <goals>\n              <goal>npm</goal>\n            </goals>\n            <phase>compile</phase>\n            <configuration>\n              <arguments>run lint</arguments>\n            </configuration>\n          </execution>\n          <execution>\n            <id>npm run build</id>\n            <goals>\n              <goal>npm</goal>\n            </goals>\n            <phase>compile</phase>\n            <configuration>\n              <arguments>run build</arguments>\n            </configuration>\n          </execution>\n          <execution>\n            <id>npm run test</id>\n            <goals>\n              <goal>npm</goal>\n            </goals>\n            <phase>test</phase>\n            <configuration>\n              <skipTests>${npm.skipTests}</skipTests>\n              <arguments>run test</arguments>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <artifactId>maven-resources-plugin</artifactId>\n        <version>${maven-resources-plugin.version}</version>\n        <executions>\n          <execution>\n            <id>copy NPM build to zipkin-lens directory</id>\n            <!-- This needs to happen after compile, or it could copy an empty directory! -->\n            <phase>prepare-package</phase>\n            <goals>\n              <goal>copy-resources</goal>\n            </goals>\n            <configuration>\n              <!-- NPM build output will end up in the jar under the 'zipkin-lens' directory -->\n              <outputDirectory>${project.build.directory}/classes/zipkin-lens</outputDirectory>\n              <resources>\n                <resource>\n                  <directory>build</directory>\n                </resource>\n              </resources>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <!-- remove the META-INF directory, as this is just static assets -->\n      <plugin>\n        <artifactId>maven-shade-plugin</artifactId>\n        <executions>\n          <execution>\n            <phase>package</phase>\n            <goals>\n              <goal>shade</goal>\n            </goals>\n            <configuration>\n              <filters>\n                <filter>\n                  <artifact>*:*</artifact>\n                  <excludes>\n                    <exclude>META-INF/</exclude>\n                  </excludes>\n                </filter>\n              </filters>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n\n  <profiles>\n    <!-- Allows us to use a true value in maven-exec-plugin when skipTests exist only in name -->\n    <profile>\n      <id>normalize skipTests</id>\n      <activation>\n        <property>\n          <name>skipTests</name>\n        </property>\n      </activation>\n      <properties>\n        <npm.skipTests>true</npm.skipTests>\n      </properties>\n    </profile>\n\n    <!-- frontend-maven-plugin requires downloading via a public URL, and suggests exec-maven-plugin\n         otherwise.\n\n         ARM64 is not supported with musl, yet https://github.com/nodejs/node/blob/master/BUILDING.md\n         See issue #3166\n\n         There are problems on alpine+arm64 with posix_spawn. https://github.com/openzipkin/docker-java/issues/34\n         So, always execute exporting MAVEN_OPTS=-Djdk.lang.Process.launchMechanism=vfork -->\n    <profile>\n      <id>exec-maven-plugin</id>\n      <activation>\n        <os>\n          <arch>aarch64</arch>\n        </os>\n        <file>\n          <exists>/etc/alpine-release</exists>\n        </file>\n      </activation>\n      <build>\n        <plugins>\n          <!-- It isn't currently possible to disable frontend-maven-plugin.\n               Instead, we set each execution to none -->\n          <plugin>\n            <groupId>com.github.eirslett</groupId>\n            <artifactId>frontend-maven-plugin</artifactId>\n            <executions>\n              <execution>\n                <id>install node and npm</id>\n                <phase>none</phase>\n              </execution>\n              <execution>\n                <id>npm install</id>\n                <phase>none</phase>\n              </execution>\n              <execution>\n                <id>npm lint</id>\n                <phase>none</phase>\n              </execution>\n              <execution>\n                <id>npm run build</id>\n                <phase>none</phase>\n              </execution>\n              <execution>\n                <id>npm run test</id>\n                <phase>none</phase>\n              </execution>\n            </executions>\n          </plugin>\n          <!-- This duplicates exactly what we did in frontend-maven-plugin -->\n          <plugin>\n            <groupId>org.codehaus.mojo</groupId>\n            <artifactId>exec-maven-plugin</artifactId>\n            <configuration>\n              <environmentVariables>\n                <!--\n                create-react-app runs tests in watch mode unless this is defined. We define it here for running Maven locally.\n                -->\n                <CI>true</CI>\n              </environmentVariables>\n            </configuration>\n            <executions>\n              <execution>\n                <id>npm install</id>\n                <goals>\n                  <goal>exec</goal>\n                </goals>\n                <phase>generate-resources</phase>\n                <configuration>\n                  <executable>npm</executable>\n                  <arguments>\n                    <argument>install</argument>\n                  </arguments>\n                </configuration>\n              </execution>\n              <execution>\n                <id>npm run build</id>\n                <goals>\n                  <goal>exec</goal>\n                </goals>\n                <phase>compile</phase>\n                <configuration>\n                  <executable>npm</executable>\n                  <arguments>\n                    <argument>run</argument>\n                    <argument>build</argument>\n                  </arguments>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n\n    <profile>\n      <id>release</id>\n      <build>\n        <plugins>\n          <!-- Creates empty javadoc jar -->\n          <plugin>\n            <artifactId>maven-jar-plugin</artifactId>\n            <executions>\n              <execution>\n                <id>empty-javadoc-jar</id>\n                <phase>package</phase>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n                <configuration>\n                  <classifier>javadoc</classifier>\n                  <classesDirectory>${project.basedir}/javadoc</classesDirectory>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n  </profiles>\n</project>\n"
  },
  {
    "path": "zipkin-lens/src/components/App/AlertSnackbar.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport Snackbar from '@material-ui/core/Snackbar';\nimport Alert from '@material-ui/lab/Alert';\nimport React, { useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { RootState } from '../../store';\n\nimport { clearAlert } from './slice';\n\n/**\n * Renders a message at the top of the page, usually to indicate success / failure of a background\n * action.\n */\nconst AlertSnackbar: React.FC = () => {\n  const dispatch = useDispatch();\n  const { alert, alertOpen } = useSelector((state: RootState) => state.app);\n  const onSnackbarClose = useCallback(() => dispatch(clearAlert()), [dispatch]);\n  return (\n    <Snackbar\n      open={alertOpen}\n      autoHideDuration={3000}\n      onClose={onSnackbarClose}\n      anchorOrigin={{\n        vertical: 'top',\n        horizontal: 'center',\n      }}\n    >\n      <Alert elevation={6} variant=\"filled\" severity={alert.severity}>\n        {alert.message}\n      </Alert>\n    </Snackbar>\n  );\n};\n\nexport default AlertSnackbar;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/App.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport MomentUtils from '@date-io/moment';\nimport {\n  CircularProgress,\n  ThemeProvider as MuiThemeProvider,\n} from '@material-ui/core';\nimport { MuiPickersUtilsProvider } from '@material-ui/pickers';\nimport React, { Suspense, useMemo } from 'react';\nimport { Provider } from 'react-redux';\nimport { BrowserRouter, Route } from 'react-router-dom';\nimport { useTitle } from 'react-use';\nimport { ThemeProvider } from 'styled-components';\n\nimport Layout from './Layout';\nimport DependenciesPage from '../DependenciesPage';\nimport DiscoverPage from '../DiscoverPage';\nimport TracePage from '../TracePage';\nimport { UiConfig, UiConfigConsumer } from '../UiConfig';\nimport configureStore from '../../store/configure-store';\nimport { theme } from '../../constants/color';\nimport AlertSnackbar from './AlertSnackbar';\nimport { BASE_PATH } from '../../constants/api';\n\nconst App: React.FC = () => {\n  useTitle('Zipkin');\n  const baseName = useMemo(() => {\n    return import.meta.env.DEV ? '/zipkin' : BASE_PATH;\n  }, []);\n\n  return (\n    <Suspense fallback={<CircularProgress />}>\n      <UiConfig>\n        <MuiPickersUtilsProvider utils={MomentUtils}>\n          <ThemeProvider theme={theme}>\n            <MuiThemeProvider theme={theme}>\n              <UiConfigConsumer>\n                {(config) => (\n                  <Provider store={configureStore(config)}>\n                    <AlertSnackbar />\n                    <BrowserRouter basename={baseName}>\n                      <Layout>\n                        <Route exact path=\"/\" component={DiscoverPage} />\n                        {config.dependency.enabled && (\n                          <Route\n                            exact\n                            path=\"/dependency\"\n                            component={DependenciesPage}\n                          />\n                        )}\n                        <Route\n                          exact\n                          path={['/traces/:traceId', '/traceViewer']}\n                          component={TracePage}\n                        />\n                      </Layout>\n                    </BrowserRouter>\n                  </Provider>\n                )}\n              </UiConfigConsumer>\n            </MuiThemeProvider>\n          </ThemeProvider>\n        </MuiPickersUtilsProvider>\n      </UiConfig>\n    </Suspense>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/HeaderMenuItem.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { IconProp } from '@fortawesome/fontawesome-svg-core';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { Theme, Typography, createStyles, makeStyles } from '@material-ui/core';\nimport classNames from 'classnames';\nimport React from 'react';\nimport { Link, useLocation } from 'react-router-dom';\nimport styled from 'styled-components';\nimport { getTheme } from '../../util/theme';\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    root: {\n      minHeight: 64,\n      paddingRight: theme.spacing(2),\n      paddingLeft: theme.spacing(2),\n      color: theme.palette.grey[500],\n      textDecoration: 'none',\n      borderTop: `2px solid transparent`,\n      borderBottom: `2px solid transparent`,\n      display: 'flex',\n      alignItems: 'center',\n      justifyContent: 'center',\n      cursor: 'pointer',\n      '&:hover': {\n        color:\n          getTheme() === 'dark'\n            ? theme.palette.grey[200]\n            : theme.palette.grey[100],\n      },\n    },\n    'root--selected': {\n      color: theme.palette.common.white,\n      borderBottom: `2px solid ${theme.palette.common.white}`,\n    },\n  }),\n);\n\ninterface HeaderMenuItemProps {\n  icon: IconProp;\n  title: string;\n  path: string;\n}\n\nconst HeaderMenuItem: React.FC<HeaderMenuItemProps> = ({\n  icon,\n  title,\n  path,\n}) => {\n  const classes = useStyles();\n  const location = useLocation();\n  const isSelected = path === location.pathname;\n\n  return (\n    <Link\n      to={path}\n      className={classNames(classes.root, {\n        [classes['root--selected']]: isSelected,\n      })}\n    >\n      <FontAwesomeIcon icon={icon} size=\"lg\" />\n      <Title>{title}</Title>\n    </Link>\n  );\n};\n\nexport default HeaderMenuItem;\n\nconst Title = styled(Typography).attrs({\n  variant: 'body1',\n})`\n  margin-left: ${({ theme }) => theme.spacing(1.5)}px;\n`;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/LanguageSelector.test.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { fireEvent, screen } from '@testing-library/react';\nimport React from 'react';\n\nimport render from '../../test/util/render-with-default-settings';\nimport { describe, it, expect } from 'vitest';\nimport LanguageSelector from './LanguageSelector';\nimport i18n from '../../translations/i18n';\n\ndescribe('<LanguageSelector />', () => {\n  it('displays button', async () => {\n    render(<LanguageSelector />);\n    const changeLanguageButton = screen.getByTestId('change-language-button');\n    expect(changeLanguageButton).toBeDefined();\n    i18n.language;\n    expect(i18n.language).toEqual('en');\n  });\n\n  it('displays all languages', async () => {\n    render(<LanguageSelector />);\n    expect(screen.getAllByTestId('language-list-item-en')).toBeDefined();\n    expect(screen.getAllByTestId('language-list-item-es')).toBeDefined();\n    expect(screen.getAllByTestId('language-list-item-fr')).toBeDefined();\n    expect(screen.getAllByTestId('language-list-item-zh_cn')).toBeDefined();\n    expect(i18n.language).toEqual('en');\n  });\n\n  it('language select changes locale and refreshes', async () => {\n    render(<LanguageSelector />);\n    fireEvent.click(screen.getAllByTestId('language-list-item-zh_cn')[0]);\n    expect(i18n.language).toEqual('zh_cn');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/App/LanguageSelector.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Button, Menu, MenuItem } from '@material-ui/core';\nimport ExpandMoreIcon from '@material-ui/icons/ExpandMore';\nimport TranslateIcon from '@material-ui/icons/Translate';\nimport React, { useCallback, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { FALLBACK_LOCALE } from '../../translations/i18n';\n\n// We want to display all the languages in native language, not current locale, so hard-code the\n// strings here instead of using internationalization.\n//\n// Exported for testing\nexport const LANGUAGES = [\n  {\n    locale: 'en',\n    name: 'English',\n    iso6391: 'EN',\n  },\n  {\n    locale: 'es',\n    name: 'Español',\n    iso6391: 'ES',\n  },\n  {\n    locale: 'fr',\n    name: 'Français',\n    iso6391: 'FR',\n  },\n  {\n    locale: 'zh_cn',\n    name: '中文 (简体)',\n    iso6391: 'ZH',\n  },\n];\n\nconst LanguageSelector = () => {\n  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);\n  const { i18n } = useTranslation();\n\n  const handleButtonClick = useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      event.preventDefault();\n      setAnchorEl(event.currentTarget);\n    },\n    [],\n  );\n\n  const handleMenuClose = useCallback(() => {\n    setAnchorEl(null);\n  }, []);\n\n  const handleMenuItemClick = useCallback(\n    (event: React.MouseEvent<HTMLLIElement>) => {\n      setAnchorEl(null);\n      const { locale } = event.currentTarget.dataset;\n      if (!locale) {\n        return;\n      }\n      if (locale === i18n.language) {\n        return;\n      }\n\n      i18n.changeLanguage(locale);\n    },\n    [i18n],\n  );\n\n  useEffect(() => {\n    if (LANGUAGES.find((lang) => lang.locale === i18n.language)) {\n      i18n.changeLanguage(i18n.language);\n    } else {\n      i18n.changeLanguage(FALLBACK_LOCALE); // fallback to default language if the selected language is not supported\n    }\n  }, [i18n]);\n\n  return (\n    <>\n      <Button\n        onClick={handleButtonClick}\n        startIcon={<TranslateIcon />}\n        endIcon={<ExpandMoreIcon />}\n        data-testid=\"change-language-button\"\n      >\n        {LANGUAGES.find((lang) => lang.locale === i18n.language)?.iso6391}\n      </Button>\n      <Menu\n        anchorEl={anchorEl}\n        keepMounted\n        open={Boolean(anchorEl)}\n        onClose={handleMenuClose}\n      >\n        {LANGUAGES.map((lang) => (\n          <MenuItem\n            key={lang.locale}\n            onClick={handleMenuItemClick}\n            selected={lang.locale === i18n.language}\n            data-locale={lang.locale}\n            data-testid={`language-list-item-${lang.locale}`}\n          >\n            {lang.name}\n          </MenuItem>\n        ))}\n      </Menu>\n    </>\n  );\n};\n\nexport default LanguageSelector;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/Layout.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport React from 'react';\n\nimport { describe, it, expect, afterEach } from 'vitest';\nimport { cleanup, screen } from '@testing-library/react';\nimport render from '../../test/util/render-with-default-settings';\nimport Layout from './Layout';\n\ndescribe('<Layout />', () => {\n  afterEach(cleanup);\n  it('does not render help link with default config', () => {\n    // children is required so avoid warning by passing dummy children\n    render(\n      <Layout>\n        <span>Test</span>\n        <span>Test</span>\n      </Layout>,\n    );\n    const helpLink = screen.queryByTitle('Support');\n    expect(helpLink).toBeNull();\n  });\n\n  it('does render help link when defined', () => {\n    // children is required so avoid warning by passing dummy children\n    render(\n      <Layout>\n        <span>Test</span>\n        <span>Test</span>\n      </Layout>,\n      {\n        uiConfig: {\n          supportUrl: 'https://gitter.im/openzipkin/zipkin',\n        },\n      },\n    );\n    const helpLink = screen.getByTitle('Support');\n    expect(helpLink).toBeDefined();\n    expect(helpLink.href).toEqual('https://gitter.im/openzipkin/zipkin');\n  });\n\n  it('does render Dependencies Page with default config', () => {\n    // children is required so avoid warning by passing dummy children\n    render(\n      <Layout>\n        <span>Test</span>\n        <span>Test</span>\n      </Layout>,\n    );\n    expect(screen.queryByText('Dependencies')).toBeDefined();\n  });\n\n  it('does not render Dependencies Page when disabled', () => {\n    // children is required so avoid warning by passing dummy children\n    render(\n      <Layout>\n        <span>Test</span>\n        <span>Test</span>\n      </Layout>,\n      {\n        uiConfig: {\n          dependency: {\n            enabled: false,\n          },\n        },\n      },\n    );\n    expect(screen.queryByTitle('Dependencies')).toBeNull();\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/App/Layout.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  faProjectDiagram,\n  faSearch,\n  faQuestionCircle,\n} from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport {\n  AppBar as MuiAppBar,\n  Box,\n  CssBaseline,\n  ThemeProvider,\n  Toolbar as MuiToolbar,\n  Typography,\n  IconButton as MuiIconButton,\n  Tooltip,\n} from '@material-ui/core';\nimport React from 'react';\nimport styled from 'styled-components';\n\nimport { useTranslation } from 'react-i18next';\nimport HeaderMenuItem from './HeaderMenuItem';\nimport LanguageSelector from './LanguageSelector';\nimport ThemeSelector from './ThemeSelector';\nimport TraceIdSearch from './TraceIdSearch';\nimport TraceJsonUploader from './TraceJsonUploader';\nimport { useUiConfig } from '../UiConfig';\nimport { darkTheme } from '../../constants/color';\n\nconst Layout: React.FC = ({ children }) => {\n  const { t } = useTranslation();\n  const config = useUiConfig();\n\n  return (\n    <Box display=\"flex\">\n      <CssBaseline />\n      <AppBar>\n        <Toolbar>\n          <Box\n            width=\"100%\"\n            display=\"flex\"\n            justifyContent=\"space-between\"\n            alignItems=\"center\"\n          >\n            <Box display=\"flex\" alignItems=\"center\">\n              <Box\n                width={64}\n                height={64}\n                display=\"flex\"\n                justifyContent=\"center\"\n                alignItems=\"center\"\n              >\n                <Logo alt={t(`Zipkin`).toString()} />\n              </Box>\n              <Title>\n                <strong>Zipkin</strong>\n              </Title>\n              <Box display=\"flex\" ml={3}>\n                <HeaderMenuItem\n                  title={t(`Find a trace`)}\n                  path=\"/\"\n                  icon={faSearch}\n                />\n                {config.dependency.enabled && (\n                  <HeaderMenuItem\n                    title={t(`Dependencies`)}\n                    path=\"/dependency\"\n                    icon={faProjectDiagram}\n                  />\n                )}\n              </Box>\n            </Box>\n            <ThemeProvider theme={darkTheme}>\n              <Box display=\"flex\" alignItems=\"center\">\n                <Box mr={2} ml={2}>\n                  <TraceJsonUploader />\n                </Box>\n                <TraceIdSearch />\n                {config.supportUrl && (\n                  <Box ml={1}>\n                    <Tooltip title={t(`Support`).toString()}>\n                      <MuiIconButton href={config.supportUrl}>\n                        <FontAwesomeIcon icon={faQuestionCircle} />\n                      </MuiIconButton>\n                    </Tooltip>\n                  </Box>\n                )}\n                <Box pl={2} ml={2} mr={2} borderLeft={1} borderColor={'#FFF'}>\n                  <LanguageSelector />\n                </Box>\n                <ThemeSelector />\n              </Box>\n            </ThemeProvider>\n          </Box>\n        </Toolbar>\n      </AppBar>\n      <Box component=\"main\" width=\"100%\">\n        <ToolbarSpace />\n        {children}\n      </Box>\n    </Box>\n  );\n};\n\nexport default Layout;\n\nconst AppBar = styled(MuiAppBar).attrs({\n  position: 'fixed',\n})`\n  background-color: ${({ theme }) => theme.palette.grey[800]};\n  z-index: 1300;\n`;\n\nconst Toolbar = styled(MuiToolbar)`\n  padding-left: 0;\n`;\n\nconst Logo = styled.img.attrs({\n  src: './static/media/zipkin-logo.png',\n})`\n  width: 42px;\n  height: 42px;\n`;\n\nconst Title = styled(Typography).attrs({\n  variant: 'h5',\n})`\n  margin-left: ${({ theme }) => theme.spacing(2)}px;\n  margin-right: ${({ theme }) => theme.spacing(2)}px;\n`;\n\nconst ToolbarSpace = styled.div`\n  min-height: 64px;\n`;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/ThemeSelector.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { IconButton } from '@material-ui/core';\nimport Brightness7Icon from '@material-ui/icons/Brightness7';\nimport Brightness4Icon from '@material-ui/icons/Brightness4';\nimport React, { useCallback, useEffect, useState } from 'react';\n\nimport { getTheme, setTheme } from '../../util/theme';\n\n// ThemeSelector Component\n// This component allows the user to toggle between light and dark themes by clicking an icon.\n// It automatically updates the icon to reflect the current theme (sun for light, moon for dark).\n// The component uses local state to track the current theme and updates it based on user interaction.\nconst ThemeSelector = () => {\n  const [currentTheme, setCurrentTheme] = useState('light'); // Default theme set to 'light'\n\n  useEffect(() => {\n    const theme = getTheme(); // Retrieves the current theme\n    setCurrentTheme(theme); // Updates the state with the current theme\n  }, []);\n\n  const toggleTheme = useCallback(() => {\n    const newTheme = currentTheme === 'light' ? 'dark' : 'light'; // Toggles between themes\n    setTheme(newTheme); // Sets the new theme\n    setCurrentTheme(newTheme); // Updates the state with the new theme\n    window.location.reload(); // Reloads the page to apply the theme change\n  }, [currentTheme]);\n\n  return (\n    <>\n      <IconButton onClick={toggleTheme} color=\"inherit\">\n        {currentTheme === 'light' ? <Brightness7Icon /> : <Brightness4Icon />}\n      </IconButton>\n    </>\n  );\n};\n\nexport default ThemeSelector;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/TraceIdSearch.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { makeStyles, TextField } from '@material-ui/core';\nimport React, { useCallback, useState } from 'react';\nimport { useHistory } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nconst TraceIdSearch: React.FC = () => {\n  const { t } = useTranslation();\n  const history = useHistory();\n\n  const [traceId, setTraceId] = useState('');\n\n  const handleChange = useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      setTraceId(event.target.value);\n    },\n    [],\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLInputElement>) => {\n      if (event.key === 'Enter') {\n        history.push({\n          pathname: `/traces/${traceId}`,\n        });\n      }\n    },\n    [history, traceId],\n  );\n\n  const useStyles = makeStyles({\n    traceIdSearch: {\n      // Questo selettore cambia il colore del bordo al passaggio del mouse\n      '&:hover fieldset': {\n        borderColor: '#FFF !important',\n      },\n      '& .Mui-focused fieldset': {\n        borderColor: '#FFF !important',\n      },\n      '& label.Mui-focused': {\n        color: '#FFF !important',\n      },\n    },\n  });\n  const classes = useStyles();\n\n  return (\n    <TextField\n      className={classes.traceIdSearch}\n      label=\"Search by trace ID\"\n      value={traceId}\n      onChange={handleChange}\n      onKeyDown={handleKeyDown}\n      variant=\"outlined\"\n      size=\"small\"\n      placeholder={t(`Trace ID`).toString()}\n    />\n  );\n};\n\nexport default TraceIdSearch;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/TraceJsonUploader.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { faUpload } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { IconButton, Tooltip } from '@material-ui/core';\nimport { unwrapResult } from '@reduxjs/toolkit';\nimport React, { useCallback, useRef } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useHistory } from 'react-router-dom';\nimport styled from 'styled-components';\n\nimport { Trans, useTranslation } from 'react-i18next';\nimport { setAlert } from './slice';\nimport { loadJsonTrace } from '../../slices/tracesSlice';\n\nconst TraceJsonUploader: React.FC = () => {\n  const dispatch = useDispatch();\n  const history = useHistory();\n  const inputEl = useRef<HTMLInputElement>(null);\n  const { t } = useTranslation();\n\n  const handleClick = useCallback(() => {\n    if (inputEl.current) {\n      inputEl.current.click();\n    }\n  }, []);\n\n  const handleFileChange = useCallback(\n    (event) => {\n      const [file] = event.target.files;\n      dispatch(loadJsonTrace(file))\n        .then(unwrapResult)\n        .then(({ traceId }) => {\n          history.push({\n            pathname: `/traces/${traceId}`,\n          });\n        })\n        .catch((err) => {\n          dispatch(\n            setAlert({\n              message: `Failed to load file: ${err.message}`,\n              severity: 'error',\n            }),\n          );\n        });\n    },\n    [dispatch, history],\n  );\n\n  return (\n    <>\n      <FileInput ref={inputEl} onChange={handleFileChange} />\n      <Tooltip title={<Trans t={t}>Upload JSON</Trans>}>\n        <IconButton onClick={handleClick}>\n          <FontAwesomeIcon icon={faUpload} size=\"sm\" />\n        </IconButton>\n      </Tooltip>\n    </>\n  );\n};\n\nexport default TraceJsonUploader;\n\nconst FileInput = styled.input.attrs({\n  type: 'file',\n})`\n  display: none;\n`;\n"
  },
  {
    "path": "zipkin-lens/src/components/App/index.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { default } from './App';\n"
  },
  {
    "path": "zipkin-lens/src/components/App/slice.test.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport slice, { clearAlert, setAlert } from './slice';\n\ndescribe('<App /> slice', () => {\n  const { reducer } = slice;\n  const alert = {\n    message: 'Hi there!',\n    severity: 'success' as const,\n  };\n\n  it('initially has alert closed', () => {\n    expect(reducer(undefined, { type: 'undefined' }).alertOpen).toEqual(false);\n  });\n\n  it('sets alert when setAlert called', () => {\n    const state = reducer(undefined, setAlert(alert));\n    expect(state.alertOpen).toEqual(true);\n    expect(state.alert).toEqual(alert);\n  });\n\n  it('alert not open when clearAlert called', () => {\n    const alerted = reducer(undefined, setAlert(alert));\n    const state = reducer(alerted, clearAlert());\n    expect(state.alertOpen).toEqual(false);\n    // Alert still present in state to allow it to animate away.\n    expect(state.alert).toEqual(alert);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/App/slice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Color } from '@material-ui/lab/Alert';\nimport { createSlice, PayloadAction } from '@reduxjs/toolkit';\n\ninterface Alert {\n  message: string;\n  severity: Color;\n}\n\ninterface AppState {\n  alert: Alert;\n  // We don't use truthiness of alert to control whether the alert is open or not to allow a clean\n  // animation when closing the alert, which requires the alert to still be rendered even when the\n  // snackbar is set to closed.\n  alertOpen: boolean;\n}\n\nconst initialState: AppState = {\n  alert: {\n    message: '',\n    severity: 'info',\n  },\n  alertOpen: false,\n};\n\nconst appSlice = createSlice({\n  name: 'app',\n  reducers: {\n    clearAlert(state): AppState {\n      return {\n        ...state,\n        alertOpen: false,\n      };\n    },\n    setAlert(state, action: PayloadAction<Alert>): AppState {\n      return {\n        ...state,\n        alert: action.payload,\n        alertOpen: true,\n      };\n    },\n  },\n  initialState,\n});\n\nexport default appSlice;\n\nexport const { clearAlert, setAlert } = appSlice.actions;\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/DependenciesGraph.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { it, expect } from 'vitest';\nimport { getNodesAndEdges } from './DependenciesGraph';\n\n//     10:3           20:7\n// A ----------> B ----------> E\n// |                           ^\n// |                           |\n// +------> C ------> D -------+\n//   12:1       7:0       3:1\nit('getNodesAndEdges', () => {\n  const dependencies = [\n    {\n      parent: 'A',\n      child: 'B',\n      callCount: 10,\n      errorCount: 3,\n    },\n    {\n      parent: 'B',\n      child: 'E',\n      callCount: 20,\n      errorCount: 7,\n    },\n    {\n      parent: 'A',\n      child: 'C',\n      callCount: 12,\n      errorCount: 1,\n    },\n    {\n      parent: 'C',\n      child: 'D',\n      callCount: 7,\n      errorCount: 0,\n    },\n    {\n      parent: 'D',\n      child: 'E',\n      callCount: 3,\n      errorCount: 1,\n    },\n  ];\n\n  const { nodes, edges } = getNodesAndEdges(dependencies);\n\n  const nodeNames = nodes.map((node) => node.name);\n  expect(['A', 'B', 'C', 'D', 'E']).toEqual(expect.arrayContaining(nodeNames));\n\n  expect(edges[0]).toEqual({\n    source: 'A',\n    target: 'B',\n    metrics: {\n      normal: 10,\n      danger: 3,\n    },\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/DependenciesGraph.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-shadow */\n\nimport {\n  Box,\n  Theme,\n  createStyles,\n  makeStyles,\n  useTheme,\n  TextField,\n} from '@material-ui/core';\nimport { Autocomplete } from '@material-ui/lab';\nimport moment from 'moment';\nimport React, { useState, useCallback, useMemo } from 'react';\nimport Dependencies from '../../models/Dependencies';\nimport NodeDetailData from './NodeDetailData';\nimport { Edge } from './types';\nimport VizceralWrapper from './VizceralWrapper';\nimport { getTheme } from '../../util/theme';\n\n// These filter functions use any type because they are passed directly to untyped JS code.\nconst filterConnections = (object: any, value: any) => {\n  if (!value) {\n    return true;\n  }\n  if (object.name === value) {\n    return true;\n  }\n  return object.source.name === value || object.target.name === value;\n};\n\nconst filterNodes = (object: any, value: any) => {\n  if (!value) {\n    return true;\n  }\n  if (object.name === value) {\n    return true;\n  }\n  return (\n    object.incomingConnections.find(\n      (conn: any) => conn.source.name === value,\n    ) ||\n    object.outgoingConnections.find((conn: any) => conn.target.name === value)\n  );\n};\n\n// Export for testing.\nexport const getNodesAndEdges = (dependencies: Dependencies) => {\n  const nodes: { name: string }[] = [];\n  const edges: Edge[] = [];\n\n  dependencies.forEach((edge) => {\n    const nodeNames = nodes.map((node) => node.name);\n\n    if (!nodeNames.includes(edge.parent)) {\n      nodes.push({ name: edge.parent });\n    }\n    if (!nodeNames.includes(edge.child)) {\n      nodes.push({ name: edge.child });\n    }\n\n    edges.push({\n      source: edge.parent,\n      target: edge.child,\n      metrics: {\n        normal: edge.callCount || 0,\n        danger: edge.errorCount || 0,\n      },\n    });\n  });\n  return { nodes, edges };\n};\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    detailWrapper: {\n      flex: '0 0 420px',\n      width: 420,\n      borderLeft: `1px solid ${theme.palette.divider}`,\n    },\n  }),\n);\n\ninterface DependenciesGraphProps {\n  dependencies: Dependencies;\n}\n\nconst DependenciesGraph: React.FC<DependenciesGraphProps> = ({\n  dependencies,\n}) => {\n  const classes = useStyles();\n  const theme = useTheme();\n  const vizStyle = {\n    colorText: theme.palette.primary.contrastText,\n    colorTextDisabled: theme.palette.primary.contrastText,\n    colorConnectionLine:\n      getTheme() === 'dark'\n        ? theme.palette.grey['A700']\n        : theme.palette.grey[800],\n    colorTraffic: {\n      normal: theme.palette.primary.dark,\n      warning: theme.palette.secondary.dark,\n      danger: theme.palette.secondary.dark,\n    },\n    colorDonutInternalColor: theme.palette.primary.light,\n    colorDonutInternalColorHighlighted: theme.palette.primary.light,\n    colorLabelBorder: theme.palette.primary.dark,\n    colorLabelText: theme.palette.primary.contrastText,\n    colorTrafficHighlighted: {\n      normal: theme.palette.primary.main,\n    },\n  };\n\n  const [focusedNodeName, setFocusedNodeName] = useState('');\n\n  const [filter, setFilter] = useState<string | null>();\n  const handleFilterChange = useCallback(\n    (_event: any, value: string | null) => {\n      setFilter(value);\n    },\n    [],\n  );\n\n  const { nodes, edges, createdTs } = useMemo(() => {\n    const { nodes, edges } = getNodesAndEdges(dependencies);\n    return {\n      nodes,\n      edges,\n      createdTs: moment().valueOf(),\n    };\n  }, [dependencies]);\n\n  const targetEdges = useMemo(() => {\n    if (focusedNodeName) {\n      return edges.filter((edge) => edge.source === focusedNodeName);\n    }\n    return [];\n  }, [edges, focusedNodeName]);\n\n  const sourceEdges = useMemo(() => {\n    if (focusedNodeName) {\n      return edges.filter((edge) => edge.target === focusedNodeName);\n    }\n    return [];\n  }, [edges, focusedNodeName]);\n\n  // These filter functions use any type because they are passed directly to untyped JS code.\n  const handleObjectHighlight = useCallback(\n    (highlightedObject?: any) => {\n      if (!highlightedObject) {\n        setFocusedNodeName('');\n        return;\n      }\n      if (\n        highlightedObject.type === 'node' &&\n        highlightedObject.getName() !== focusedNodeName\n      ) {\n        setFocusedNodeName(highlightedObject.getName());\n      }\n    },\n    [focusedNodeName],\n  );\n\n  const maxVolume = useMemo(() => {\n    if (edges.length > 0) {\n      return edges\n        .map((edge) => edge.metrics.normal + edge.metrics.danger)\n        .reduce((a, b) => Math.max(a, b));\n    }\n    return 0;\n  }, [edges]);\n\n  return (\n    <Box\n      width=\"100%\"\n      height=\"100%\"\n      data-testid=\"dependencies-graph\"\n      display=\"flex\"\n    >\n      <Box flex=\"1 1\" position=\"relative\">\n        <VizceralWrapper\n          allowDraggingOfNodes\n          targetFramerate={30}\n          traffic={{\n            renderer: 'region',\n            layout: 'ltrTree',\n            name: 'dependencies-graph',\n            maxVolume: maxVolume * 50,\n            nodes,\n            connections: edges,\n            updated: createdTs,\n          }}\n          objectHighlighted={handleObjectHighlight}\n          styles={vizStyle}\n          key={\n            // Normally, when updating filters, Vizsceral will show and hide nodes without relaying\n            // them out. For a large dependency graph, this often won't let us see well the\n            // information about the filtered nodes, so we prefer to force a relayout any time we\n            // update the filter. Changing the key based on the filter like this causes react to\n            // destroy and reconstruct the component from scratch, which will have a layout zooming\n            // in on the filtered nodes.\n            filter\n          }\n          filters={[\n            {\n              name: 'shownConnections',\n              type: 'connection',\n              passes: filterConnections,\n              value: filter,\n            },\n            {\n              name: 'shownNodes',\n              type: 'node',\n              passes: filterNodes,\n              value: filter,\n            },\n          ]}\n        />\n        <Box position=\"absolute\" left={20} top={20} width={300}>\n          <Autocomplete\n            value={filter}\n            onChange={handleFilterChange}\n            options={nodes.map((node) => node.name)}\n            fullWidth\n            renderInput={(params) => (\n              <TextField\n                {...params}\n                variant=\"outlined\"\n                label=\"Filter\"\n                placeholder=\"Select...\"\n                size=\"small\"\n              />\n            )}\n          />\n        </Box>\n      </Box>\n      {focusedNodeName ? (\n        <Box className={classes.detailWrapper}>\n          <NodeDetailData\n            serviceName={focusedNodeName}\n            targetEdges={targetEdges}\n            sourceEdges={sourceEdges}\n          />\n        </Box>\n      ) : null}\n    </Box>\n  );\n};\n\nexport default DependenciesGraph;\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/DependenciesPage.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { cleanup, fireEvent, screen } from '@testing-library/react';\nimport fetchMock from 'fetch-mock';\n// @ts-ignore\nimport { createMemoryHistory } from 'history';\nimport moment from 'moment';\nimport React from 'react';\n\nimport DependenciesPage from './DependenciesPage';\nimport render from '../../test/util/render-with-default-settings';\n\nvi.mock('@material-ui/pickers', () => {\n  // eslint-disable-next-line\n  const moment = require('moment');\n  return {\n    // eslint-disable-next-line react/prop-types\n    KeyboardDateTimePicker: ({ value, onChange }) => (\n      <input\n        // eslint-disable-next-line react/prop-types\n        value={value.format('MM/DD/YYYY HH:mm:ss')}\n        onChange={(event) => onChange(moment(event.target.value))}\n        data-testid=\"date-time-picker\"\n      />\n    ),\n  };\n});\n\n// vizceral uses setTimeout internally, so if you use jest.runAllTimers\n// in the test, problems will occur.\n// To avoid it, mock Vizceral (VizceralWrapper).\nvi.mock('./VizceralWrapper', () => ({\n  default: () => <div />,\n}));\n\nvi.useFakeTimers();\n\ndescribe('<DependenciesPage />', () => {\n  afterEach(cleanup);\n  it('should manage the temporary time range with DateTimePicker and reflect it in the URL when the search button is clicked', () => {\n    const history = createMemoryHistory();\n    render(<DependenciesPage />, {\n      history,\n    });\n\n    const dateTimePickers = screen.getAllByTestId('date-time-picker');\n    const [startDateTimePicker, endDateTimePicker] = dateTimePickers;\n\n    const startTimeStr = '2013-02-08 09:30:26';\n    const endTimeStr = '2013-02-09 10:40:45';\n    const startTime = moment(startTimeStr);\n    const endTime = moment(endTimeStr);\n\n    fireEvent.change(startDateTimePicker, {\n      target: { value: startTimeStr },\n    });\n    expect(startDateTimePicker.value).toBe('02/08/2013 09:30:26');\n\n    fireEvent.change(endDateTimePicker, {\n      target: { value: endTimeStr },\n    });\n    expect(endDateTimePicker.value).toBe('02/09/2013 10:40:45');\n\n    // When the search button is clicked, reflect the temp time range changes to URL search params.\n    fireEvent.click(screen.getByTestId('search-button'));\n    const params = new URLSearchParams(history.location.search);\n    expect(params.get('startTime')).toBe(startTime.valueOf().toString());\n    expect(params.get('endTime')).toBe(endTime.valueOf().toString());\n  });\n\n  it('should fetch or clear dependencies when URL is changed', async () => {\n    fetchMock.get('*', [\n      {\n        parent: 'serviceA',\n        child: 'serverB',\n        callCount: 10,\n        errorCount: 20,\n      },\n    ]);\n\n    const history = createMemoryHistory();\n    const { rerender } = render(<DependenciesPage />, {\n      history,\n    });\n\n    // When the query parameter is set, dependencies will be fetched\n    // and loading-indicator will be displayed.\n    history.push({\n      location: '/dependencies',\n      search: '?startTime=1586268120000&endTime=1587132132201',\n    });\n    rerender(\n      <DependenciesPage history={history} location={history.location} />,\n    );\n    expect(screen.getAllByTestId('loading-indicator').length).toBe(1);\n\n    // Because setTimeout is used in the action creator that fetches dependencies,\n    // use jest.runAllTimers to complete all timers.\n    vi.runAllTimers();\n    // If wait a while after loading-indicator is displayed,\n    // dependencies-graph will appear.\n    const components = await screen.findAllByTestId('dependencies-graph');\n    expect(components.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/DependenciesPage.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { faProjectDiagram, faSync } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport {\n  Box,\n  Button,\n  Theme,\n  createStyles,\n  makeStyles,\n} from '@material-ui/core';\nimport { KeyboardDateTimePicker } from '@material-ui/pickers';\nimport { MaterialUiPickersDate } from '@material-ui/pickers/typings/date';\nimport moment from 'moment';\nimport React, { useEffect, useState, useCallback, useMemo } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { withRouter, RouteComponentProps } from 'react-router-dom';\nimport { Trans, useTranslation } from 'react-i18next';\nimport {\n  clearDependencies,\n  loadDependencies,\n} from '../../slices/dependenciesSlice';\nimport { RootState } from '../../store';\nimport { clearAlert, setAlert } from '../App/slice';\nimport ExplainBox from '../common/ExplainBox';\nimport { LoadingIndicator } from '../common/LoadingIndicator';\nimport DependenciesGraph from './DependenciesGraph';\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    searchWrapper: {\n      backgroundColor: theme.palette.background.paper,\n      padding: theme.spacing(2),\n      flex: '0 0',\n      borderBottom: `1px solid ${theme.palette.divider}`,\n    },\n    dateTimePicker: {\n      marginLeft: theme.spacing(1),\n      marginRight: theme.spacing(1),\n    },\n  }),\n);\n\ntype DependenciesPageProps = RouteComponentProps;\n\nconst useTimeRange = (history: any, location: any) => {\n  const setTimeRange = useCallback(\n    (timeRange: { startTime: moment.Moment; endTime: moment.Moment }) => {\n      const ps = new URLSearchParams(location.search);\n      ps.set('startTime', timeRange.startTime.valueOf().toString());\n      ps.set('endTime', timeRange.endTime.valueOf().toString());\n      history.push({\n        pathname: location.pathname,\n        search: ps.toString(),\n      });\n    },\n    [history, location.pathname, location.search],\n  );\n\n  const timeRange = useMemo(() => {\n    const ps = new URLSearchParams(location.search);\n    const startTimeStr = ps.get('startTime');\n    let startTime;\n    if (startTimeStr) {\n      startTime = moment(parseInt(startTimeStr, 10));\n    }\n\n    const endTimeStr = ps.get('endTime');\n    let endTime;\n    if (endTimeStr) {\n      endTime = moment(parseInt(endTimeStr, 10));\n    }\n\n    return {\n      startTime,\n      endTime,\n    };\n  }, [location.search]);\n\n  return { timeRange, setTimeRange };\n};\n\nconst useFetchDependencies = (timeRange: {\n  startTime?: moment.Moment;\n  endTime?: moment.Moment;\n}) => {\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (!timeRange.startTime || !timeRange.endTime) {\n      dispatch(clearDependencies());\n      return;\n    }\n    const lookback = timeRange.endTime.diff(timeRange.startTime);\n    dispatch(\n      loadDependencies({ lookback, endTs: timeRange.endTime.valueOf() }),\n    );\n  }, [dispatch, timeRange.endTime, timeRange.startTime]);\n};\n\nconst DependenciesPageImpl: React.FC<DependenciesPageProps> = ({\n  history,\n  location,\n}) => {\n  const classes = useStyles();\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n\n  // tempTimeRange manages a time range which is inputted in the form.\n  // On the other hand, timeRange is a time range obtained from the URL search params.\n  const { timeRange, setTimeRange } = useTimeRange(history, location);\n  const [tempTimeRange, setTempTimeRange] = useState({\n    startTime: timeRange.startTime\n      ? timeRange.startTime\n      : moment().subtract({ days: 1 }),\n    endTime: timeRange.endTime ? timeRange.endTime : moment(),\n  });\n  useFetchDependencies(timeRange);\n\n  const { isLoading, dependencies, error } = useSelector(\n    (state: RootState) => state.dependencies,\n  );\n\n  useEffect(() => {\n    if (error) {\n      dispatch(\n        setAlert({\n          message: 'Failed to load dependencies...',\n          severity: 'error',\n        }),\n      );\n    } else {\n      dispatch(clearAlert());\n    }\n  }, [error, dispatch]);\n\n  const handleStartTimeChange = useCallback(\n    (startTime: MaterialUiPickersDate) => {\n      if (startTime) {\n        setTempTimeRange({ ...tempTimeRange, startTime });\n      }\n    },\n    [tempTimeRange],\n  );\n\n  const handleEndTimeChange = useCallback(\n    (endTime: MaterialUiPickersDate) => {\n      if (endTime) {\n        setTempTimeRange({ ...tempTimeRange, endTime });\n      }\n    },\n    [tempTimeRange],\n  );\n\n  const handleSearchButtonClick = useCallback(() => {\n    setTimeRange(tempTimeRange);\n  }, [setTimeRange, tempTimeRange]);\n\n  useEffect(() => {\n    return () => {\n      dispatch(clearDependencies());\n    };\n  }, [dispatch]);\n\n  let content: JSX.Element;\n  if (isLoading) {\n    content = <LoadingIndicator />;\n  } else if (dependencies.length > 0) {\n    content = <DependenciesGraph dependencies={dependencies} />;\n  } else {\n    content = (\n      <ExplainBox\n        icon={faProjectDiagram}\n        headerText={<Trans t={t}>Search Dependencies</Trans>}\n        text={\n          <Trans t={t}>\n            Please select the start and end time. Then, click the search button.\n          </Trans>\n        }\n      />\n    );\n  }\n\n  return (\n    <Box\n      width=\"100%\"\n      height=\"calc(100vh - 64px)\"\n      display=\"flex\"\n      flexDirection=\"column\"\n    >\n      <Box className={classes.searchWrapper}>\n        <Box display=\"flex\" justifyContent=\"center\" alignItems=\"center\">\n          <Box display=\"flex\" mr={0.5} alignItems=\"center\">\n            <KeyboardDateTimePicker\n              label={t(`Start Time`)}\n              inputVariant=\"outlined\"\n              value={tempTimeRange.startTime}\n              onChange={handleStartTimeChange}\n              format=\"MM/DD/YYYY HH:mm:ss\"\n              className={classes.dateTimePicker}\n              size=\"small\"\n            />\n            -\n            <KeyboardDateTimePicker\n              label={t(`End Time`)}\n              inputVariant=\"outlined\"\n              value={tempTimeRange.endTime}\n              onChange={handleEndTimeChange}\n              format=\"MM/DD/YYYY HH:mm:ss\"\n              className={classes.dateTimePicker}\n              size=\"small\"\n            />\n          </Box>\n          <Button\n            color=\"primary\"\n            variant=\"contained\"\n            onClick={handleSearchButtonClick}\n            data-testid=\"search-button\"\n            startIcon={<FontAwesomeIcon icon={faSync} />}\n          >\n            <Trans t={t}>Run Query</Trans>\n          </Button>\n        </Box>\n      </Box>\n      <Box flex=\"1 1\" bgcolor=\"background.paper\" overflow=\"hidden\">\n        {content}\n      </Box>\n    </Box>\n  );\n};\n\nexport default withRouter(DependenciesPageImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/NodeDetailData.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport { fireEvent, screen } from '@testing-library/react';\nimport { createMemoryHistory } from 'history';\nimport React from 'react';\n\nimport NodeDetailData from './NodeDetailData';\nimport render from '../../test/util/render-with-default-settings';\n\ndescribe('<NodeDetailData />', () => {\n  it('should go to the search traces page when the search traces button is clicked.', () => {\n    const history = createMemoryHistory();\n\n    render(\n      <NodeDetailData\n        serviceName=\"serviceA\"\n        targetEdges={[]}\n        sourceEdges={[]}\n      />,\n      { history },\n    );\n\n    fireEvent.click(screen.getByTestId('search-traces-button'));\n\n    expect(history.location.pathname).toBe('/');\n    expect(history.location.search).toBe('?serviceName=serviceA');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/NodeDetailData.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { faSquare, faSearch } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport {\n  Box,\n  Button,\n  Table,\n  TableBody,\n  TableCell,\n  TableRow,\n  Theme,\n  Typography,\n  createStyles,\n  makeStyles,\n} from '@material-ui/core';\nimport React, { useCallback } from 'react';\nimport { withRouter, RouteComponentProps } from 'react-router-dom';\nimport { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';\nimport { Edge } from './types';\nimport { selectServiceColor } from '../../constants/color';\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    title: {\n      wordWrap: 'break-word',\n      flex: '1 1',\n      minWidth: 0,\n    },\n    table: {\n      tableLayout: 'fixed',\n    },\n    tableRow: {\n      '&:first-child > *': {\n        borderTop: `1px solid ${theme.palette.divider}`,\n      },\n    },\n    tableCell: {\n      wordWrap: 'break-word',\n    },\n    contentWrapper: {\n      flex: '1 1',\n      borderTop: `1px solid ${theme.palette.divider}`,\n      overflow: 'auto',\n      '& > :not(:last-child)': {\n        marginBottom: theme.spacing(2),\n      },\n      paddingBottom: theme.spacing(2),\n    },\n  }),\n);\n\ninterface NodeDetailDataProps extends RouteComponentProps {\n  serviceName: string;\n  targetEdges: Edge[];\n  sourceEdges: Edge[];\n}\n\nconst NodeDetailDataImpl: React.FC<NodeDetailDataProps> = ({\n  serviceName,\n  targetEdges,\n  sourceEdges,\n  history,\n}) => {\n  const classes = useStyles();\n\n  const handleSearchTracesButtonClick = useCallback(() => {\n    const params = new URLSearchParams();\n    params.set('serviceName', serviceName);\n    history.push({\n      pathname: '/',\n      search: params.toString(),\n    });\n  }, [serviceName, history]);\n\n  const shownDataList = [] as {\n    title: string;\n    edges: Edge[];\n    selectNodeName: (edge: Edge) => string;\n    formatter: (totalEdges: number) => string;\n  }[];\n  if (targetEdges.length !== 0) {\n    shownDataList.push({\n      title: 'Uses',\n      edges: targetEdges,\n      selectNodeName: (edge: Edge) => edge.target,\n      formatter: (totalEdges: number) =>\n        `This service uses ${totalEdges} service${totalEdges <= 1 ? '' : 's'}`,\n    });\n  }\n  if (sourceEdges.length !== 0) {\n    shownDataList.push({\n      title: 'Used',\n      edges: sourceEdges,\n      selectNodeName: (edge: Edge) => edge.source,\n      formatter: (totalEdges: number) =>\n        `This service is used by ${totalEdges} service${\n          totalEdges <= 1 ? '' : 's'\n        }`,\n    });\n  }\n\n  return (\n    <Box height=\"100%\" display=\"flex\" flexDirection=\"column\">\n      <Box px={2} py={1} display=\"flex\" flex=\"0 0\" alignItems=\"center\">\n        <Typography variant=\"h6\" className={classes.title}>\n          {serviceName}\n        </Typography>\n        <Box flex=\"0 0\" ml={0.5}>\n          <Button\n            variant=\"outlined\"\n            onClick={handleSearchTracesButtonClick}\n            data-testid=\"search-traces-button\"\n            startIcon={<FontAwesomeIcon icon={faSearch} />}\n          >\n            Traces\n          </Button>\n        </Box>\n      </Box>\n      <Box className={classes.contentWrapper}>\n        {shownDataList.map((d) => (\n          <Box key={d.title}>\n            <Box height={240} position=\"relative\">\n              <Box position=\"absolute\" top={15} left={15}>\n                <Typography variant=\"body1\">\n                  {d.title} (traced requests)\n                </Typography>\n              </Box>\n              <Box position=\"absolute\" bottom={15} right={15}>\n                <Typography variant=\"body2\" color=\"textSecondary\">\n                  {d.formatter(d.edges.length)}\n                </Typography>\n              </Box>\n              <ResponsiveContainer>\n                <PieChart>\n                  <Pie\n                    data={d.edges.map((edge) => ({\n                      name: d.selectNodeName(edge),\n                      value: edge.metrics.normal + edge.metrics.danger,\n                    }))}\n                    nameKey=\"name\"\n                    dataKey=\"value\"\n                    outerRadius={70}\n                  >\n                    {d.edges.map((edge) => (\n                      <Cell\n                        key={d.selectNodeName(edge)}\n                        fill={selectServiceColor(d.selectNodeName(edge))}\n                      />\n                    ))}\n                  </Pie>\n                  <Tooltip />\n                </PieChart>\n              </ResponsiveContainer>\n            </Box>\n            <Box px={2}>\n              <Table size=\"small\" className={classes.table}>\n                <TableBody>\n                  {d.edges.map((edge) => (\n                    <TableRow\n                      key={`${edge.source}---${edge.target}`}\n                      className={classes.tableRow}\n                    >\n                      <TableCell className={classes.tableCell}>\n                        <FontAwesomeIcon\n                          icon={faSquare}\n                          color={selectServiceColor(d.selectNodeName(edge))}\n                        />\n                        <Box component=\"span\" ml={0.5}>\n                          {d.selectNodeName(edge)}\n                        </Box>\n                      </TableCell>\n                      <TableCell className={classes.tableCell} align=\"right\">\n                        {edge.metrics.normal}\n                      </TableCell>\n                      <TableCell className={classes.tableCell} align=\"right\">\n                        {edge.metrics.danger}\n                      </TableCell>\n                    </TableRow>\n                  ))}\n                </TableBody>\n              </Table>\n            </Box>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n};\n\nexport default withRouter(NodeDetailDataImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/VizceralWrapper.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport React from 'react';\nimport Vizceral from 'vizceral-react';\n\n// Vizceral (vizceral-react) does not release some resources when unmounting the component.\n// Therefore, when Vizceral is mounted many times, rendering speed decreases.\n// So this class inherits Vizceral and overrides componentWillUnmount for releasing resources.\nclass VizceralExt extends Vizceral {\n  componentWillUnmount() {\n    delete this.vizceral;\n  }\n}\n\n// This component is defined to avoid the strict type checking.\nconst VizceralWrapper = (props: any) => {\n  return <VizceralExt {...props} />;\n};\n\nexport default VizceralWrapper;\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { default } from './DependenciesPage';\n"
  },
  {
    "path": "zipkin-lens/src/components/DependenciesPage/types.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport type Edge = {\n  source: string;\n  target: string;\n  metrics: {\n    normal: number;\n    danger: number;\n  };\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/Criterion.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport shortid from 'shortid';\n\ntype Criterion = {\n  key: string;\n  value: string;\n  id: string; // for React key props.\n};\n\nexport const newCriterion = (key = '', value = '') => {\n  return {\n    key,\n    value,\n    id: shortid.generate(),\n  };\n};\n\nexport default Criterion;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/DiscoverPage.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, CircularProgress, Typography } from '@material-ui/core';\nimport React, { useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport { ThunkDispatch } from 'redux-thunk';\n\nimport { Trans, useTranslation } from 'react-i18next';\nimport DiscoverPageContent from './DiscoverPageContent';\nimport { useUiConfig } from '../UiConfig';\nimport { loadAutocompleteKeys } from '../../slices/autocompleteKeysSlice';\nimport { loadServices } from '../../slices/servicesSlice';\nimport { RootState } from '../../store';\n\ninterface DiscoverPageImplProps {\n  autocompleteKeys: string[];\n  isLoadingAutocompleteKeys: boolean;\n  isLoadingServices: boolean;\n  loadAutocompleteKeys: () => void;\n  loadServices: () => void;\n}\n\nconst DiscoverPageImpl: React.FC<DiscoverPageImplProps> = ({\n  autocompleteKeys,\n  isLoadingAutocompleteKeys,\n  isLoadingServices,\n  loadAutocompleteKeys,\n  loadServices,\n}) => {\n  const config = useUiConfig();\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    loadAutocompleteKeys();\n    loadServices();\n  }, []);\n\n  if (!config.searchEnabled) {\n    return (\n      <Typography variant=\"body1\">\n        <Trans t={t}>\n          Searching has been disabled via the searchEnabled property. You can\n          still view specific traces of which you know the trace id by entering\n          it in the &quot;Trace ID...&quot; textbox on the top-right.\n        </Trans>\n      </Typography>\n    );\n  }\n\n  if (isLoadingAutocompleteKeys || isLoadingServices) {\n    // Need to fetch autocompleteKeys before displaying a search bar,\n    // because SearchBar uses autocompleteKeys inside.\n    return (\n      <Box\n        height=\"100vh\"\n        width=\"100%\"\n        top={0}\n        position=\"fixed\"\n        display=\"flex\"\n        alignItems=\"center\"\n        justifyContent=\"center\"\n      >\n        <CircularProgress />\n      </Box>\n    );\n  }\n\n  return <DiscoverPageContent autocompleteKeys={autocompleteKeys} />;\n};\n\n// For unit testing, `connect` is easier to use than\n// `useSelector` or `useDispatch` hooks.\nconst mapStateToProps = (state: RootState) => ({\n  autocompleteKeys: state.autocompleteKeys.autocompleteKeys,\n  isLoadingAutocompleteKeys: state.autocompleteKeys.isLoading,\n  isLoadingServices: state.services.isLoading,\n});\n\nconst mapDispatchToProps = (\n  dispatch: ThunkDispatch<RootState, undefined, any>,\n) => ({\n  loadAutocompleteKeys: () => {\n    dispatch(loadAutocompleteKeys());\n  },\n  loadServices: () => {\n    dispatch(loadServices());\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(DiscoverPageImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/DiscoverPageContent.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, afterEach } from 'vitest';\nimport { fireEvent, cleanup, screen } from '@testing-library/react';\nimport { createMemoryHistory } from 'history';\nimport moment from 'moment';\nimport React from 'react';\nimport { Router } from 'react-router-dom';\n\nimport { renderHook } from '@testing-library/react-hooks/lib/pure';\nimport { act } from 'react-dom/test-utils';\nimport DiscoverPageContent, {\n  buildApiQuery,\n  parseDuration,\n  useQueryParams,\n} from './DiscoverPageContent';\nimport render from '../../test/util/render-with-default-settings';\n\ndescribe('useQueryParams', () => {\n  it('should extract criteria from query string', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n\n    history.push({\n      pathname: '/zipkin/',\n      // serviceName: serviceA\n      // spanName: spanB\n      // remoteServiceName: remoteServiceNameC\n      // minDuration: 10us\n      // maxDuration: 100ms\n      // annotationQuery:\n      //   key1: value1\n      //   key2\n      //   key3: value3\n      search:\n        '?serviceName=serviceA&spanName=spanB&remoteServiceName=remoteServiceNameC&minDuration=10us&maxDuration=100ms&annotationQuery=key1%3Dvalue1+and+key2+and+key3%3Dvalue3&limit=10',\n    });\n\n    const { result } = renderHook(() => useQueryParams(['key3']), { wrapper });\n\n    const expected = [\n      { key: 'serviceName', value: 'serviceA' },\n      { key: 'spanName', value: 'spanB' },\n      { key: 'remoteServiceName', value: 'remoteServiceNameC' },\n      { key: 'minDuration', value: '10us' },\n      { key: 'maxDuration', value: '100ms' },\n      // AnnotationQuery\n      { key: 'key3', value: 'value3' },\n      { key: 'tagQuery', value: 'key1=value1 and key2' },\n    ];\n\n    for (let i = 0; i < expected.length; i += 1) {\n      expect(result.current.criteria[i].key).toBe(expected[i].key);\n      expect(result.current.criteria[i].value).toBe(expected[i].value);\n    }\n  });\n\n  it('should extract range lookback from query string', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n    history.push({\n      pathname: '/zipkin/',\n      search: '?lookback=range&startTs=1588558961791&endTs=1588558961791',\n    });\n\n    const { result } = renderHook(() => useQueryParams([]), { wrapper });\n    expect(result.current.lookback.type).toBe('range');\n    expect(result.current.lookback.startTime.valueOf()).toBe(1588558961791);\n    expect(result.current.lookback.endTime.valueOf()).toBe(1588558961791);\n  });\n\n  it('should extract fixed lookback from query string', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n    history.push({\n      pathname: '/zipkin/',\n      search: '?lookback=2h&endTs=1588558961791',\n    });\n\n    const { result } = renderHook(() => useQueryParams([]), { wrapper });\n    expect(result.current.lookback.type).toBe('fixed');\n    expect(result.current.lookback.value).toBe('2h');\n    expect(result.current.lookback.endTime.valueOf()).toBe(1588558961791);\n  });\n\n  it('should extract millis lookback from query string', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n    history.push({\n      pathname: '/zipkin/',\n      search: '?lookback=millis&endTs=1588558961791&millis=1234',\n    });\n\n    const { result } = renderHook(() => useQueryParams([]), { wrapper });\n    expect(result.current.lookback.type).toBe('millis');\n    expect(result.current.lookback.value).toBe(1234);\n    expect(result.current.lookback.endTime.valueOf()).toBe(1588558961791);\n  });\n\n  it('should extract limit from query string', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n    history.push({\n      pathname: '/zipkin/',\n      search: '?limit=300',\n    });\n\n    const { result } = renderHook(() => useQueryParams([]), { wrapper });\n    expect(result.current.limit).toBe(300);\n  });\n\n  it('should set query string using setQueryParams', () => {\n    const history = createMemoryHistory();\n    const wrapper = ({ children }) => {\n      return <Router history={history}>{children}</Router>;\n    };\n    history.push({\n      pathname: '/zipkin/',\n      search: '?limit=300',\n    });\n\n    const { result } = renderHook(() => useQueryParams(['key3']), { wrapper });\n\n    act(() => {\n      result.current.setQueryParams(\n        [\n          { key: 'serviceName', value: 'serviceA' },\n          { key: 'spanName', value: 'spanB' },\n          { key: 'remoteServiceName', value: 'remoteServiceNameC' },\n          // Durations will NOT converted to microsecond values.\n          { key: 'minDuration', value: '10us' },\n          { key: 'maxDuration', value: '100ms' },\n          // AnnotationQuery\n          { key: 'tagQuery', value: 'key1=value1 and key2' },\n          { key: 'key3', value: 'value3' },\n        ],\n        {\n          type: 'fixed',\n          endTime: moment(1588558961791),\n          value: '2h',\n        },\n        10,\n      );\n    });\n    expect(history.location.search).toBe(\n      '?serviceName=serviceA&spanName=spanB&remoteServiceName=remoteServiceNameC&minDuration=10us&maxDuration=100ms&annotationQuery=key1%3Dvalue1+and+key2+and+key3%3Dvalue3&lookback=2h&endTs=1588558961791&limit=10',\n    );\n  });\n});\n\nit('parseDuration', () => {\n  [\n    { in: '35', out: 35 },\n    { in: '35us', out: 35 },\n    { in: '35ms', out: 35 * 1000 },\n    { in: '35s', out: 35 * 1000 * 1000 },\n  ].forEach((e) => {\n    expect(parseDuration(e.in)).toBe(e.out);\n  });\n});\n\ndescribe('buildApiQuery', () => {\n  it('should build API Query', () => {\n    const params = buildApiQuery(\n      [\n        { key: 'serviceName', value: 'serviceA' },\n        { key: 'spanName', value: 'spanB' },\n        { key: 'remoteServiceName', value: 'remoteServiceNameC' },\n        // Durations will converted to microsecond values.\n        { key: 'minDuration', value: '10us' },\n        { key: 'maxDuration', value: '100ms' },\n        // AnnotationQuery\n        { key: 'tagQuery', value: 'key1=value1 and key2' },\n        { key: 'key3', value: 'value3' },\n      ],\n      {\n        type: 'fixed',\n        endTime: moment(1588558961791),\n        value: '2h',\n      },\n      30,\n      ['key3'],\n    );\n    expect(params.serviceName).toBe('serviceA');\n    expect(params.spanName).toBe('spanB');\n    expect(params.remoteServiceName).toBe('remoteServiceNameC');\n    expect(params.minDuration).toBe('10');\n    expect(params.maxDuration).toBe('100000');\n    expect(params.annotationQuery).toBe('key1=value1 and key2 and key3=value3');\n    expect(params.lookback).toBe('7200000');\n    expect(params.endTs).toBe('1588558961791');\n    expect(params.limit).toBe('30');\n  });\n});\n\ndescribe('<DiscoverPageContent />', () => {\n  afterEach(cleanup);\n\n  it('should initialize fixed lookback using config.json', () => {\n    const { rerender } = render(<DiscoverPageContent />, {\n      uiConfig: {\n        defaultLookback: 60 * 1000 * 5, // 5m\n      },\n    });\n    fireEvent.click(screen.getByTestId('settings-button')); // Open settings\n    rerender(<DiscoverPageContent />);\n    expect(screen.getAllByText('Last 5 minutes').length).toBe(1);\n  });\n\n  it('should initialze millis lookback using config.json', () => {\n    const { rerender } = render(<DiscoverPageContent />, {\n      uiConfig: {\n        defaultLookback: 12345,\n      },\n    });\n    fireEvent.click(screen.getByTestId('settings-button')); // Open settings\n    rerender(<DiscoverPageContent />);\n    expect(screen.getAllByText('12345ms').length).toBe(1);\n  });\n\n  it('should initialize the query limit using config.json', () => {\n    const { rerender } = render(<DiscoverPageContent />, {\n      uiConfig: {\n        queryLimit: 30,\n      },\n    });\n    fireEvent.click(screen.getByTestId('settings-button')); // Open settings\n    rerender(<DiscoverPageContent />);\n    const items = screen.getAllByTestId('query-limit');\n    expect(items.length).toBe(1);\n    expect(items[0].value).toBe('30');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/DiscoverPageContent.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { faHistory, faSync, faSearch } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport {\n  Box,\n  Button,\n  ButtonGroup,\n  ButtonProps,\n  Collapse,\n  Divider,\n  makeStyles,\n  TextField,\n  Typography,\n} from '@material-ui/core';\nimport ExpandLessIcon from '@material-ui/icons/ExpandLess';\nimport ExpandMoreIcon from '@material-ui/icons/ExpandMore';\nimport SettingsIcon from '@material-ui/icons/Settings';\nimport { Autocomplete } from '@material-ui/lab';\nimport moment from 'moment';\nimport React, { useCallback, useEffect, useMemo, useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useHistory, useLocation } from 'react-router-dom';\nimport styled from 'styled-components';\nimport { Trans, useTranslation } from 'react-i18next';\nimport Criterion, { newCriterion } from './Criterion';\nimport LookbackMenu from './LookbackMenu';\nimport SearchBar from './SearchBar';\nimport TraceSummaryTable from './TraceSummaryTable';\nimport { Lookback, fixedLookbackMap, millisecondsToValue } from './lookback';\nimport { setAlert } from '../App/slice';\nimport { useUiConfig } from '../UiConfig';\nimport ExplainBox from '../common/ExplainBox';\nimport TraceSummary from '../../models/TraceSummary';\nimport { clearSearch, searchTraces } from '../../slices/tracesSlice';\nimport { RootState } from '../../store';\nimport { LoadingIndicator } from '../common/LoadingIndicator';\n\ninterface DiscoverPageContentProps {\n  autocompleteKeys: string[];\n}\n\n// Export for testing\nexport const useQueryParams = (autocompleteKeys: string[]) => {\n  const history = useHistory();\n  const location = useLocation();\n\n  const setQueryParams = useCallback(\n    (criteria: Criterion[], lookback: Lookback, limit: number) => {\n      const params = new URLSearchParams();\n      const annotationQuery: string[] = [];\n      criteria.forEach((criterion) => {\n        // If the key is 'tag' or a string included in autocompleteKeys,\n        // the criterion will be included in annotationQuery.\n        if (criterion.key === 'tagQuery') {\n          annotationQuery.push(criterion.value);\n        } else if (autocompleteKeys.includes(criterion.key)) {\n          if (criterion.value) {\n            annotationQuery.push(`${criterion.key}=${criterion.value}`);\n          } else {\n            annotationQuery.push(criterion.key);\n          }\n        } else {\n          params.set(criterion.key, criterion.value);\n        }\n      });\n      if (annotationQuery.length > 0) {\n        params.set('annotationQuery', annotationQuery.join(' and '));\n      }\n      switch (lookback.type) {\n        case 'fixed':\n          params.set('lookback', lookback.value);\n          params.set('endTs', lookback.endTime.valueOf().toString());\n          break;\n        case 'range':\n          params.set('lookback', 'range');\n          params.set('endTs', lookback.endTime.valueOf().toString());\n          params.set('startTs', lookback.startTime.valueOf().toString());\n          break;\n        case 'millis':\n          params.set('lookback', 'millis');\n          params.set('endTs', lookback.endTime.valueOf().toString());\n          params.set('millis', lookback.value.toString());\n          break;\n        default:\n      }\n      params.set('limit', limit.toString());\n      history.push({\n        pathname: location.pathname,\n        search: params.toString(),\n      });\n    },\n    [autocompleteKeys, history, location.pathname],\n  );\n\n  const criteria = useMemo(() => {\n    const ret: Criterion[] = [];\n    const params = new URLSearchParams(location.search);\n\n    params.forEach((value, key) => {\n      switch (key) {\n        case 'lookback':\n        case 'startTs':\n        case 'endTs':\n        case 'millis':\n        case 'limit':\n          break;\n        case 'annotationQuery': {\n          // Split annotationQuery into keys of autocompleteKeys and the others.\n          // If the autocompleteKeys is ['projectID', 'phase'] and the annotationQuery is\n          // 'projectID=projectA and phase=BETA and http.path=/api/v1/users and http.method=GET',\n          // criterion will be like the following.\n          // [\n          //   { key: 'tagQuery', value: 'http.path=/api/v1/users and http.method=GET' },\n          //   { key: 'projectID', value: 'projectA' },\n          //   { key: 'phase', value: 'BETA' },\n          // ]\n          const tagQuery: string[] = [];\n          const exps = value.split(' and ');\n          exps.forEach((exp) => {\n            const strs = exp.split('=');\n            if (strs.length === 0) {\n              return;\n            }\n            const [key, value] = strs;\n            if (autocompleteKeys.includes(key)) {\n              ret.push(newCriterion(key, value || ''));\n            } else {\n              tagQuery.push(exp);\n            }\n          });\n          ret.push(newCriterion('tagQuery', tagQuery.join(' and ')));\n          break;\n        }\n        default:\n          ret.push(newCriterion(key, value));\n          break;\n      }\n    });\n    return ret;\n  }, [autocompleteKeys, location.search]);\n\n  const lookback = useMemo<Lookback | undefined>(() => {\n    const ps = new URLSearchParams(location.search);\n    const lookback = ps.get('lookback');\n    if (!lookback) {\n      return undefined;\n    }\n\n    switch (lookback) {\n      case 'range': {\n        const startTs = ps.get('startTs');\n        const endTs = ps.get('endTs');\n        if (!endTs || !startTs) {\n          return undefined;\n        }\n        const startTime = moment(parseInt(startTs, 10));\n        const endTime = moment(parseInt(endTs, 10));\n        return {\n          type: 'range',\n          startTime,\n          endTime,\n        };\n      }\n      case 'millis': {\n        const endTs = ps.get('endTs');\n        const valueStr = ps.get('millis');\n        if (!endTs || !valueStr) {\n          return undefined;\n        }\n        const endTime = moment(parseInt(endTs, 10));\n        const value = parseInt(valueStr, 10);\n        return {\n          type: 'millis',\n          endTime,\n          value,\n        };\n      }\n      default: {\n        // Otherwise lookback should be 1m, 5m 15m, ...\n        const data = fixedLookbackMap[lookback];\n        if (!data) {\n          return undefined;\n        }\n        const endTs = ps.get('endTs');\n        if (!endTs) {\n          return undefined;\n        }\n        return {\n          type: 'fixed',\n          value: data.value,\n          endTime: moment(parseInt(endTs, 10)),\n        };\n      }\n    }\n  }, [location.search]);\n\n  const limit = useMemo(() => {\n    const ps = new URLSearchParams(location.search);\n    const limit = ps.get('limit');\n    if (!limit) {\n      return undefined;\n    }\n    return parseInt(limit, 10);\n  }, [location.search]);\n\n  return {\n    setQueryParams,\n    criteria,\n    lookback,\n    limit,\n  };\n};\n\n// Export for testing\nexport const parseDuration = (duration: string) => {\n  const regex = /^(\\d+)(s|ms|us)?$/;\n  const match = duration.match(regex);\n\n  if (!match || match.length < 2) {\n    return undefined;\n  }\n  if (match.length === 2 || typeof match[2] === 'undefined') {\n    return parseInt(match[1], 10);\n  }\n  switch (match[2]) {\n    case 's':\n      return parseInt(match[1], 10) * 1000 * 1000;\n    case 'ms':\n      return parseInt(match[1], 10) * 1000;\n    case 'us':\n      return parseInt(match[1], 10);\n    default:\n      return undefined;\n  }\n};\n\n// Export for testing\nexport const buildApiQuery = (\n  criteria: Criterion[],\n  lookback: Lookback,\n  limit: number,\n  autocompleteKeys: string[],\n) => {\n  const params: { [key: string]: string } = {};\n  const annotationQuery: string[] = [];\n  criteria.forEach((criterion) => {\n    if (criterion.key === 'tagQuery') {\n      annotationQuery.push(criterion.value);\n    } else if (autocompleteKeys.includes(criterion.key)) {\n      if (criterion.value) {\n        annotationQuery.push(`${criterion.key}=${criterion.value}`);\n      } else {\n        annotationQuery.push(criterion.key);\n      }\n    } else if (\n      criterion.key === 'minDuration' ||\n      criterion.key === 'maxDuration'\n    ) {\n      const duration = parseDuration(criterion.value);\n      if (duration) {\n        params[criterion.key] = duration.toString();\n      }\n    } else {\n      params[criterion.key] = criterion.value;\n    }\n  });\n  if (annotationQuery.length > 0) {\n    params.annotationQuery = annotationQuery.join(' and ');\n  }\n\n  params.endTs = lookback.endTime.valueOf().toString();\n  switch (lookback.type) {\n    case 'range': {\n      const lb = lookback.endTime.valueOf() - lookback.startTime.valueOf();\n      params.lookback = lb.toString();\n      break;\n    }\n    case 'millis': {\n      params.lookback = lookback.value.toString();\n      break;\n    }\n    case 'fixed': {\n      params.lookback = fixedLookbackMap[lookback.value].duration\n        .asMilliseconds()\n        .toString();\n      break;\n    }\n    default:\n  }\n\n  params.limit = limit.toString();\n  return params;\n};\n\nconst useFetchTraces = (\n  autocompleteKeys: string[],\n  criteria: Criterion[],\n  lookback?: Lookback,\n  limit?: number,\n) => {\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    // For searching, lookback and limit are always required.\n    // If it doesn't exist, clear trace summaries.\n    if (!lookback || !limit) {\n      dispatch(clearSearch());\n      return;\n    }\n    const params = buildApiQuery(criteria, lookback, limit, autocompleteKeys);\n    dispatch(searchTraces(params));\n  }, [autocompleteKeys, criteria, dispatch, limit, lookback]);\n};\n\nconst initialLookback = (\n  lookback: Lookback | undefined,\n  defaultLookback: number,\n): Lookback => {\n  if (lookback) {\n    return lookback;\n  }\n  const fixed = millisecondsToValue[defaultLookback];\n  if (fixed) {\n    return {\n      type: 'fixed',\n      value: fixed,\n      endTime: moment(),\n    };\n  }\n  return {\n    type: 'millis',\n    value: defaultLookback,\n    endTime: moment(),\n  };\n};\n\nconst useFilters = (traceSummaries: TraceSummary[]) => {\n  const [filters, setFilters] = useState<string[]>([]);\n\n  const filterOptions = useMemo(\n    () =>\n      Array.from(\n        traceSummaries.reduce((set, traceSummary) => {\n          traceSummary.serviceSummaries.forEach((serviceSummary) => {\n            set.add(serviceSummary.serviceName);\n          });\n          return set;\n        }, new Set<string>()),\n      ),\n    [traceSummaries],\n  );\n\n  const handleFiltersChange = useCallback((event: any, value: string[]) => {\n    setFilters(value);\n  }, []);\n\n  const toggleFilter = useCallback(\n    (serviceName: string) => {\n      if (filters.includes(serviceName)) {\n        setFilters(filters.filter((filter) => filter !== serviceName));\n      } else {\n        const newFilters = [...filters];\n        newFilters.push(serviceName);\n        setFilters(newFilters);\n      }\n    },\n    [filters],\n  );\n\n  return {\n    filters,\n    handleFiltersChange,\n    filterOptions,\n    toggleFilter,\n  };\n};\n\nconst useTraceSummaryOpenState = (traceSummaries: TraceSummary[]) => {\n  const [traceSummaryOpenMap, setTraceSummaryOpenMap] = useState<{\n    [key: string]: boolean;\n  }>({});\n\n  const expandAll = useCallback(() => {\n    setTraceSummaryOpenMap(\n      traceSummaries.reduce((acc, cur) => {\n        acc[cur.traceId] = true;\n        return acc;\n      }, {} as { [key: string]: boolean }),\n    );\n  }, [traceSummaries]);\n\n  const collapseAll = useCallback(() => {\n    setTraceSummaryOpenMap({});\n  }, []);\n\n  const toggleTraceSummaryOpen = useCallback((traceId: string) => {\n    setTraceSummaryOpenMap((prev) => {\n      const newTraceSummaryOpenMap = { ...prev };\n      newTraceSummaryOpenMap[traceId] = !newTraceSummaryOpenMap[traceId];\n      return newTraceSummaryOpenMap;\n    });\n  }, []);\n\n  return {\n    traceSummaryOpenMap,\n    expandAll,\n    collapseAll,\n    toggleTraceSummaryOpen,\n  };\n};\n\nconst useStyles = makeStyles((theme) => ({\n  searchRow: {\n    borderBottom: `1px solid ${theme.palette.divider}`,\n    display: 'flex',\n    padding: theme.spacing(1.5, 3),\n  },\n  settings: {\n    borderBottom: `1px solid ${theme.palette.divider}`,\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'flex-end',\n    padding: theme.spacing(1, 3),\n  },\n  resultHeader: {\n    display: 'flex',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n    padding: theme.spacing(1.5, 3),\n  },\n}));\n\nconst DiscoverPageContent: React.FC<DiscoverPageContentProps> = ({\n  autocompleteKeys,\n}) => {\n  const classes = useStyles();\n  // Retrieve search criteria from the URL query string and use them to search for traces.\n  const { setQueryParams, criteria, lookback, limit } =\n    useQueryParams(autocompleteKeys);\n  useFetchTraces(autocompleteKeys, criteria, lookback, limit);\n  const { t } = useTranslation();\n  // Temporary search criteria.\n  const [tempCriteria, setTempCriteria] = useState(criteria);\n  const { defaultLookback, queryLimit } = useUiConfig();\n  const [tempLookback, setTempLookback] = useState<Lookback>(\n    initialLookback(lookback, defaultLookback),\n  );\n  const [tempLimit, setTempLimit] = useState(limit || queryLimit);\n\n  const lookbackDisplay = useMemo<string>(() => {\n    switch (tempLookback.type) {\n      case 'fixed':\n        return fixedLookbackMap[tempLookback.value].display;\n      case 'range':\n        return `${tempLookback.startTime.format(\n          'MM/DD/YYYY HH:mm:ss',\n        )} - ${tempLookback.endTime.format('MM/DD/YYYY HH:mm:ss')}`;\n      case 'millis':\n        return `${tempLookback.value}ms`;\n      default:\n        return '';\n    }\n  }, [tempLookback]);\n\n  const handleLimitChange = useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      setTempLimit(parseInt(event.target.value, 10));\n    },\n    [],\n  );\n\n  const searchTraces = useCallback(() => {\n    const criteria = tempCriteria.filter((c) => !!c.key);\n\n    // If the lookback is fixed or millis, need to set the click time to endTime.\n    if (tempLookback.type === 'fixed' || tempLookback.type === 'millis') {\n      setQueryParams(\n        criteria,\n        { ...tempLookback, endTime: moment() },\n        tempLimit,\n      );\n    } else {\n      setQueryParams(criteria, tempLookback, tempLimit);\n    }\n  }, [setQueryParams, tempCriteria, tempLookback, tempLimit]);\n\n  const { isLoading, traceSummaries, error } = useSelector(\n    (state: RootState) => state.traces.search,\n  );\n\n  const [isShowingLookbackMenu, setIsShowingLookbackMenu] = useState(false);\n  const toggleLookbackMenu = useCallback(() => {\n    setIsShowingLookbackMenu((prev) => !prev);\n  }, []);\n  const closeLookbackMenu = useCallback(() => {\n    setIsShowingLookbackMenu(false);\n  }, []);\n\n  const [isOpeningSettings, setIsOpeningSettings] = useState(false);\n  const handleSettingsButtonClick = useCallback(() => {\n    setIsOpeningSettings((prev) => !prev);\n  }, []);\n\n  const { filters, handleFiltersChange, filterOptions, toggleFilter } =\n    useFilters(traceSummaries);\n\n  const {\n    traceSummaryOpenMap,\n    expandAll,\n    collapseAll,\n    toggleTraceSummaryOpen,\n  } = useTraceSummaryOpenState(traceSummaries);\n\n  const filteredTraceSummaries = useMemo(() => {\n    return traceSummaries.filter((traceSummary) => {\n      const serviceNameSet = new Set(\n        traceSummary.serviceSummaries.map(\n          (serviceSummary) => serviceSummary.serviceName,\n        ),\n      );\n      return !filters.find((filter) => !serviceNameSet.has(filter));\n    });\n  }, [filters, traceSummaries]);\n\n  const dispatch = useDispatch();\n\n  // If there is a problem during the search, show it.\n  useEffect(() => {\n    if (error) {\n      let message = 'Failed to search';\n      if (error.message) {\n        message += `: ${error.message}`;\n      }\n      dispatch(\n        setAlert({\n          message,\n          severity: 'error',\n        }),\n      );\n    }\n  }, [dispatch, error]);\n\n  let content: JSX.Element | undefined;\n\n  if (isLoading) {\n    content = <LoadingIndicator />;\n  } else if (traceSummaries.length === 0) {\n    content = (\n      <ExplainBox\n        icon={faSearch}\n        headerText={<Trans t={t}>Search Traces</Trans>}\n        text={\n          <Trans t={t}>\n            Please select criteria in the search bar. Then, click the search\n            button.\n          </Trans>\n        }\n      />\n    );\n  } else {\n    content = (\n      <TraceSummaryTable\n        traceSummaries={filteredTraceSummaries}\n        toggleFilter={toggleFilter}\n        traceSummaryOpenMap={traceSummaryOpenMap}\n        toggleTraceSummaryOpen={toggleTraceSummaryOpen}\n      />\n    );\n  }\n\n  return (\n    <Box\n      width=\"100%\"\n      height=\"calc(100vh - 64px)\"\n      display=\"flex\"\n      flexDirection=\"column\"\n      bgcolor=\"background.paper\"\n    >\n      <Box flex=\"0 0\">\n        <Box className={classes.searchRow}>\n          <Box flexGrow={1} mr={1}>\n            <SearchBar\n              criteria={tempCriteria}\n              onChange={setTempCriteria}\n              searchTraces={searchTraces}\n            />\n          </Box>\n          <SearchButton onClick={searchTraces}>\n            <Trans t={t}>Run Query</Trans>\n          </SearchButton>\n          <SettingsButton\n            onClick={handleSettingsButtonClick}\n            isOpening={isOpeningSettings}\n            data-testid=\"settings-button\"\n          >\n            <SettingsIcon />\n          </SettingsButton>\n        </Box>\n        <Collapse in={isOpeningSettings}>\n          <Box className={classes.settings}>\n            <Divider />\n            <TextField\n              label={<Trans>Limit</Trans>}\n              type=\"number\"\n              variant=\"outlined\"\n              value={tempLimit}\n              onChange={handleLimitChange}\n              size=\"small\"\n              inputProps={{\n                'data-testid': 'query-limit',\n              }}\n            />\n            <Box ml={1} position=\"relative\">\n              <LookbackButton\n                onClick={toggleLookbackMenu}\n                isShowingLookbackMenu={isShowingLookbackMenu}\n              >\n                {lookbackDisplay}\n              </LookbackButton>\n              {isShowingLookbackMenu && (\n                <LookbackMenu\n                  close={closeLookbackMenu}\n                  onChange={setTempLookback}\n                  lookback={tempLookback}\n                />\n              )}\n            </Box>\n          </Box>\n        </Collapse>\n        {traceSummaries.length > 0 && (\n          <Box className={classes.resultHeader}>\n            <Typography variant=\"body1\">\n              {filteredTraceSummaries.length === 1 ? (\n                <Trans t={t}>1 Result</Trans>\n              ) : (\n                <Trans>{filteredTraceSummaries.length} Results</Trans>\n              )}\n            </Typography>\n            <Box display=\"flex\">\n              <ButtonGroup>\n                <Button onClick={expandAll}>Expand All</Button>\n                <Button onClick={collapseAll}>Collapse All</Button>\n              </ButtonGroup>\n              <Box width={300} ml={2}>\n                <Autocomplete\n                  multiple\n                  value={filters}\n                  options={filterOptions}\n                  renderInput={(params) => (\n                    <TextField\n                      {...params}\n                      variant=\"outlined\"\n                      label=\"Service filters\"\n                      placeholder=\"Service filters\"\n                      size=\"small\"\n                    />\n                  )}\n                  size=\"small\"\n                  onChange={handleFiltersChange}\n                />\n              </Box>\n            </Box>\n          </Box>\n        )}\n      </Box>\n      <Box flexGrow={1} overflow=\"auto\">\n        {content}\n      </Box>\n    </Box>\n  );\n};\n\nexport default DiscoverPageContent;\n\nconst LookbackButton = styled(\n  ({\n    isShowingLookbackMenu,\n    ...rest\n  }: { isShowingLookbackMenu: boolean } & ButtonProps) => (\n    <Button\n      variant=\"outlined\"\n      startIcon={<FontAwesomeIcon icon={faHistory} />}\n      endIcon={isShowingLookbackMenu ? <ExpandLessIcon /> : <ExpandMoreIcon />}\n      {...rest}\n    />\n  ),\n)`\n  /* Align LookbackButton height with the TextField height. */\n  padding-top: 7.5px;\n  padding-bottom: 7.5px;\n`;\n\nconst SearchButton = styled(Button).attrs({\n  variant: 'contained',\n  color: 'primary',\n  startIcon: <FontAwesomeIcon icon={faSync} />,\n})`\n  flex-shrink: 0;\n  color: ${({ theme }) => theme.palette.common.white};\n`;\n\nconst SettingsButton = styled(\n  ({ isOpening, ...rest }: { isOpening: boolean } & ButtonProps) => (\n    <Button\n      variant=\"outlined\"\n      endIcon={isOpening ? <ExpandLessIcon /> : <ExpandMoreIcon />}\n      {...rest}\n    />\n  ),\n)`\n  flex-shrink: 0;\n  margin-left: ${({ theme }) => theme.spacing(1)}px;\n`;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/LookbackMenu.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { cleanup, fireEvent, screen } from '@testing-library/react';\nimport moment from 'moment';\nimport React from 'react';\n\nimport LookbackMenu from './LookbackMenu';\nimport render from '../../test/util/render-with-default-settings';\n\nvi.mock('@material-ui/pickers', () => {\n  // eslint-disable-next-line\n  const moment = require('moment');\n  return {\n    // eslint-disable-next-line react/prop-types\n    KeyboardDateTimePicker: ({ value, onChange }) => (\n      <input\n        // eslint-disable-next-line react/prop-types\n        value={value.format('MM/DD/YYYY HH:mm:ss')}\n        onChange={(event) => onChange(moment(event.target.value))}\n        data-testid=\"date-time-picker\"\n      />\n    ),\n  };\n});\n\ndescribe('<LookbackMenu />', () => {\n  afterEach(cleanup);\n\n  it('should change lookback and close when a list item is clicked', () => {\n    const lookback = {\n      type: 'fixed',\n      value: '15m',\n      endTime: moment(),\n    };\n\n    const close = vi.fn();\n    const onChange = vi.fn();\n\n    render(\n      <LookbackMenu close={close} onChange={onChange} lookback={lookback} />,\n    );\n\n    fireEvent.click(screen.getByTestId('lookback-2h'));\n\n    expect(onChange.mock.calls.length).toBe(1);\n    expect(onChange.mock.calls[0][0].type).toBe('fixed');\n    expect(onChange.mock.calls[0][0].value).toBe('2h');\n    expect(close.mock.calls.length).toBe(1);\n  });\n\n  it('should change lookback and close when Apply button is clicked', () => {\n    const lookback = {\n      type: 'fixed',\n      value: '15m',\n      endTime: moment(),\n    };\n\n    const close = vi.fn();\n    const onChange = vi.fn();\n\n    render(\n      <LookbackMenu close={close} onChange={onChange} lookback={lookback} />,\n    );\n\n    const dateTimePickers = screen.getAllByTestId('date-time-picker');\n    const [startDateTimePicker, endDateTimePicker] = dateTimePickers;\n\n    const startTimeStr = '2013-02-08 09:30:26';\n    const endTimeStr = '2013-02-09 10:40:45';\n    const startTime = moment(startTimeStr);\n    const endTime = moment(endTimeStr);\n\n    // Change date time pickers' state.\n    fireEvent.change(startDateTimePicker, {\n      target: { value: startTimeStr },\n    });\n    fireEvent.change(endDateTimePicker, {\n      target: { value: endTimeStr },\n    });\n\n    fireEvent.click(screen.getByTestId('apply-button'));\n    expect(onChange.mock.calls.length).toBe(1);\n    expect(onChange.mock.calls[0][0].type).toBe('range');\n    expect(onChange.mock.calls[0][0].endTime.valueOf()).toBe(endTime.valueOf());\n    expect(onChange.mock.calls[0][0].startTime.valueOf()).toBe(\n      startTime.valueOf(),\n    );\n    expect(close.mock.calls.length).toBe(1);\n  });\n\n  it('should change lookback and close when Millis Apply button is clicked', () => {\n    const lookback = {\n      type: 'fixed',\n      value: '15m',\n      endTime: moment(),\n    };\n\n    const close = vi.fn();\n    const onChange = vi.fn();\n\n    render(\n      <LookbackMenu close={close} onChange={onChange} lookback={lookback} />,\n    );\n    const millisInput = screen.getByTestId('millis-input');\n\n    fireEvent.change(millisInput, {\n      target: { value: '12345' },\n    });\n\n    fireEvent.click(screen.getByTestId('millis-apply-button'));\n    expect(onChange.mock.calls.length).toBe(1);\n    expect(onChange.mock.calls[0][0].type).toBe('millis');\n    expect(onChange.mock.calls[0][0].value).toBe(12345);\n    expect(close.mock.calls.length).toBe(1);\n  });\n\n  it('should close when click outside', () => {\n    const lookback = {\n      type: 'fixed',\n      value: '15m',\n      endTime: moment(),\n    };\n\n    const close = vi.fn();\n    const onChange = vi.fn();\n\n    render(\n      <LookbackMenu close={close} onChange={onChange} lookback={lookback} />,\n    );\n\n    // Click outside of the component.\n    fireEvent.click(document);\n    expect(close.mock.calls.length).toBe(0);\n    expect(onChange.mock.calls.length).toBe(0); // onChange must not be called.\n  });\n\n  // TODO: Test that `close` is not invoked when\n  // DateTimePicker dialog is clicked.\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/LookbackMenu.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Box,\n  Button,\n  ClickAwayListener,\n  Grid,\n  List,\n  ListItem,\n  ListItemText,\n  Paper,\n  TextField,\n  Theme,\n  createStyles,\n  makeStyles,\n} from '@material-ui/core';\nimport { KeyboardDateTimePicker } from '@material-ui/pickers';\nimport { MaterialUiPickersDate } from '@material-ui/pickers/typings/date';\nimport moment, { Moment } from 'moment';\nimport React, { useCallback, useState } from 'react';\n\nimport { fixedLookbackMap, FixedLookbackValue, Lookback } from './lookback';\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    root: {\n      position: 'absolute',\n      top: 45,\n      right: 0,\n      height: 420,\n      width: 600,\n      zIndex: theme.zIndex.modal,\n    },\n    containerGrid: {\n      height: '100%',\n    },\n    fixedLookbackItemGrid: {\n      height: '100%',\n      overflowY: 'auto',\n      borderRight: `1px solid ${theme.palette.divider}`,\n    },\n    list: {\n      padding: 0,\n    },\n  }),\n);\n\ninterface LookbackMenuProps {\n  close: () => void;\n  onChange: (lookback: Lookback) => void;\n  lookback: Lookback;\n}\n\nconst initialMillis = (lookback: Lookback): number => {\n  if (lookback.type === 'millis') {\n    return lookback.value;\n  }\n  return 0;\n};\n\nconst initialStartTime = (lookback: Lookback): Moment => {\n  if (lookback.type === 'range') {\n    return lookback.startTime;\n  }\n  return moment().subtract(1, 'h');\n};\n\nconst initialEndTime = (lookback: Lookback): Moment => {\n  if (lookback.type === 'range') {\n    return lookback.endTime;\n  }\n  return moment();\n};\n\nconst LookbackMenu: React.FC<LookbackMenuProps> = ({\n  close,\n  onChange,\n  lookback,\n}) => {\n  const classes = useStyles();\n\n  // LookbackMenu is closed when click on the outside of the component.\n  // This state is needed to prevent LookbackMenu component from closing\n  // when the dialog of DateTimePicker is clicked.\n  const [isOpeningDialog, setIsOpeningDialog] = useState(false);\n\n  const handleDialogOpen = useCallback(() => {\n    setIsOpeningDialog(true);\n  }, []);\n\n  const handleDialogClose = useCallback(() => {\n    setIsOpeningDialog(false);\n  }, []);\n\n  const handleOutsideClick = useCallback(() => {\n    if (!isOpeningDialog) {\n      close();\n    }\n  }, [close, isOpeningDialog]);\n\n  // For Millis Lookback\n  const [millis, setMillis] = useState(initialMillis(lookback));\n  // Range Lookback\n  const [startTime, setStartTime] = useState(initialStartTime(lookback));\n  const [endTime, setEndTime] = useState(initialEndTime(lookback));\n\n  const handleMillisChange = useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      setMillis(parseInt(event.target.value, 10));\n    },\n    [],\n  );\n\n  const handleStartTimeChange = useCallback((date: MaterialUiPickersDate) => {\n    if (date) {\n      setStartTime(date);\n    }\n  }, []);\n\n  const handleEndTimeChange = useCallback((date: MaterialUiPickersDate) => {\n    if (date) {\n      setEndTime(date);\n    }\n  }, []);\n\n  const handleListItemClick = (value: FixedLookbackValue) => () => {\n    onChange({\n      type: 'fixed',\n      value,\n      endTime: moment(),\n    });\n    close();\n  };\n\n  const handleMillisApplyButtonClick = useCallback(() => {\n    onChange({\n      type: 'millis',\n      endTime: moment(),\n      value: millis,\n    });\n    close();\n  }, [close, millis, onChange]);\n\n  const handleRangeApplyButtonClick = useCallback(() => {\n    onChange({\n      type: 'range',\n      startTime,\n      endTime,\n    });\n    close();\n  }, [startTime, endTime, onChange, close]);\n\n  return (\n    <ClickAwayListener onClickAway={handleOutsideClick}>\n      <Paper elevation={5} className={classes.root}>\n        <Grid container className={classes.containerGrid}>\n          <Grid item xs={5} className={classes.fixedLookbackItemGrid}>\n            <List className={classes.list}>\n              {(\n                [\n                  '1m',\n                  '5m',\n                  '15m',\n                  '30m',\n                  '1h',\n                  '2h',\n                  '3h',\n                  '6h',\n                  '12h',\n                  '1d',\n                  '2d',\n                  '7d',\n                ] as FixedLookbackValue[]\n              ).map((value) => (\n                <ListItem\n                  button\n                  onClick={handleListItemClick(value)}\n                  key={value}\n                  data-testid={`lookback-${value}`}\n                >\n                  <ListItemText primary={fixedLookbackMap[value].display} />\n                </ListItem>\n              ))}\n            </List>\n          </Grid>\n          <Grid item xs={7}>\n            <Box p={2}>\n              <Box fontSize=\"1.1rem\" color=\"text.secondary\" mb={2}>\n                Millis Lookback\n              </Box>\n              <TextField\n                label=\"Milliseconds\"\n                onChange={handleMillisChange}\n                value={millis.toString()}\n                variant=\"outlined\"\n                size=\"small\"\n                type=\"number\"\n                inputProps={{\n                  min: '0',\n                  'data-testid': 'millis-input',\n                }}\n              />\n              <Box display=\"flex\" justifyContent=\"flex-end\" mt={1}>\n                <Button\n                  variant=\"contained\"\n                  color=\"secondary\"\n                  onClick={handleMillisApplyButtonClick}\n                  data-testid=\"millis-apply-button\"\n                >\n                  Apply\n                </Button>\n              </Box>\n            </Box>\n            <Box p={2} borderTop={1} borderColor=\"divider\">\n              <Box fontSize=\"1.1rem\" color=\"text.secondary\" mb={2}>\n                Range Lookback\n              </Box>\n              <Box mb={2}>\n                <KeyboardDateTimePicker\n                  label=\"Start Time\"\n                  inputVariant=\"outlined\"\n                  format=\"MM/DD/YYYY HH:mm:ss\"\n                  value={startTime}\n                  onChange={handleStartTimeChange}\n                  onOpen={handleDialogOpen}\n                  onClose={handleDialogClose}\n                  data-testid=\"date-time-picker\"\n                  size=\"small\"\n                  fullWidth\n                />\n              </Box>\n              <Box mb={2}>\n                <KeyboardDateTimePicker\n                  label=\"End Time\"\n                  inputVariant=\"outlined\"\n                  format=\"MM/DD/YYYY HH:mm:ss\"\n                  value={endTime}\n                  onChange={handleEndTimeChange}\n                  onOpen={handleDialogOpen}\n                  onClose={handleDialogClose}\n                  data-testid=\"date-time-picker\"\n                  size=\"small\"\n                  fullWidth\n                />\n              </Box>\n              <Box display=\"flex\" justifyContent=\"flex-end\">\n                <Button\n                  variant=\"contained\"\n                  color=\"secondary\"\n                  onClick={handleRangeApplyButtonClick}\n                  data-testid=\"apply-button\"\n                >\n                  Apply\n                </Button>\n              </Box>\n            </Box>\n          </Grid>\n        </Grid>\n      </Paper>\n    </ClickAwayListener>\n  );\n};\n\nexport default LookbackMenu;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/CriterionBox.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { cleanup, fireEvent, screen } from '@testing-library/react';\nimport React from 'react';\n\nimport CriterionBox from './CriterionBox';\nimport render from '../../../test/util/render-with-default-settings';\n\ndescribe('<CriterionBox />', () => {\n  afterEach(cleanup);\n  const commonProps = {\n    criteria: [],\n    criterion: { key: '', value: '' },\n    serviceNames: ['serviceA', 'serviceB', 'serviceC'],\n    spanNames: ['spanA', 'spanB', 'spanC'],\n    autocompleteKeys: ['keyA', 'keyB', 'keyC'],\n    autocompleteValues: ['valueA', 'valueB', 'valueC'],\n    isLoadingServiceNames: false,\n    isLoadingRemoteServiceNames: false,\n    isLoadingSpanNames: false,\n    isLoadingAutocompleteValues: false,\n    isFocused: true,\n    onFocus: vi.fn(),\n    onBlur: vi.fn(),\n    onDecide: vi.fn(),\n    onChange: vi.fn(),\n    onDelete: vi.fn(),\n    loadAutocompleteValues: vi.fn(),\n  };\n\n  it('should show an input element when focused', () => {\n    render(<CriterionBox {...commonProps} />);\n    const items = screen.getAllByTestId('criterion-input');\n    expect(items.length).toBe(1);\n  });\n\n  it('should filter key suggestions', () => {\n    render(<CriterionBox {...commonProps} />);\n\n    expect(screen.queryAllByText('serviceName').length).toBe(1);\n    // spanName and remoteServiceName are not displayed until serviceName is selected.\n    expect(screen.queryAllByText('spanName').length).toBe(0);\n    expect(screen.queryAllByText('remoteServiceName').length).toBe(0);\n    expect(screen.queryAllByText('maxDuration').length).toBe(1);\n    expect(screen.queryAllByText('minDuration').length).toBe(1);\n    expect(screen.queryAllByText('tagQuery').length).toBe(1);\n    expect(screen.queryAllByText('keyA').length).toBe(1);\n    expect(screen.queryAllByText('keyB').length).toBe(1);\n    expect(screen.queryAllByText('keyC').length).toBe(1);\n\n    const items = screen.getAllByTestId('criterion-input');\n    fireEvent.change(items[0], { target: { value: 's' } });\n\n    expect(screen.queryAllByText('serviceName').length).toBe(1);\n    // spanName and remoteServiceName are not displayed until serviceName is selected.\n    expect(screen.queryAllByText('spanName').length).toBe(0);\n    expect(screen.queryAllByText('remoteServiceName').length).toBe(0);\n    expect(screen.queryAllByText('maxDuration').length).toBe(0);\n    expect(screen.queryAllByText('minDuration').length).toBe(0);\n    expect(screen.queryAllByText('tagQuery').length).toBe(0);\n    expect(screen.queryAllByText('keyA').length).toBe(0);\n    expect(screen.queryAllByText('keyB').length).toBe(0);\n    expect(screen.queryAllByText('keyC').length).toBe(0);\n  });\n\n  it('should show spanName and remoteServiceName after serviceName is selected', () => {\n    render(\n      <CriterionBox\n        {...commonProps}\n        criteria={[\n          /* serviceName was selected */\n          { key: 'serviceName', value: 'serviceA' },\n        ]}\n      />,\n    );\n    expect(screen.queryAllByText('serviceName').length).toBe(0);\n    // Show spanName and remoteServiceName.\n    expect(screen.queryAllByText('spanName').length).toBe(1);\n    expect(screen.queryAllByText('remoteServiceName').length).toBe(1);\n    expect(screen.queryAllByText('maxDuration').length).toBe(1);\n    expect(screen.queryAllByText('minDuration').length).toBe(1);\n    expect(screen.queryAllByText('tagQuery').length).toBe(1);\n    expect(screen.queryAllByText('keyA').length).toBe(1);\n    expect(screen.queryAllByText('keyB').length).toBe(1);\n    expect(screen.queryAllByText('keyC').length).toBe(1);\n  });\n\n  it('should filter value suggestions', () => {\n    render(\n      <CriterionBox\n        {...commonProps}\n        serviceNames={[\n          'service10',\n          'service11',\n          'service12',\n          'service20',\n          'service21',\n          'service22',\n        ]}\n      />,\n    );\n    const items = screen.getAllByTestId('criterion-input');\n    fireEvent.change(items[0], { target: { value: 'serviceName=service' } });\n    expect(screen.queryAllByText('service10').length).toBe(1);\n    expect(screen.queryAllByText('service11').length).toBe(1);\n    expect(screen.queryAllByText('service12').length).toBe(1);\n    expect(screen.queryAllByText('service20').length).toBe(1);\n    expect(screen.queryAllByText('service21').length).toBe(1);\n    expect(screen.queryAllByText('service22').length).toBe(1);\n\n    fireEvent.change(items[0], { target: { value: 'serviceName=service1' } });\n\n    expect(screen.queryAllByText('service10').length).toBe(1);\n    expect(screen.queryAllByText('service11').length).toBe(1);\n    expect(screen.queryAllByText('service12').length).toBe(1);\n    expect(screen.queryAllByText('service20').length).toBe(0);\n    expect(screen.queryAllByText('service21').length).toBe(0);\n    expect(screen.queryAllByText('service22').length).toBe(0);\n\n    fireEvent.change(items[0], { target: { value: 'serviceName=service11' } });\n\n    expect(screen.queryAllByText('service10').length).toBe(0);\n    expect(screen.queryAllByText('service11').length).toBe(1);\n    expect(screen.queryAllByText('service12').length).toBe(0);\n    expect(screen.queryAllByText('service20').length).toBe(0);\n    expect(screen.queryAllByText('service21').length).toBe(0);\n    expect(screen.queryAllByText('service22').length).toBe(0);\n  });\n\n  it(\"should insert '=' when Enter key is down while entering key\", () => {\n    render(<CriterionBox {...commonProps} />);\n    const items = screen.getAllByTestId('criterion-input');\n    fireEvent.change(items[0], { target: { value: 'serviceName' } });\n    fireEvent.keyDown(items[0], { key: 'Enter' });\n    expect(items[0].value).toBe('serviceName=');\n  });\n\n  it('should decide when Enter key is down while entering value', () => {\n    const onDecide = vi.fn();\n    const { getAllByTestId } = render(\n      <CriterionBox {...commonProps} onDecide={onDecide} />,\n    );\n    const items = getAllByTestId('criterion-input');\n    fireEvent.change(items[0], { target: { value: 'serviceName=serviceA' } });\n    fireEvent.keyDown(items[0], { key: 'Enter' });\n    expect(onDecide.call.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/CriterionBox.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-shadow */\n\nimport { faTimes } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { Box, ClickAwayListener } from '@material-ui/core';\nimport React, {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { useMount } from 'react-use';\nimport styled, { keyframes } from 'styled-components';\nimport Criterion, { newCriterion } from '../Criterion';\nimport { CRITERION_BOX_HEIGHT } from './constants';\nimport HowToUse from './HowToUse';\nimport SuggestionList from './SuggestionList';\n\nconst fadeIn = keyframes`\n  0% { opacity: 0 }\n  30% { opacity: 0.1 }\n  70% { opacity: 0.9 }\n  100% { opacity: 1 }\n`;\n\nconst Root = styled(Box)`\n  display: flex;\n  height: ${CRITERION_BOX_HEIGHT}px;\n  border-radius: 3px;\n  box-shadow: ${({ theme }) => theme.shadows[1]};\n  overflow: hidden;\n  font-size: 1rem;\n  color: ${({ theme }) => theme.palette.common.white};\n  cursor: pointer;\n  & > *:hover {\n    opacity: 0.9;\n  }\n  animation: 0.25s 0s both ${fadeIn};\n`;\n\nconst FocusedRoot = styled(Box)`\n  margin-right: ${({ theme }) => theme.spacing(2)}px;\n  position: relative;\n  animation: 0.25s 0s both ${fadeIn};\n  z-index: ${({ theme }) => theme.zIndex.modal};\n`;\n\nconst DeleteButton = styled.button`\n  height: 100%;\n  width: 30;\n  color: ${({ theme }) => theme.palette.common.white};\n  background-color: ${({ theme }) => theme.palette.primary.main};\n  cursor: pointer;\n  border: none;\n  &:focus {\n    outline: none;\n  }\n`;\n\nconst Input = styled.input.attrs(() => ({\n  'data-testid': 'criterion-input',\n}))`\n  width: 350px;\n  height: ${CRITERION_BOX_HEIGHT}px;\n  padding: 10px;\n  box-sizing: border-box;\n  font-size: 1rem;\n`;\n\ninterface CriterionBoxProps {\n  criteria: Criterion[];\n  criterion: Criterion;\n  criterionIndex: number;\n  serviceNames: string[];\n  remoteServiceNames: string[];\n  spanNames: string[];\n  autocompleteKeys: string[];\n  autocompleteValues: string[];\n  isLoadingServiceNames: boolean;\n  isLoadingRemoteServiceNames: boolean;\n  isLoadingSpanNames: boolean;\n  isLoadingAutocompleteValues: boolean;\n  isFocused: boolean;\n  onFocus: (index: number) => void;\n  onBlur: () => void;\n  onDecide: (index: number) => void;\n  onChange: (index: number, criterion: Criterion) => void;\n  onDelete: (index: number) => void;\n  loadAutocompleteValues: (autocompleteKey: string) => void;\n}\n\nconst initialText = (criterion: Criterion) => {\n  if (criterion.key) {\n    if (criterion.value) {\n      return `${criterion.key}=${criterion.value}`;\n    }\n    return `${criterion.key}=`;\n  }\n  return '';\n};\n\nconst CriterionBox: React.FC<CriterionBoxProps> = ({\n  criteria,\n  criterion,\n  criterionIndex,\n  serviceNames,\n  remoteServiceNames,\n  spanNames,\n  autocompleteKeys,\n  autocompleteValues,\n  isLoadingServiceNames,\n  isLoadingRemoteServiceNames,\n  isLoadingSpanNames,\n  isLoadingAutocompleteValues,\n  isFocused,\n  onFocus,\n  onBlur,\n  onDecide,\n  onChange,\n  onDelete,\n  loadAutocompleteValues,\n}) => {\n  const inputEl = useRef<HTMLInputElement>(null);\n\n  const [text, setText] = useState(initialText(criterion));\n  const [fixedText, setFixedText] = useState(initialText(criterion));\n\n  useMount(() => {\n    if (inputEl.current) {\n      inputEl.current.focus();\n    }\n  });\n\n  const prevIsFocused = useRef(isFocused);\n  useLayoutEffect(() => {\n    if (prevIsFocused.current && !isFocused) {\n      if (!fixedText) {\n        onDelete(criterionIndex);\n        return;\n      }\n      let strs = fixedText.split('=');\n\n      if (strs.length === 1 || strs[1] === '') {\n        switch (strs[0]) {\n          // If the value does not exist in these conditions, delete this criterion.\n          case 'serviceName':\n          case 'spanName':\n          case 'remoteServiceName':\n          case 'maxDuration':\n          case 'minDuration':\n          case 'tagQuery':\n            setFixedText('');\n            onDelete(criterionIndex);\n            return;\n          default:\n            break;\n        }\n      }\n\n      // If the length is greater than 2, there is more than one \"=\" in the text.\n      // Service names, span names, and tag's keys and values can contain '=',\n      // so this is also valid.\n      // In this case, treat the first \"=\" as a separator between key and value.\n      if (strs.length > 2) {\n        strs = fixedText.split(/=(.+)/);\n      }\n      onChange(criterionIndex, newCriterion(strs[0], strs[1] || ''));\n    } else if (!prevIsFocused.current && isFocused) {\n      if (inputEl.current) {\n        inputEl.current.focus();\n      }\n    }\n    prevIsFocused.current = isFocused;\n  }, [isFocused, fixedText, onChange, onDelete, criterionIndex]);\n\n  const [keyText, valueText] = useMemo(() => {\n    const ss = fixedText.split('=');\n    return [ss[0], ss[1] || ''];\n  }, [fixedText]);\n\n  const isEnteringKey = !text.includes('=');\n  const isLoadingSuggestions = useMemo(() => {\n    if (isEnteringKey) {\n      return false;\n    }\n    switch (keyText) {\n      case 'serviceName':\n        return isLoadingServiceNames;\n      case 'spanName':\n        return isLoadingSpanNames;\n      case 'remoteServiceName':\n        return isLoadingRemoteServiceNames;\n      default:\n        if (autocompleteKeys.includes(keyText)) {\n          return isLoadingAutocompleteValues;\n        }\n    }\n    return false;\n  }, [\n    keyText,\n    isEnteringKey,\n    isLoadingServiceNames,\n    isLoadingSpanNames,\n    isLoadingRemoteServiceNames,\n    isLoadingAutocompleteValues,\n    autocompleteKeys,\n  ]);\n\n  const suggestions = useMemo(() => {\n    if (isEnteringKey) {\n      let keys;\n\n      // spanName and remoteServiceName are fetched after serviceName is\n      // selected, so so they will not be displayed until serviceName is selected.\n      if (criteria.find(({ key }) => key === 'serviceName')) {\n        keys = [\n          'serviceName',\n          'spanName',\n          'remoteServiceName',\n          'maxDuration',\n          'minDuration',\n          'tagQuery',\n          ...autocompleteKeys,\n        ];\n      } else {\n        keys = [\n          'serviceName',\n          'maxDuration',\n          'minDuration',\n          'tagQuery',\n          ...autocompleteKeys,\n        ];\n      }\n\n      return keys\n        .filter(\n          (key) =>\n            !criteria.find((criterion) => criterion.key === key) ||\n            criterion.key === key,\n        )\n        .filter((key) => key.includes(keyText));\n    }\n\n    let ss: string[];\n    switch (keyText) {\n      case 'serviceName':\n        ss = serviceNames;\n        break;\n      case 'spanName':\n        ss = spanNames;\n        break;\n      case 'remoteServiceName':\n        ss = remoteServiceNames;\n        break;\n      default:\n        if (autocompleteKeys.includes(keyText)) {\n          ss = autocompleteValues;\n          break;\n        }\n        return undefined;\n    }\n    return ss.filter((s) => s.includes(valueText));\n  }, [\n    autocompleteKeys,\n    autocompleteValues,\n    serviceNames,\n    spanNames,\n    remoteServiceNames,\n    isEnteringKey,\n    keyText,\n    valueText,\n    criteria,\n    criterion,\n  ]);\n\n  useEffect(() => {\n    if (autocompleteKeys.includes(keyText)) {\n      loadAutocompleteValues(keyText);\n    }\n  }, [keyText, autocompleteKeys, loadAutocompleteValues]);\n\n  const [suggestionIndex, setSuggestionIndex] = useState(-1);\n\n  const handleChange = useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      setText(event.target.value);\n      setFixedText(event.target.value);\n      setSuggestionIndex(-1);\n    },\n    [],\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLInputElement>) => {\n      switch (event.key) {\n        case 'Enter':\n          event.preventDefault();\n          if (isEnteringKey) {\n            if (!text) {\n              onDecide(criterionIndex);\n              return;\n            }\n            const newText = `${text}=`;\n            setText(newText);\n            setFixedText(newText);\n            setSuggestionIndex(-1);\n          } else {\n            setFixedText(text);\n            setSuggestionIndex(-1);\n            onDecide(criterionIndex);\n          }\n          break;\n        case 'ArrowUp': {\n          event.preventDefault();\n          if (\n            isLoadingSuggestions ||\n            !suggestions ||\n            suggestions.length === 0 ||\n            !(suggestionIndex > 0)\n          ) {\n            break;\n          }\n          const nextSuggestionIndex = suggestionIndex - 1;\n          setSuggestionIndex(nextSuggestionIndex);\n          if (isEnteringKey) {\n            setText(suggestions[nextSuggestionIndex]);\n          } else {\n            setText(`${keyText}=${suggestions[nextSuggestionIndex]}`);\n          }\n          break;\n        }\n        case 'ArrowDown':\n        case 'Tab': {\n          event.preventDefault();\n          if (\n            isLoadingSuggestions ||\n            !suggestions ||\n            suggestions.length === 0\n          ) {\n            break;\n          }\n          let nextSuggestionIndex: number;\n          if (suggestionIndex === suggestions.length - 1) {\n            nextSuggestionIndex = 0;\n          } else {\n            nextSuggestionIndex = suggestionIndex + 1;\n          }\n          setSuggestionIndex(nextSuggestionIndex);\n          if (isEnteringKey) {\n            setText(suggestions[nextSuggestionIndex]);\n          } else {\n            setText(`${keyText}=${suggestions[nextSuggestionIndex]}`);\n          }\n          break;\n        }\n        case 'Escape': {\n          onDecide(criterionIndex);\n          break;\n        }\n        default:\n          break;\n      }\n    },\n    [\n      isEnteringKey,\n      text,\n      onDecide,\n      criterionIndex,\n      isLoadingSuggestions,\n      suggestions,\n      suggestionIndex,\n      keyText,\n    ],\n  );\n\n  const handleDeleteButtonClick = useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      event.stopPropagation();\n      onDelete(criterionIndex);\n    },\n    [onDelete, criterionIndex],\n  );\n\n  const handleSuggestionItemClick = (index: number) => () => {\n    if (!suggestions) {\n      return;\n    }\n    if (isEnteringKey) {\n      const newText = `${suggestions[index]}=`;\n      setText(newText);\n      setFixedText(newText);\n      setSuggestionIndex(-1);\n      if (inputEl.current) {\n        // When the suggestion is clicked, the focus is removed from input.\n        // So need to refocus.\n        inputEl.current.focus();\n      }\n    } else {\n      const newText = `${keyText}=${suggestions[index]}`;\n      setText(newText);\n      setFixedText(newText);\n      setSuggestionIndex(-1);\n      onDecide(criterionIndex);\n    }\n  };\n\n  const handleClick = useCallback(() => {\n    onFocus(criterionIndex);\n  }, [criterionIndex, onFocus]);\n\n  if (!isFocused) {\n    return (\n      <Root onClick={handleClick}>\n        <Box\n          maxWidth={150}\n          height=\"100%\"\n          bgcolor=\"primary.dark\"\n          overflow=\"hidden\"\n          whiteSpace=\"nowrap\"\n          textOverflow=\"ellipsis\"\n          lineHeight={`${CRITERION_BOX_HEIGHT}px`}\n          px={1}\n        >\n          {criterion.key}\n        </Box>\n        {criterion.value && (\n          <Box\n            maxWidth={200}\n            height=\"100%\"\n            bgcolor=\"primary.main\"\n            overflow=\"hidden\"\n            whiteSpace=\"nowrap\"\n            textOverflow=\"ellipsis\"\n            lineHeight={`${CRITERION_BOX_HEIGHT}px`}\n            px={1}\n          >\n            {criterion.value}\n          </Box>\n        )}\n        <DeleteButton type=\"button\" onClick={handleDeleteButtonClick}>\n          <FontAwesomeIcon icon={faTimes} size=\"lg\" />\n        </DeleteButton>\n      </Root>\n    );\n  }\n\n  return (\n    <ClickAwayListener onClickAway={onBlur}>\n      <FocusedRoot>\n        <Input\n          ref={inputEl}\n          value={text}\n          onKeyDown={handleKeyDown}\n          onChange={handleChange}\n        />\n        {(isLoadingSuggestions ||\n          (suggestions && suggestions.length !== 0)) && (\n          <SuggestionList\n            suggestions={suggestions || []}\n            isLoadingSuggestions={isLoadingSuggestions}\n            suggestionIndex={suggestionIndex}\n            onItemClick={handleSuggestionItemClick}\n          />\n        )}\n        {!isEnteringKey &&\n          (keyText === 'minDuration' ||\n            keyText === 'maxDuration' ||\n            keyText === 'tagQuery') && <HowToUse target={keyText} />}\n      </FocusedRoot>\n    </ClickAwayListener>\n  );\n};\n\nexport default CriterionBox;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/HowToUse.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Typography } from '@material-ui/core';\nimport React from 'react';\nimport styled from 'styled-components';\n\nconst Root = styled.div`\n  position: absolute;\n  top: 45px;\n  left: 0px;\n  right: 0px;\n  border-radius: 1px;\n  background-color: ${({ theme }) => theme.palette.background.paper};\n  box-shadow: ${({ theme }) => theme.shadows[3]};\n  max-height: 300px;\n  overflow: auto;\n  z-index: ${({ theme }) => theme.zIndex.modal};\n  padding: ${({ theme }) => theme.spacing(1)}px;\n`;\n\nconst Code = styled.code`\n  padding: 2px;\n  background-color: ${({ theme }) => theme.palette.grey[100]};\n  border: 1px solid ${({ theme }) => theme.palette.grey[300]};\n  border-radius: 3px;\n  box-sizing: border-box;\n  display: inline-block;\n  font-family: monospace;\n`;\n\nconst ExampleList = styled.ul`\n  margin-block-start: ${({ theme }) => theme.spacing(0.5)}px;\n  margin-block-end: ${({ theme }) => theme.spacing(0.5)}px;\n`;\n\nconst ExampleListItem = styled.li`\n  padding-top: 2px;\n  padding-bottom: 2px;\n`;\n\ninterface HowToUseProps {\n  target: 'minDuration' | 'maxDuration' | 'tagQuery';\n}\n\nconst HowToUse: React.FC<HowToUseProps> = ({ target }) => {\n  let content;\n\n  switch (target) {\n    case 'minDuration':\n    case 'maxDuration':\n      content = (\n        <>\n          <Typography variant=\"h6\">Examples</Typography>\n          <ExampleList>\n            <ExampleListItem>\n              <Code>{target}=10s</Code>\n              &nbsp;(Seconds)\n            </ExampleListItem>\n            <ExampleListItem>\n              <Code>{target}=10ms</Code>\n              &nbsp;(Milliseconds)\n            </ExampleListItem>\n            <ExampleListItem>\n              <Code>{target}=10us</Code>\n              &nbsp;(Microseconds)\n            </ExampleListItem>\n          </ExampleList>\n          If no units are specified, the number is treated as <Code>us</Code>.\n          <Code>{target}=10</Code> is the same as <Code>{target}=10us</Code>.\n        </>\n      );\n      break;\n    case 'tagQuery':\n      content = (\n        <>\n          Limit results to traces that include one or more tags. Tags are only\n          searchable by their exact value. Use <Code> and </Code> to combine\n          terms.\n          <Code> or </Code> is not supported.\n          <Typography variant=\"h6\">Example</Typography>\n          <ExampleList>\n            <ExampleListItem>\n              <Code>tagQuery=error and http.method=POST</Code>\n            </ExampleListItem>\n          </ExampleList>\n        </>\n      );\n      break;\n    default:\n      break;\n  }\n\n  return <Root>{content}</Root>;\n};\n\nexport default HowToUse;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/SearchBar.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { fireEvent, screen } from '@testing-library/react';\nimport React from 'react';\nimport { vi, describe, it, expect } from 'vitest';\nimport { SearchBarImpl } from './SearchBar';\nimport render from '../../../test/util/render-with-default-settings';\n\nvi.mock('./CriterionBox', () => ({\n  default: () => {\n    const shortid2 = import('shortid');\n\n    return ({ criterionIndex, onChange }) => (\n      <input\n        key=\"default\"\n        onChange={(event) => {\n          const ss = event.target.value.split('=', 2);\n          onChange(criterionIndex, {\n            key: ss[0],\n            value: ss[1],\n            id: shortid2.generate(),\n          });\n        }}\n        data-testid=\"criterion-box\"\n      />\n    );\n  },\n}));\n\ndescribe('<SearchBar />', () => {\n  const commonProps = {\n    criteria: [],\n    onChange: vi.fn(),\n    serviceNames: [],\n    isLoadingServiceNames: false,\n    spanNames: [],\n    isLoadingSpanNames: false,\n    remoteServiceNames: [],\n    isLoadingRemoteServiceNames: false,\n    autocompleteKeys: [],\n    autocompleteValues: [],\n    isLoadingAutocompleteValues: false,\n    loadRemoteServices: vi.fn(),\n    loadSpans: vi.fn(),\n  };\n\n  it('should add an empty criterion when add button is clicked', () => {\n    render(<SearchBarImpl {...commonProps} />);\n    fireEvent.click(screen.getByTestId('add-button'));\n    expect(commonProps.onChange.mock.calls.length).toBe(1);\n    expect(commonProps.onChange.mock.calls[0][0][0].key).toEqual('');\n    expect(commonProps.onChange.mock.calls[0][0][0].value).toEqual('');\n  });\n\n  /*\n  it('should load spans and remote services when service name is changed', () => {\n    let criteria = [\n      { key: 'serviceName', value: 'serviceA', id: shortid.generate() },\n    ];\n    const onChange = (newCriteria) => {\n      criteria = newCriteria;\n    };\n\n    let props = { ...commonProps, criteria, onChange };\n    const { rerender } = render(<SearchBarImpl {...props} />);\n\n    expect(commonProps.loadSpans.mock.calls.length).toBe(1);\n    expect(commonProps.loadRemoteServices.mock.calls.length).toBe(1);\n    expect(commonProps.loadSpans.mock.calls[0][0]).toBe('serviceA');\n    expect(commonProps.loadRemoteServices.mock.calls[0][0]).toBe('serviceA');\n\n    // serviceA -> serviceB\n    fireEvent.change(screen.getByTestId('criterion-box'), {\n      target: { value: 'serviceName=serviceB' },\n    });\n\n    props = { ...commonProps, criteria, onChange };\n    rerender(<SearchBarImpl {...props} />);\n\n    expect(commonProps.loadSpans.mock.calls.length).toBe(2);\n    expect(commonProps.loadRemoteServices.mock.calls.length).toBe(2);\n    expect(commonProps.loadSpans.mock.calls[1][0]).toBe('serviceB');\n    expect(commonProps.loadRemoteServices.mock.calls[1][0]).toBe('serviceB');\n\n    // serviceB -> serviceB\n    // If there is no change, nothing will be done.\n    fireEvent.change(screen.getByTestId('criterion-box'), {\n      target: { value: 'serviceName=serviceB' },\n    });\n    expect(commonProps.loadSpans.mock.calls.length).toBe(2);\n    expect(commonProps.loadRemoteServices.mock.calls.length).toBe(2);\n  }); */\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/SearchBar.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-shadow */\n\nimport { faPlus } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport {\n  Box,\n  Button,\n  Theme,\n  createStyles,\n  makeStyles,\n} from '@material-ui/core';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { connect } from 'react-redux';\nimport { ThunkDispatch } from 'redux-thunk';\nimport { loadAutocompleteValues } from '../../../slices/autocompleteValuesSlice';\nimport { loadRemoteServices } from '../../../slices/remoteServicesSlice';\nimport { loadSpans } from '../../../slices/spansSlice';\nimport { RootState } from '../../../store';\nimport Criterion, { newCriterion } from '../Criterion';\nimport { CRITERION_BOX_HEIGHT } from './constants';\nimport CriterionBox from './CriterionBox';\n\nconst useStyles = makeStyles((theme: Theme) =>\n  createStyles({\n    root: {\n      display: 'flex',\n      gap: `${theme.spacing(1)}px`,\n      alignItems: 'center',\n      padding: theme.spacing(1),\n      borderRadius: 3,\n      backgroundColor: theme.palette.background.paper,\n      flexWrap: 'wrap',\n      border: `1px solid ${theme.palette.divider}`,\n    },\n    addButton: {\n      height: CRITERION_BOX_HEIGHT,\n      width: CRITERION_BOX_HEIGHT,\n      minWidth: 0,\n      color: theme.palette.common.white,\n    },\n  }),\n);\n\ntype SearchBarProps = {\n  searchTraces: () => void;\n  criteria: Criterion[];\n  onChange: (criteria: Criterion[]) => void;\n  serviceNames: string[];\n  isLoadingServiceNames: boolean;\n  spanNames: string[];\n  isLoadingSpanNames: boolean;\n  remoteServiceNames: string[];\n  isLoadingRemoteServiceNames: boolean;\n  autocompleteKeys: string[];\n  autocompleteValues: string[];\n  isLoadingAutocompleteValues: boolean;\n  loadRemoteServices: (serviceName: string) => void;\n  loadSpans: (serviceName: string) => void;\n  loadAutocompleteValues: (autocompleteKey: string) => void;\n};\n\nexport const SearchBarImpl: React.FC<SearchBarProps> = ({\n  searchTraces,\n  criteria,\n  onChange,\n  serviceNames,\n  isLoadingServiceNames,\n  spanNames,\n  isLoadingSpanNames,\n  remoteServiceNames,\n  isLoadingRemoteServiceNames,\n  autocompleteKeys,\n  autocompleteValues,\n  isLoadingAutocompleteValues,\n  loadRemoteServices,\n  loadSpans,\n  loadAutocompleteValues,\n}) => {\n  const classes = useStyles();\n\n  // criterionIndex is the index of the criterion currently being edited.\n  // If the value is -1, there is no criterion being edited.\n  const [criterionIndex, setCriterionIndex] = useState(-1);\n\n  const handleCriterionFocus = (index: number) => {\n    setCriterionIndex(index);\n  };\n\n  const handleCriterionChange = (index: number, criterion: Criterion) => {\n    const newCriteria = [...criteria];\n    newCriteria[index] = criterion;\n    onChange(newCriteria);\n  };\n\n  const handleCriterionBlur = () => {\n    setCriterionIndex(-1);\n  };\n\n  const handleCriterionDelete = (index: number) => {\n    const newCriteria = criteria.filter((_, i) => i !== index);\n    onChange(newCriteria);\n    setCriterionIndex(-1);\n  };\n\n  const handleCriterionDecide = (index: number) => {\n    if (index === criteria.length - 1) {\n      const newCriteria = [...criteria];\n      newCriteria.push(newCriterion('', ''));\n      onChange(newCriteria);\n      const nextCriterionIndex = criteria.length;\n      setCriterionIndex(nextCriterionIndex);\n    } else {\n      setCriterionIndex(-1);\n    }\n  };\n\n  const handleAddButtonClick = useCallback(() => {\n    const newCriteria = [...criteria];\n    newCriteria.push(newCriterion('', ''));\n    onChange(newCriteria);\n    const nextCriterionIndex = criteria.length;\n    setCriterionIndex(nextCriterionIndex);\n  }, [criteria, onChange]);\n\n  const prevServiceName = useRef('');\n  useEffect(() => {\n    const criterion = criteria.find(\n      // eslint-disable-next-line no-shadow\n      (criterion) => criterion.key === 'serviceName',\n    );\n    const serviceName = criterion ? criterion.value : '';\n    if (serviceName !== prevServiceName.current) {\n      prevServiceName.current = serviceName;\n      loadSpans(serviceName);\n      loadRemoteServices(serviceName);\n    }\n  }, [criteria, loadSpans, loadRemoteServices]);\n\n  // Search for traces if not all criterions are in focus\n  // and the Enter key is pressed.\n  // Use ref to use the latest criterionIndex state in the callback.\n  const isFocusedRef = useRef(false);\n  isFocusedRef.current = criterionIndex !== -1;\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent) => {\n      // Use setTimeout to ensure that the callback is called\n      // after the criterionIndex has been updated.\n      setTimeout(() => {\n        if (!document.activeElement) {\n          return;\n        }\n        if (\n          !isFocusedRef.current &&\n          document.activeElement.tagName === 'BODY' &&\n          event.key === 'Enter'\n        ) {\n          searchTraces();\n        }\n      }, 0); // Maybe 0 is enough.\n    },\n    [searchTraces],\n  );\n  useEffect(() => {\n    window.addEventListener('keydown', handleKeyDown);\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [handleKeyDown]);\n\n  return (\n    <Box className={classes.root}>\n      {criteria.map((criterion, index) => (\n        <CriterionBox\n          key={criterion.id}\n          criteria={criteria}\n          criterion={criterion}\n          criterionIndex={index}\n          serviceNames={serviceNames}\n          remoteServiceNames={remoteServiceNames}\n          spanNames={spanNames}\n          autocompleteKeys={autocompleteKeys}\n          autocompleteValues={autocompleteValues}\n          isLoadingServiceNames={isLoadingServiceNames}\n          isLoadingRemoteServiceNames={isLoadingRemoteServiceNames}\n          isLoadingSpanNames={isLoadingSpanNames}\n          isLoadingAutocompleteValues={isLoadingAutocompleteValues}\n          isFocused={index === criterionIndex}\n          onFocus={handleCriterionFocus}\n          onBlur={handleCriterionBlur}\n          onDecide={handleCriterionDecide}\n          onChange={handleCriterionChange}\n          onDelete={handleCriterionDelete}\n          loadAutocompleteValues={loadAutocompleteValues}\n        />\n      ))}\n      <Button\n        color=\"secondary\"\n        variant=\"contained\"\n        onClick={handleAddButtonClick}\n        className={classes.addButton}\n        data-testid=\"add-button\"\n      >\n        <FontAwesomeIcon icon={faPlus} size=\"lg\" />\n      </Button>\n    </Box>\n  );\n};\n\n// For unit testing, `connect` is easier to use than\n// useSelector or useDispatch hooks.\nconst mapStateToProps = (state: RootState) => ({\n  serviceNames: state.services.services,\n  isLoadingServiceNames: state.services.isLoading,\n  spanNames: state.spans.spans,\n  isLoadingSpanNames: state.spans.isLoading,\n  remoteServiceNames: state.remoteServices.remoteServices,\n  isLoadingRemoteServiceNames: state.remoteServices.isLoading,\n  autocompleteKeys: state.autocompleteKeys.autocompleteKeys,\n  autocompleteValues: state.autocompleteValues.autocompleteValues,\n  isLoadingAutocompleteValues: state.autocompleteValues.isLoading,\n});\n\n// TODO: Give the appropriate type to ThunkDispatch after TypeScriptizing all action creators.\nconst mapDispatchToProps = (\n  dispatch: ThunkDispatch<RootState, undefined, any>,\n) => ({\n  loadRemoteServices: (serviceName: string) => {\n    dispatch(loadRemoteServices(serviceName));\n  },\n  loadSpans: (serviceName: string) => {\n    dispatch(loadSpans(serviceName));\n  },\n  loadAutocompleteValues: (autocompleteKey: string) => {\n    dispatch(loadAutocompleteValues(autocompleteKey));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(SearchBarImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/SuggestionList.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { CircularProgress } from '@material-ui/core';\nimport React, { useEffect, useRef } from 'react';\nimport styled from 'styled-components';\nimport { getTheme } from '../../../util/theme';\n\nconst Root = styled.div<{ isLoading: boolean }>`\n  position: absolute;\n  top: 45px;\n  left: 0px;\n  right: 0px;\n  border-radius: 1px;\n  background-color: ${({ theme }) => theme.palette.background.paper};\n  box-shadow: ${({ theme }) => theme.shadows[3]};\n  max-height: 300px;\n  overflow: auto;\n  z-index: ${({ theme }) => theme.zIndex.modal};\n\n  ${({ isLoading, theme }) =>\n    !isLoading\n      ? ''\n      : `\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: ${theme.spacing(1)}px;\n      `}\n`;\n\nconst List = styled.ul`\n  list-style-type: none;\n  margin: 0px;\n  padding: 0px;\n  font-size: 1rem;\n`;\n\nconst ListItem = styled.li<{ isFocused: boolean }>`\n  padding-top: 8px;\n  padding-bottom: 8px;\n  padding-right: 12px;\n  padding-left: 12px;\n  border-left: ${({ isFocused, theme }) =>\n    `5px solid ${isFocused ? theme.palette.primary.main : 'rgba(0, 0, 0, 0)'}`};\n  cursor: pointer;\n  &:hover {\n    background-color: ${({ theme }) =>\n      getTheme() === 'dark'\n        ? theme.palette.grey[600]\n        : theme.palette.grey[300]};\n  }\n`;\n\ninterface SuggestionListProps {\n  suggestions: string[];\n  isLoadingSuggestions: boolean;\n  suggestionIndex: number;\n  onItemClick: (index: number) => () => void;\n}\n\nconst SuggestionList: React.FC<SuggestionListProps> = ({\n  suggestions,\n  isLoadingSuggestions,\n  suggestionIndex,\n  onItemClick,\n}) => {\n  const listEls = useRef<HTMLLIElement[]>([]);\n  const setListEl = (index: number) => (el: HTMLLIElement) => {\n    listEls.current[index] = el;\n  };\n\n  useEffect(() => {\n    if (suggestionIndex === -1) {\n      return;\n    }\n    listEls.current[suggestionIndex].scrollIntoView(false);\n  }, [suggestionIndex]);\n\n  if (isLoadingSuggestions) {\n    return (\n      <Root isLoading>\n        <CircularProgress size={20} />\n      </Root>\n    );\n  }\n\n  return (\n    <Root isLoading={false}>\n      <List>\n        {suggestions.map((suggestion, index) => (\n          <ListItem\n            key={suggestion}\n            ref={setListEl(index)}\n            isFocused={suggestionIndex === index}\n            onClick={onItemClick(index)}\n          >\n            {suggestion}\n          </ListItem>\n        ))}\n      </List>\n    </Root>\n  );\n};\n\nexport default SuggestionList;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/constants.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport const CRITERION_BOX_HEIGHT = 34;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/SearchBar/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { default } from './SearchBar';\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/TraceSummaryRow.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport React from 'react';\nimport TraceSummaryRow from './TraceSummaryRow';\nimport render from '../../test/util/render-with-default-settings';\nimport { screen } from '@testing-library/react';\n\ndescribe('<TraceSummaryRow />', () => {\n  it('should render timestamp and duration in correct unit', () => {\n    render(\n      <table>\n        <tbody>\n          <TraceSummaryRow\n            traceSummary={{\n              traceId: 'a03ee8fff1dcd9b9',\n              timestamp: 1571896375237354,\n              duration: 131848,\n              serviceSummaries: [],\n              spanCount: 10,\n              width: 10,\n              root: {\n                serviceName: 'routing',\n                spanName: 'post /location/update/v4',\n              },\n            }}\n          />\n        </tbody>\n      </table>,\n    );\n\n    const startTimeFormat = screen.getByTestId(\n      'TraceSummaryRow-startTimeFormat',\n    );\n    expect(startTimeFormat).toBeDefined();\n    // Don't assert on hour as the timezone will be different in CI\n    expect(startTimeFormat.textContent).toMatch(\n      /10\\/2[34] [0-9][0-9]:52:55:237/,\n    );\n    // Intentionally not asserting the relative time from now as it would drift tests\n\n    const duration = screen.getByTestId('TraceSummaryRow-duration');\n    expect(duration).toBeDefined();\n    expect(duration.textContent).toBe('131.848ms');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/TraceSummaryRow.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Box,\n  Collapse,\n  IconButton,\n  TableCell,\n  TableRow,\n  Typography,\n  Button,\n} from '@material-ui/core';\nimport KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';\nimport KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';\nimport moment from 'moment';\nimport React, { useMemo, useCallback } from 'react';\nimport { Link } from 'react-router-dom';\nimport styled from 'styled-components';\nimport { ErrorOutline as ErrorOutlineIcon } from '@material-ui/icons';\nimport TraceSummary from '../../models/TraceSummary';\nimport { formatDuration } from '../../util/timestamp';\nimport ServiceBadge from '../common/ServiceBadge';\nimport { selectColorByInfoClass } from '../../constants/color';\n\ninterface TraceSummaryRowProps {\n  traceSummary: TraceSummary;\n  toggleFilter: (serviceName: string) => void;\n  open: boolean;\n  toggleOpen: (traceId: string) => void;\n}\n\nfunction shouldShowIcon(traceSummary: TraceSummary) {\n  return (\n    traceSummary.infoClass === 'trace-error-critical' ||\n    traceSummary.infoClass === 'trace-error-transient'\n  );\n}\n\nconst TraceSummaryRow: React.FC<TraceSummaryRowProps> = ({\n  traceSummary,\n  toggleFilter,\n  open,\n  toggleOpen,\n}) => {\n  const startTime = moment(traceSummary.timestamp / 1000);\n\n  const sortedServiceSummaries = useMemo(\n    () =>\n      [...traceSummary.serviceSummaries].sort(\n        (a, b) => b.spanCount - a.spanCount,\n      ),\n    [traceSummary.serviceSummaries],\n  );\n\n  const handleToggleOpen = useCallback(() => {\n    toggleOpen(traceSummary.traceId);\n  }, [toggleOpen, traceSummary.traceId]);\n\n  return (\n    <>\n      <Root>\n        <TableCell>\n          <IconButton size=\"small\" onClick={handleToggleOpen}>\n            {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}\n          </IconButton>\n        </TableCell>\n        <TableCell>\n          <Box display=\"flex\" alignItems=\"center\">\n            {shouldShowIcon(traceSummary) && (\n              <ErrorOutlineIcon\n                style={{ marginRight: '8px' }}\n                fontSize=\"small\"\n                color=\"error\"\n              />\n            )}\n            {`${traceSummary.root.serviceName}: ${traceSummary.root.spanName}`}\n          </Box>\n        </TableCell>\n        <TableCell>\n          <Box display=\"flex\" justifyContent=\"flex-end\" alignItems=\"center\">\n            <FromNowTypography>{startTime.fromNow()}</FromNowTypography>\n            <Typography\n              variant=\"body2\"\n              color=\"textSecondary\"\n              data-testid=\"TraceSummaryRow-startTimeFormat\"\n            >\n              ({startTime.format('MM/DD HH:mm:ss:SSS')})\n            </Typography>\n          </Box>\n        </TableCell>\n        <TableCell align=\"right\">{traceSummary.spanCount}</TableCell>\n        <TableCell align=\"left\">\n          <Box\n            position=\"relative\"\n            width=\"100%\"\n            top=\"-4px\"\n            data-testid=\"TraceSummaryRow-duration\"\n          >\n            {formatDuration(traceSummary.duration)}\n            <DurationBar\n              width={traceSummary.width}\n              infoClass={traceSummary.infoClass}\n            />\n          </Box>\n        </TableCell>\n        <TableCell>\n          <Button\n            variant=\"outlined\"\n            size=\"small\"\n            component={Link}\n            to={`traces/${traceSummary.traceId}`}\n          >\n            Show\n          </Button>\n        </TableCell>\n      </Root>\n      <TableRow>\n        <CollapsibleTableCell>\n          <Collapse in={open} timeout=\"auto\" unmountOnExit>\n            <Box margin={1}>\n              <BadgesWrapper>\n                {sortedServiceSummaries.map((serviceSummary) => (\n                  <ServiceBadge\n                    key={serviceSummary.serviceName}\n                    serviceName={serviceSummary.serviceName}\n                    count={serviceSummary.spanCount}\n                    onClick={toggleFilter}\n                  />\n                ))}\n              </BadgesWrapper>\n            </Box>\n          </Collapse>\n        </CollapsibleTableCell>\n      </TableRow>\n    </>\n  );\n};\n\nexport default TraceSummaryRow;\n\nconst Root = styled(TableRow)`\n  & > * {\n    border-bottom: unset;\n  }\n`;\n\nconst FromNowTypography = styled(Typography).attrs({\n  variant: 'body2',\n})`\n  margin-right: ${({ theme }) => theme.spacing(1)}px;\n`;\n\nconst DurationBar = styled.div<{ width: number; infoClass?: string }>`\n  position: absolute;\n  background-color: ${({ infoClass }) =>\n    selectColorByInfoClass(infoClass || '')};\n  opacity: 0.9;\n  top: 80%;\n  left: 0;\n  height: 50%;\n  width: ${({ width }) => width}%;\n  border-top-right-radius: 3px;\n  border-bottom-right-radius: 3px;\n`;\n\nconst CollapsibleTableCell = styled(TableCell).attrs({\n  colSpan: 6,\n})`\n  padding-bottom: 0;\n  padding-top: 0;\n`;\n\nconst BadgesWrapper = styled.div`\n  display: flex;\n  flex-wrap: wrap;\n  gap: ${({ theme }) => theme.spacing(0.5)}px;\n`;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/TraceSummaryTable.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableRow,\n  TableSortLabel,\n} from '@material-ui/core';\nimport React, { useState, useCallback } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport TraceSummary from '../../models/TraceSummary';\nimport TraceSummaryRow from './TraceSummaryRow';\n\ninterface TraceSummaryTableProps {\n  traceSummaries: TraceSummary[];\n  toggleFilter: (serivceName: string) => void;\n  traceSummaryOpenMap: { [key: string]: boolean };\n  toggleTraceSummaryOpen: (traceId: string) => void;\n}\n\nconst TraceSummaryTable: React.FC<TraceSummaryTableProps> = ({\n  traceSummaries,\n  toggleFilter,\n  traceSummaryOpenMap,\n  toggleTraceSummaryOpen,\n}) => {\n  const [order, setOrder] = useState<'asc' | 'desc'>('desc');\n  const [orderBy, setOrderBy] = useState<\n    'duration' | 'timestamp' | 'spanCount'\n  >('duration');\n  const { t } = useTranslation();\n\n  const handleSortButtonClick = useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      const { cellkey } = event.currentTarget.dataset;\n      if (\n        !cellkey ||\n        (cellkey !== 'duration' &&\n          cellkey !== 'timestamp' &&\n          cellkey !== 'spanCount')\n      ) {\n        return;\n      }\n\n      if (cellkey === orderBy) {\n        setOrder((prev) => {\n          if (prev === 'asc') return 'desc';\n          return 'asc';\n        });\n      } else {\n        setOrder('desc');\n        setOrderBy(cellkey);\n      }\n    },\n    [orderBy],\n  );\n\n  return (\n    <Table size=\"small\">\n      <TableHead>\n        <TableRow>\n          <TableCell />\n          <TableCell>\n            <Trans t={t}>Root</Trans>\n          </TableCell>\n          {[\n            { label: <Trans t={t}>Start Time</Trans>, key: 'timestamp' },\n            { label: <Trans t={t}>Spans</Trans>, key: 'spanCount' },\n            { label: <Trans t={t}>Duration</Trans>, key: 'duration' },\n          ].map(({ label, key }) => (\n            <TableCell\n              key={key}\n              align=\"right\"\n              sortDirection={orderBy === key ? order : false}\n            >\n              <TableSortLabel\n                active={orderBy === key}\n                direction={orderBy === key ? order : 'asc'}\n                data-cellkey={key}\n                onClick={handleSortButtonClick}\n              >\n                {label}\n              </TableSortLabel>\n            </TableCell>\n          ))}\n          <TableCell />\n        </TableRow>\n      </TableHead>\n      <TableBody>\n        {traceSummaries\n          .sort((a, b) => {\n            if (order === 'asc') {\n              return a[orderBy] - b[orderBy];\n            }\n            return b[orderBy] - a[orderBy];\n          })\n          .map((traceSummary) => (\n            <TraceSummaryRow\n              key={traceSummary.traceId}\n              traceSummary={traceSummary}\n              toggleFilter={toggleFilter}\n              open={!!traceSummaryOpenMap[traceSummary.traceId]}\n              toggleOpen={toggleTraceSummaryOpen}\n            />\n          ))}\n      </TableBody>\n    </Table>\n  );\n};\n\nexport default TraceSummaryTable;\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/index.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { default } from './DiscoverPage';\n"
  },
  {
    "path": "zipkin-lens/src/components/DiscoverPage/lookback.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport moment from 'moment';\n\nexport type FixedLookbackValue =\n  | '1m'\n  | '5m'\n  | '15m'\n  | '30m'\n  | '1h'\n  | '2h'\n  | '3h'\n  | '6h'\n  | '12h'\n  | '1d'\n  | '2d'\n  | '7d';\n\nexport const millisecondsToValue: { [key: number]: FixedLookbackValue } = {\n  [60 * 1000]: '1m',\n  [60 * 1000 * 5]: '5m',\n  [60 * 1000 * 15]: '15m',\n  [60 * 1000 * 30]: '30m',\n  [60 * 1000 * 60]: '1h',\n  [60 * 1000 * 60 * 2]: '2h',\n  [60 * 1000 * 60 * 3]: '3h',\n  [60 * 1000 * 60 * 6]: '6h',\n  [60 * 1000 * 60 * 12]: '12h',\n  [60 * 1000 * 60 * 24]: '1d',\n  [60 * 1000 * 60 * 24 * 2]: '2d',\n  [60 * 1000 * 60 * 24 * 7]: '7d',\n};\n\nexport interface FixedLookback {\n  type: 'fixed';\n  value: FixedLookbackValue;\n  endTime: moment.Moment;\n}\n\nexport interface RangeLookback {\n  type: 'range';\n  startTime: moment.Moment;\n  endTime: moment.Moment;\n}\n\nexport interface MillisLookback {\n  type: 'millis';\n  value: number;\n  endTime: moment.Moment;\n}\n\nexport type Lookback = FixedLookback | RangeLookback | MillisLookback;\n\ninterface FixedLookbackEntry {\n  duration: moment.Duration;\n  value: FixedLookbackValue;\n  display: string;\n}\n\nexport const fixedLookbacks: Array<FixedLookbackEntry> = [\n  {\n    duration: moment.duration({ minutes: 1 }),\n    value: '1m',\n    display: 'Last 1 minute',\n  },\n  {\n    duration: moment.duration({ minutes: 5 }),\n    value: '5m',\n    display: 'Last 5 minutes',\n  },\n  {\n    duration: moment.duration({ minutes: 15 }),\n    value: '15m',\n    display: 'Last 15 minutes',\n  },\n  {\n    duration: moment.duration({ minutes: 30 }),\n    value: '30m',\n    display: 'Last 30 minutes',\n  },\n  {\n    duration: moment.duration({ hours: 1 }),\n    value: '1h',\n    display: 'Last 1 hour',\n  },\n  {\n    duration: moment.duration({ hours: 2 }),\n    value: '2h',\n    display: 'Last 2 hours',\n  },\n  {\n    duration: moment.duration({ hours: 3 }),\n    value: '3h',\n    display: 'Last 3 hours',\n  },\n  {\n    duration: moment.duration({ hours: 6 }),\n    value: '6h',\n    display: 'Last 6 hours',\n  },\n  {\n    duration: moment.duration({ hours: 12 }),\n    value: '12h',\n    display: 'Last 12 hours',\n  },\n  {\n    duration: moment.duration({ days: 1 }),\n    value: '1d',\n    display: 'Last 1 day',\n  },\n  {\n    duration: moment.duration({ days: 2 }),\n    value: '2d',\n    display: 'Last 2 days',\n  },\n  {\n    duration: moment.duration({ days: 7 }),\n    value: '7d',\n    display: 'Last 7 days',\n  },\n];\n\nexport const fixedLookbackMap = fixedLookbacks.reduce((acc, cur) => {\n  acc[cur.value] = cur;\n  return acc;\n}, {} as { [key: string]: FixedLookbackEntry });\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/AnnotationTable/AnnotationTable.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  makeStyles,\n  Table,\n  TableBody,\n  TableCell,\n  TableRow,\n} from '@material-ui/core';\nimport React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { AdjustedAnnotation } from '../../../models/AdjustedTrace';\nimport { formatTimestamp } from '../../../util/timestamp';\n\nconst useStyles = makeStyles((theme) => ({\n  table: {\n    tableLayout: 'fixed',\n  },\n  tableRow: {\n    '&:last-child > *': {\n      borderBottom: 'none',\n    },\n  },\n  relativeTimeCell: {\n    width: 120,\n  },\n  labelCell: {\n    width: 120,\n    color: theme.palette.text.secondary,\n  },\n  valueCell: {\n    wordWrap: 'break-word',\n  },\n}));\n\ntype AnnotationTableProps = {\n  annotations: AdjustedAnnotation[];\n};\n\nexport const AnnotationTable = ({ annotations }: AnnotationTableProps) => {\n  const classes = useStyles();\n  const { t } = useTranslation();\n\n  return (\n    <Table size=\"small\" className={classes.table}>\n      <TableBody>\n        {annotations.map((annotation) => (\n          <TableRow\n            key={`${annotation.value}-${annotation.timestamp}`}\n            className={classes.tableRow}\n          >\n            <TableCell className={classes.relativeTimeCell}>\n              {annotation.relativeTime}\n            </TableCell>\n            <TableCell>\n              <Table size=\"small\" className={classes.table}>\n                <TableBody>\n                  {[\n                    {\n                      label: t(`Start Time`),\n                      value: formatTimestamp(annotation.timestamp),\n                    },\n                    { label: 'Value', value: annotation.value },\n                    { label: t(`Address`), value: annotation.endpoint },\n                  ].map(({ label, value }) => (\n                    <TableRow key={label} className={classes.tableRow}>\n                      <TableCell className={classes.labelCell}>\n                        {label}\n                      </TableCell>\n                      <TableCell className={classes.valueCell}>\n                        {value}\n                      </TableCell>\n                    </TableRow>\n                  ))}\n                </TableBody>\n              </Table>\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/AnnotationTable/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { AnnotationTable } from './AnnotationTable';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/AnnotationTooltip/AnnotationTooltip.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Theme, Tooltip, withStyles } from '@material-ui/core';\nimport React, { useMemo } from 'react';\nimport { AdjustedAnnotation } from '../../../models/AdjustedTrace';\nimport { AnnotationTable } from '../AnnotationTable';\n\nconst HtmlTooltip = withStyles((theme: Theme) => ({\n  tooltip: {\n    backgroundColor: theme.palette.background.paper,\n    color: theme.palette.text.primary,\n    maxWidth: 500,\n    fontSize: theme.typography.pxToRem(12),\n    border: `1px solid ${theme.palette.divider}`,\n    boxShadow: theme.shadows[3],\n  },\n}))(Tooltip);\n\ntype AnnotationTooltipProps = {\n  children: JSX.Element;\n  annotation: AdjustedAnnotation;\n};\n\nexport const AnnotationTooltip = ({\n  children,\n  annotation,\n}: AnnotationTooltipProps) => {\n  const annotations = useMemo(() => [annotation], [annotation]);\n\n  return (\n    <HtmlTooltip\n      title={<AnnotationTable annotations={annotations} />}\n      placement=\"top\"\n    >\n      {children}\n    </HtmlTooltip>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/AnnotationTooltip/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { AnnotationTooltip } from './AnnotationTooltip';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Header/Header.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, Button, makeStyles, Typography } from '@material-ui/core';\nimport { List as ListIcon } from '@material-ui/icons';\nimport React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport AdjustedTrace from '../../../models/AdjustedTrace';\nimport Span from '../../../models/Span';\nimport { HeaderMenu } from './HeaderMenu';\n\n// @ts-ignore\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    borderBottom: `1px solid ${theme.palette.divider}`,\n  },\n  titleRow: {\n    backgroundColor: theme.palette.background.paper,\n    padding: theme.spacing(1, 2),\n    borderBottom: `1px solid ${theme.palette.divider}`,\n    display: 'flex',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n  },\n  titleRowRight: {\n    display: 'flex',\n    alignItems: 'center',\n    '& > :not(:last-child)': {\n      marginRight: theme.spacing(1),\n    },\n  },\n  infoRow: {\n    padding: theme.spacing(0.5, 2),\n    display: 'flex',\n    backgroundColor: theme.palette.grey[50],\n    fontSize: theme.typography.body2.fontSize,\n  },\n  infoCell: {\n    display: 'flex',\n    // TODO: Should use theme.typography.fontWeighRegular after updating material-ui packages.\n    fontWeight: 400,\n    '&:not(:first-child)': {\n      marginLeft: theme.spacing(1),\n    },\n    '&:not(:last-child)': {\n      paddingRight: theme.spacing(1),\n      borderRight: `1px solid ${theme.palette.divider}`,\n    },\n  },\n  infoCellKey: {\n    color: theme.palette.text.hint,\n    marginRight: theme.spacing(0.5),\n  },\n  infoCellValue: {\n    color: theme.palette.text.primary,\n  },\n}));\n\ntype HeaderProps = {\n  trace: AdjustedTrace;\n  rawTrace: Span[];\n  toggleIsSpanTableOpen: () => void;\n};\n\nexport const Header = ({\n  trace,\n  rawTrace,\n  toggleIsSpanTableOpen,\n}: HeaderProps) => {\n  const classes: any = useStyles();\n  const { t } = useTranslation();\n\n  return (\n    <Box className={classes.root}>\n      <Box className={classes.titleRow}>\n        <Typography variant=\"h6\">\n          {`${trace.rootSpan.serviceName}: ${trace.rootSpan.spanName}`}\n        </Typography>\n        <Box className={classes.titleRowRight}>\n          <Button\n            variant=\"outlined\"\n            size=\"small\"\n            onClick={toggleIsSpanTableOpen}\n            startIcon={<ListIcon />}\n          >\n            Span table\n          </Button>\n          <HeaderMenu trace={trace} rawTrace={rawTrace} />\n        </Box>\n      </Box>\n      <Box className={classes.infoRow}>\n        {[\n          { key: t(`Duration`), value: trace.durationStr },\n          {\n            key: t(`Services`),\n            value: trace.serviceNameAndSpanCounts.length,\n          },\n          { key: t(`Total Spans`), value: trace.spans.length },\n          {\n            key: t(`Trace ID`),\n            value: `${trace.traceId}`,\n          },\n        ].map(({ key, value }) => (\n          <Box key={key} className={classes.infoCell}>\n            <Box className={classes.infoCellKey}>{key}</Box>\n            <Box className={classes.infoCellValue}>{value}</Box>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Header/HeaderMenu.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Button, makeStyles, Menu, MenuItem } from '@material-ui/core';\nimport { Menu as MenuIcon } from '@material-ui/icons';\nimport React, { useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { Trans, useTranslation } from 'react-i18next';\nimport * as api from '../../../constants/api';\nimport AdjustedTrace from '../../../models/AdjustedTrace';\nimport Span from '../../../models/Span';\nimport { setAlert } from '../../App/slice';\nimport { useUiConfig } from '../../UiConfig';\n\nconst useStyles = makeStyles(() => ({\n  iconButton: {\n    minWidth: 32,\n    width: 32,\n    height: 32,\n  },\n}));\n\ntype HeaderMenuProps = {\n  trace: AdjustedTrace;\n  rawTrace: Span[];\n};\n\nexport const HeaderMenu = ({ trace, rawTrace }: HeaderMenuProps) => {\n  const classes = useStyles();\n  const config = useUiConfig();\n  const dispatch = useDispatch();\n  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);\n  const { t } = useTranslation();\n\n  const logsUrl = config.logsUrl\n    ? config.logsUrl.replace(/{traceId}/g, trace.traceId)\n    : undefined;\n  const traceJsonUrl = `${api.TRACE}/${trace.traceId}`;\n  const archivePostUrl = config.archivePostUrl\n    ? config.archivePostUrl\n    : undefined;\n  const archiveUrl = config.archiveUrl\n    ? config.archiveUrl.replace('{traceId}', trace.traceId)\n    : undefined;\n\n  const handleMenuButtonClick = (\n    event: React.MouseEvent<HTMLButtonElement>,\n  ) => {\n    setAnchorEl(event.currentTarget);\n  };\n\n  const handleMenuClose = () => {\n    setAnchorEl(null);\n  };\n\n  const handleArchiveButtonClick = useCallback(() => {\n    // We don't store the raw json in the browser yet, so we need to make an\n    // HTTP call to retrieve it again.\n    fetch(`${api.TRACE}/${trace.traceId}`)\n      .then((response) => {\n        if (!response.ok) {\n          throw new Error('Failed to fetch trace from backend');\n        }\n        return response.json();\n      })\n      .then((json) => {\n        // Add zipkin.archived tag to root span\n        /* eslint-disable-next-line no-restricted-syntax */\n        for (const span of json) {\n          if ('parentId' in span === false) {\n            const tags = span.tags || {};\n            tags['zipkin.archived'] = 'true';\n            span.tags = tags;\n            break;\n          }\n        }\n        return json;\n      })\n      .then((json) => {\n        return fetch(archivePostUrl, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(json),\n        });\n      })\n      .then((response) => {\n        if (\n          !response.ok ||\n          (response.status !== 202 && response.status === 200)\n        ) {\n          throw new Error('Failed to archive the trace');\n        }\n        if (archiveUrl) {\n          dispatch(\n            setAlert({\n              message: `Archive successful! This trace is now accessible at ${archiveUrl}`,\n              severity: 'success',\n            }),\n          );\n        } else {\n          dispatch(\n            setAlert({\n              message: `Archive successful!`,\n              severity: 'success',\n            }),\n          );\n        }\n      })\n      .catch(() => {\n        dispatch(\n          setAlert({\n            message: 'Failed to archive the trace',\n            severity: 'error',\n          }),\n        );\n      });\n  }, [archivePostUrl, archiveUrl, dispatch, trace.traceId]);\n\n  const handleDownloadJsonButtonClick = useCallback(() => {\n    const url = window.URL.createObjectURL(\n      new Blob([JSON.stringify(rawTrace)], { type: 'text/json' }),\n    );\n    const a = document.createElement('a');\n    a.download = `${trace.traceId}.json`;\n    a.href = url;\n    document.body.appendChild(a);\n    a.click();\n    a.parentNode?.removeChild(a);\n\n    handleMenuClose();\n  }, [rawTrace, trace.traceId]);\n\n  return (\n    <>\n      <Button\n        variant=\"outlined\"\n        className={classes.iconButton}\n        onClick={handleMenuButtonClick}\n      >\n        <MenuIcon fontSize=\"small\" />\n      </Button>\n      <Menu\n        anchorEl={anchorEl}\n        keepMounted\n        open={Boolean(anchorEl)}\n        onClose={handleMenuClose}\n      >\n        {traceJsonUrl && (\n          <MenuItem onClick={handleDownloadJsonButtonClick}>\n            <Trans t={t}>Download JSON</Trans>\n          </MenuItem>\n        )}\n        {logsUrl && (\n          <MenuItem\n            onClick={handleMenuClose}\n            component=\"a\"\n            href={logsUrl}\n            target=\"_blank\"\n            rel=\"noopener\"\n          >\n            <Trans t={t}>View Logs</Trans>\n          </MenuItem>\n        )}\n        {archivePostUrl && (\n          <MenuItem onClick={handleArchiveButtonClick}>\n            <Trans t={t}>Archive Trace</Trans>\n          </MenuItem>\n        )}\n      </Menu>\n    </>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Header/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { Header } from './Header';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/MiniTimeline/MiniTimeline.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, Button, makeStyles, useTheme } from '@material-ui/core';\nimport React, {\n  MouseEvent,\n  ReactNode,\n  useCallback,\n  useMemo,\n  useRef,\n} from 'react';\nimport { SpanRow } from '../types';\nimport { MiniTimelineOverlay } from './MiniTimelineOverlay';\nimport { TimeRangeSelector } from './TimeRangeSelector';\nimport { MiniTimelineRow } from './MiniTimelineRow';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    width: '100%',\n    height: 50,\n    border: `1px solid ${theme.palette.divider}`,\n    backgroundColor: theme.palette.background.paper,\n    position: 'relative',\n    // Show the reset button only when hovering.\n    '& > button': {\n      display: 'none',\n    },\n    '&:hover > button': {\n      display: 'inline',\n    },\n  },\n  svg: {\n    width: '100%',\n    height: '100%',\n  },\n  resetButton: {\n    position: 'absolute',\n    right: 10,\n    top: 6,\n    backgroundColor: theme.palette.common.white,\n    '&:hover': {\n      backgroundColor: theme.palette.grey[100],\n    },\n  },\n}));\n\nconst numOfTickMarkers = 3;\n\ntype MiniTimelineProps = {\n  spanRows: SpanRow[];\n  minTimestamp: number;\n  maxTimestamp: number;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  setSelectedMinTimestamp: (value: number) => void;\n  setSelectedMaxTimestamp: (value: number) => void;\n};\n\nexport const MiniTimeline = ({\n  spanRows,\n  minTimestamp,\n  maxTimestamp,\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n  setSelectedMinTimestamp,\n  setSelectedMaxTimestamp,\n}: MiniTimelineProps) => {\n  const classes = useStyles();\n  const theme = useTheme();\n  const rootEl = useRef<SVGSVGElement | null>(null);\n\n  const ticks = useMemo(() => {\n    const result: ReactNode[] = [];\n    for (let i = 1; i < numOfTickMarkers; i += 1) {\n      const x = `${(i / numOfTickMarkers) * 100}%`;\n      result.push(\n        <line\n          key={i}\n          x1={x}\n          y1=\"0\"\n          x2={x}\n          y2=\"100%\"\n          stroke={theme.palette.divider}\n          strokeWidth={1}\n        />,\n      );\n    }\n    return result;\n  }, [theme.palette.divider]);\n\n  const handleResetButtonClick = useCallback(\n    (e: MouseEvent<HTMLButtonElement>) => {\n      e.stopPropagation();\n      setSelectedMinTimestamp(minTimestamp);\n      setSelectedMaxTimestamp(maxTimestamp);\n    },\n    [\n      maxTimestamp,\n      minTimestamp,\n      setSelectedMaxTimestamp,\n      setSelectedMinTimestamp,\n    ],\n  );\n\n  return (\n    <Box className={classes.root}>\n      {(selectedMaxTimestamp !== maxTimestamp ||\n        selectedMinTimestamp !== minTimestamp) && (\n        <Button\n          variant=\"outlined\"\n          size=\"small\"\n          className={classes.resetButton}\n          onClick={handleResetButtonClick}\n        >\n          Reset\n        </Button>\n      )}\n      <svg className={classes.svg} ref={rootEl}>\n        <g>\n          {spanRows.map((spanRow, i) => (\n            <MiniTimelineRow\n              key={spanRow.spanId}\n              top={(100 / spanRows.length) * i}\n              spanRow={spanRow}\n              minTimestamp={minTimestamp}\n              maxTimestamp={maxTimestamp}\n            />\n          ))}\n        </g>\n        <g>{ticks}</g>\n        <MiniTimelineOverlay\n          minTimestamp={minTimestamp}\n          maxTimestamp={maxTimestamp}\n          setSelectedMinTimestamp={setSelectedMinTimestamp}\n          setSelectedMaxTimestamp={setSelectedMaxTimestamp}\n        />\n        <TimeRangeSelector\n          rootEl={rootEl}\n          minTimestamp={minTimestamp}\n          maxTimestamp={maxTimestamp}\n          selectedMinTimestamp={selectedMinTimestamp}\n          selectedMaxTimestamp={selectedMaxTimestamp}\n          setSelectedMinTimestamp={setSelectedMinTimestamp}\n          setSelectedMaxTimestamp={setSelectedMaxTimestamp}\n        />\n      </svg>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/MiniTimeline/MiniTimelineOverlay.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { useTheme } from '@material-ui/core';\nimport React, {\n  MouseEvent as ReactMouseEvent,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\n\nconst calculateX = (parentRect: DOMRect, x: number) => {\n  const value =\n    ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;\n  if (value <= 0) {\n    return 0;\n  }\n  if (value >= 100) {\n    return 100;\n  }\n  return value;\n};\n\ntype MiniTimelineOverlayProps = {\n  minTimestamp: number;\n  maxTimestamp: number;\n  setSelectedMinTimestamp: (value: number) => void;\n  setSelectedMaxTimestamp: (value: number) => void;\n};\n\nexport const MiniTimelineOverlay = ({\n  minTimestamp,\n  maxTimestamp,\n  setSelectedMinTimestamp,\n  setSelectedMaxTimestamp,\n}: MiniTimelineOverlayProps) => {\n  const theme = useTheme();\n  const rootEl = useRef<SVGRectElement | null>(null);\n  const [currentX, setCurrentX] = useState<number>();\n  const [mouseDownX, setMouseDownX] = useState<number>();\n  const [hoverX, setHoverX] = useState<number>();\n\n  const mouseDownXRef = useRef(mouseDownX);\n  useEffect(() => {\n    mouseDownXRef.current = mouseDownX;\n  }, [mouseDownX]);\n\n  const handleMouseMove = useCallback((e: MouseEvent) => {\n    if (!rootEl.current) {\n      return;\n    }\n    const x = calculateX(rootEl.current.getBoundingClientRect(), e.pageX);\n    setCurrentX(x);\n  }, []);\n\n  const handleMouseUp = useCallback(\n    (e: MouseEvent) => {\n      if (!rootEl.current || mouseDownXRef.current === undefined) {\n        return;\n      }\n      const x = calculateX(rootEl.current.getBoundingClientRect(), e.pageX);\n      // Adjust to avoid overlapping minTimestamp and maxTimestamp;\n      const adjustedX = Math.abs(x - mouseDownXRef.current) < 1 ? x + 1 : x;\n\n      const t1 =\n        (mouseDownXRef.current / 100) * (maxTimestamp - minTimestamp) +\n        minTimestamp;\n      const t2 =\n        (adjustedX / 100) * (maxTimestamp - minTimestamp) + minTimestamp;\n      const newMinTimestmap = Math.min(t1, t2);\n      const newMaxTimestamp = Math.max(t1, t2);\n      setSelectedMinTimestamp(newMinTimestmap);\n      setSelectedMaxTimestamp(newMaxTimestamp);\n\n      setCurrentX(undefined);\n      setMouseDownX(undefined);\n\n      window.removeEventListener('mousemove', handleMouseMove);\n      window.removeEventListener('mouseup', handleMouseUp);\n    },\n    [\n      maxTimestamp,\n      minTimestamp,\n      handleMouseMove,\n      setSelectedMaxTimestamp,\n      setSelectedMinTimestamp,\n    ],\n  );\n\n  const handleMouseDown = useCallback(\n    (e: ReactMouseEvent<SVGRectElement>) => {\n      if (!rootEl.current) {\n        return;\n      }\n      const x = calculateX(rootEl.current.getBoundingClientRect(), e.pageX);\n      setCurrentX(x);\n      setMouseDownX(x);\n\n      window.addEventListener('mousemove', handleMouseMove);\n      window.addEventListener('mouseup', handleMouseUp);\n    },\n    [handleMouseMove, handleMouseUp],\n  );\n\n  const handleMouseHoverMove = useCallback(\n    (e: ReactMouseEvent<SVGRectElement>) => {\n      if (e.buttons !== 0 || !rootEl.current) {\n        return;\n      }\n      const x = calculateX(rootEl.current.getBoundingClientRect(), e.pageX);\n      setHoverX(x);\n    },\n    [],\n  );\n\n  const handleMouseHoverLeave = useCallback(() => {\n    setHoverX(undefined);\n  }, []);\n\n  return (\n    <g>\n      {mouseDownX !== undefined && currentX !== undefined && (\n        <rect\n          x={`${Math.min(mouseDownX, currentX)}%`}\n          y=\"0\"\n          width={`${Math.abs(mouseDownX - currentX)}%`}\n          height=\"100%\"\n          fill={theme.palette.secondary.light}\n          fillOpacity=\"0.2\"\n          pointerEvents=\"none\"\n        />\n      )}\n      <rect\n        ref={rootEl}\n        x=\"0\"\n        y=\"0\"\n        width=\"100%\"\n        height=\"100%\"\n        onMouseDown={handleMouseDown}\n        onMouseMove={handleMouseHoverMove}\n        onMouseLeave={handleMouseHoverLeave}\n        fillOpacity=\"0\"\n        cursor=\"col-resize\"\n      />\n      {hoverX && (\n        <line\n          x1={`${hoverX}%`}\n          y1=\"0\"\n          x2={`${hoverX}%`}\n          y2=\"100%\"\n          stroke={theme.palette.secondary.main}\n          strokeWidth={1}\n          pointerEvents=\"none\"\n        />\n      )}\n    </g>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/MiniTimeline/MiniTimelineRow.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport React, { useMemo } from 'react';\nimport { selectServiceColor } from '../../../constants/color';\nimport { adjustPercentValue } from '../helpers';\nimport { SpanRow } from '../types';\n\ntype MiniTimelineRowProps = {\n  top: number;\n  spanRow: SpanRow;\n  minTimestamp: number;\n  maxTimestamp: number;\n};\n\nexport const MiniTimelineRow = ({\n  top,\n  spanRow,\n  minTimestamp,\n  maxTimestamp,\n}: MiniTimelineRowProps) => {\n  const left = useMemo(\n    () =>\n      adjustPercentValue(\n        spanRow.timestamp\n          ? ((spanRow.timestamp - minTimestamp) /\n              (maxTimestamp - minTimestamp)) *\n              100\n          : 0,\n      ),\n    [maxTimestamp, minTimestamp, spanRow.timestamp],\n  );\n\n  const width = useMemo(\n    () =>\n      adjustPercentValue(\n        left !== undefined && spanRow.duration && spanRow.timestamp\n          ? Math.max(\n              ((spanRow.timestamp + spanRow.duration - minTimestamp) /\n                (maxTimestamp - minTimestamp)) *\n                100 -\n                left,\n              0.1,\n            )\n          : 0.1,\n      ),\n    [left, maxTimestamp, minTimestamp, spanRow.duration, spanRow.timestamp],\n  );\n\n  return (\n    <rect\n      x={`${left}%`}\n      y={`${top}%`}\n      width={`${width}%`}\n      height=\"3\"\n      fill={selectServiceColor(spanRow.serviceName)}\n    />\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/MiniTimeline/TimeRangeSelector.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { useTheme } from '@material-ui/core';\nimport React, {\n  MouseEvent as ReactMouseEvent,\n  MutableRefObject,\n  useCallback,\n  useMemo,\n  useState,\n} from 'react';\nimport { adjustPercentValue } from '../helpers';\n\nconst calculateX = (\n  parentRect: DOMRect,\n  x: number,\n  opositeX: number,\n  isSmallerThanOpositeX: boolean,\n) => {\n  let value =\n    ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;\n  if (isSmallerThanOpositeX) {\n    if (value >= opositeX) {\n      value = opositeX - 1;\n    }\n  } else if (value <= opositeX) {\n    value = opositeX + 1;\n  }\n  return adjustPercentValue(value);\n};\n\nconst useRangeHandler = (\n  rootEl: MutableRefObject<SVGSVGElement | null>,\n  minTimestamp: number,\n  maxTimestamp: number,\n  opositeX: number,\n  isSmallerThanOpositeX: boolean,\n  setTimestamp: (value: number) => void,\n) => {\n  const [currentX, setCurrentX] = useState<number>();\n  const [mouseDownX, setMouseDownX] = useState<number>();\n  const [isDragging, setIsDragging] = useState(false);\n\n  const onMouseMove = useCallback(\n    (e: MouseEvent) => {\n      if (!rootEl.current) {\n        return;\n      }\n      const x = calculateX(\n        rootEl.current.getBoundingClientRect(),\n        e.pageX,\n        opositeX,\n        isSmallerThanOpositeX,\n      );\n      setCurrentX(x);\n    },\n    [isSmallerThanOpositeX, opositeX, rootEl],\n  );\n\n  const onMouseUp = useCallback(\n    (e: MouseEvent) => {\n      if (!rootEl.current) {\n        return;\n      }\n      const x = calculateX(\n        rootEl.current.getBoundingClientRect(),\n        e.pageX,\n        opositeX,\n        isSmallerThanOpositeX,\n      );\n      setTimestamp((x / 100) * (maxTimestamp - minTimestamp) + minTimestamp);\n      setCurrentX(undefined);\n      setMouseDownX(undefined);\n      setIsDragging(false);\n\n      window.removeEventListener('mousemove', onMouseMove);\n      window.removeEventListener('mouseup', onMouseUp);\n    },\n    [\n      isSmallerThanOpositeX,\n      maxTimestamp,\n      minTimestamp,\n      onMouseMove,\n      opositeX,\n      rootEl,\n      setTimestamp,\n    ],\n  );\n\n  const onMouseDown = useCallback(\n    (e: ReactMouseEvent<SVGRectElement>) => {\n      if (!rootEl.current) {\n        return;\n      }\n      const x = calculateX(\n        rootEl.current.getBoundingClientRect(),\n        e.currentTarget.getBoundingClientRect().x + 3,\n        opositeX,\n        isSmallerThanOpositeX,\n      );\n      setCurrentX(x);\n      setMouseDownX(x);\n      setIsDragging(true);\n\n      window.addEventListener('mousemove', onMouseMove);\n      window.addEventListener('mouseup', onMouseUp);\n    },\n    [isSmallerThanOpositeX, onMouseMove, onMouseUp, opositeX, rootEl],\n  );\n\n  return { currentX, mouseDownX, onMouseDown, isDragging };\n};\n\ntype TimeRangeSelectorProps = {\n  rootEl: MutableRefObject<SVGSVGElement | null>;\n  minTimestamp: number;\n  maxTimestamp: number;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  setSelectedMinTimestamp: (value: number) => void;\n  setSelectedMaxTimestamp: (value: number) => void;\n};\n\nexport const TimeRangeSelector = ({\n  rootEl,\n  minTimestamp,\n  maxTimestamp,\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n  setSelectedMinTimestamp,\n  setSelectedMaxTimestamp,\n}: TimeRangeSelectorProps) => {\n  const theme = useTheme();\n\n  const minRangeHandler = useRangeHandler(\n    rootEl,\n    minTimestamp,\n    maxTimestamp,\n    ((selectedMaxTimestamp - minTimestamp) / (maxTimestamp - minTimestamp)) *\n      100,\n    true,\n    setSelectedMinTimestamp,\n  );\n\n  const maxRangeHandler = useRangeHandler(\n    rootEl,\n    minTimestamp,\n    maxTimestamp,\n    ((selectedMinTimestamp - minTimestamp) / (maxTimestamp - minTimestamp)) *\n      100,\n    false,\n    setSelectedMaxTimestamp,\n  );\n\n  const rightOnTheLeft = useMemo(\n    () =>\n      adjustPercentValue(\n        ((selectedMinTimestamp - minTimestamp) /\n          (maxTimestamp - minTimestamp)) *\n          100,\n      ),\n    [maxTimestamp, minTimestamp, selectedMinTimestamp],\n  );\n\n  const leftOnTheRight = useMemo(\n    () =>\n      adjustPercentValue(\n        ((selectedMaxTimestamp - minTimestamp) /\n          (maxTimestamp - minTimestamp)) *\n          100,\n      ),\n    [maxTimestamp, minTimestamp, selectedMaxTimestamp],\n  );\n\n  return (\n    <g>\n      <rect\n        x=\"0\"\n        y=\"0\"\n        width={`${rightOnTheLeft}%`}\n        height=\"100%\"\n        fill={theme.palette.grey[500]}\n        fillOpacity=\"0.2\"\n        pointerEvents=\"none\"\n      />\n      <rect\n        x={`${leftOnTheRight}%`}\n        y=\"0\"\n        width={`${100 - leftOnTheRight}%`}\n        height=\"100%\"\n        fill={theme.palette.grey[500]}\n        fillOpacity=\"0.2\"\n        pointerEvents=\"none\"\n      />\n      <rect\n        x={`${rightOnTheLeft}%`}\n        y=\"0\"\n        width=\"2\"\n        height=\"100%\"\n        fill={theme.palette.divider}\n        transform=\"translate(-1)\"\n        pointerEvents=\"none\"\n      />\n      <rect\n        x={`${leftOnTheRight}%`}\n        y=\"0\"\n        width=\"2\"\n        height=\"100%\"\n        fill={theme.palette.divider}\n        transform=\"translate(-1)\"\n        pointerEvents=\"none\"\n      />\n      {minRangeHandler.mouseDownX !== undefined &&\n        minRangeHandler.currentX !== undefined && (\n          <rect\n            x={`${Math.min(\n              minRangeHandler.mouseDownX,\n              minRangeHandler.currentX,\n            )}%`}\n            y=\"0\"\n            width={`${Math.abs(\n              minRangeHandler.mouseDownX - minRangeHandler.currentX,\n            )}%`}\n            height=\"100%\"\n            fill={theme.palette.secondary.light}\n            fillOpacity=\"0.2\"\n            pointerEvents=\"none\"\n          />\n        )}\n      {maxRangeHandler.mouseDownX !== undefined &&\n        maxRangeHandler.currentX !== undefined && (\n          <rect\n            x={`${Math.min(\n              maxRangeHandler.mouseDownX,\n              maxRangeHandler.currentX,\n            )}%`}\n            y=\"0\"\n            width={`${Math.abs(\n              maxRangeHandler.mouseDownX - maxRangeHandler.currentX,\n            )}%`}\n            height=\"100%\"\n            fill={theme.palette.secondary.light}\n            fillOpacity=\"0.2\"\n            pointerEvents=\"none\"\n          />\n        )}\n      <rect\n        x={`${rightOnTheLeft}%`}\n        y=\"0\"\n        width=\"6\"\n        height=\"40%\"\n        fill={\n          minRangeHandler.isDragging\n            ? theme.palette.primary.dark\n            : theme.palette.primary.main\n        }\n        onMouseDown={minRangeHandler.onMouseDown}\n        cursor=\"pointer\"\n        transform=\"translate(-3)\"\n      />\n      <rect\n        x={`${leftOnTheRight}%`}\n        y=\"0\"\n        width=\"6\"\n        height=\"40%\"\n        fill={\n          maxRangeHandler.isDragging\n            ? theme.palette.primary.dark\n            : theme.palette.primary.main\n        }\n        onMouseDown={maxRangeHandler.onMouseDown}\n        cursor=\"pointer\"\n        transform=\"translate(-3)\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/MiniTimeline/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { MiniTimeline } from './MiniTimeline';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanDetailDrawer/AnnotationViewer.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Box,\n  Collapse,\n  IconButton,\n  makeStyles,\n  Theme,\n  Typography,\n} from '@material-ui/core';\nimport {\n  KeyboardArrowDown as KeyboardArrowDownIcon,\n  KeyboardArrowUp as KeyboardArrowUpIcon,\n} from '@material-ui/icons';\nimport React from 'react';\nimport { useToggle } from 'react-use';\nimport { selectServiceColor } from '../../../constants/color';\nimport {\n  AdjustedAnnotation,\n  AdjustedSpan,\n} from '../../../models/AdjustedTrace';\nimport { AnnotationTable } from '../AnnotationTable';\nimport { AnnotationTooltip } from '../AnnotationTooltip';\nimport { TickMarkers } from '../TickMarkers';\n\nconst calculateMarkerLeftPosition = (\n  annotation: AdjustedAnnotation,\n  span: AdjustedSpan,\n) => {\n  const p = ((annotation.timestamp - span.timestamp) / span.duration) * 100;\n  if (p >= 100) {\n    return 'calc(100% - 1px)';\n  }\n  return `${p}%`;\n};\n\nconst useStyles = makeStyles<Theme, { serviceName: string }>((theme) => ({\n  bar: {\n    width: '100%',\n    height: 10,\n    backgroundColor: ({ serviceName }) => selectServiceColor(serviceName),\n    position: 'relative',\n  },\n  annotationMarker: {\n    position: 'absolute',\n    backgroundColor: theme.palette.common.black,\n    height: 18,\n    width: 1,\n    top: -4,\n    cursor: 'pointer',\n  },\n}));\n\ntype AnnotationViewerProps = {\n  minTimestamp: number;\n  span: AdjustedSpan;\n};\n\nexport const AnnotationViewer = ({\n  minTimestamp,\n  span,\n}: AnnotationViewerProps) => {\n  const classes = useStyles({ serviceName: span.serviceName });\n  const [open, toggleOpen] = useToggle(true);\n\n  return (\n    <Box>\n      <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\">\n        <Typography>Annotation</Typography>\n        <IconButton onClick={toggleOpen} size=\"small\">\n          {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}\n        </IconButton>\n      </Box>\n      <Collapse in={open}>\n        {span.timestamp && span.duration ? (\n          <Box mt={1.5} mb={1.5}>\n            <TickMarkers\n              minTimestamp={span.timestamp - minTimestamp}\n              maxTimestamp={span.timestamp + span.duration - minTimestamp}\n            />\n            <Box className={classes.bar}>\n              {span.annotations\n                .filter(\n                  (annotation) =>\n                    annotation.timestamp &&\n                    annotation.timestamp >= span.timestamp &&\n                    annotation.timestamp <= span.timestamp + span.duration,\n                )\n                .map((annotation) => (\n                  <AnnotationTooltip\n                    key={`${annotation.value}-${annotation.timestamp}`}\n                    annotation={annotation}\n                  >\n                    <Box\n                      key={`${annotation.value}-${annotation.timestamp}`}\n                      className={classes.annotationMarker}\n                      style={{\n                        left: calculateMarkerLeftPosition(annotation, span),\n                      }}\n                    />\n                  </AnnotationTooltip>\n                ))}\n            </Box>\n          </Box>\n        ) : null}\n        <AnnotationTable annotations={span.annotations} />\n      </Collapse>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanDetailDrawer/SpanDetailDrawer.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, Divider, Grid, makeStyles, Typography } from '@material-ui/core';\nimport React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\nimport { AnnotationViewer } from './AnnotationViewer';\nimport { TagList } from './TagList';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    padding: theme.spacing(2),\n    backgroundColor: theme.palette.background.paper,\n    borderLeft: `1px solid ${theme.palette.divider}`,\n    minHeight: '100%',\n  },\n  basicInfoLabel: {\n    lineHeight: 1.2,\n  },\n  basicInfoValue: {\n    wordWrap: 'break-word',\n  },\n  divider: {\n    marginTop: theme.spacing(1.5),\n    marginBottom: theme.spacing(2.5),\n  },\n}));\n\ntype SpanDetailDrawerProps = {\n  span: AdjustedSpan;\n  minTimestamp: number;\n};\n\nexport const SpanDetailDrawer = ({\n  span,\n  minTimestamp,\n}: SpanDetailDrawerProps) => {\n  const classes = useStyles();\n  const { t } = useTranslation();\n\n  return (\n    <Box className={classes.root}>\n      <Grid container spacing={1}>\n        {[\n          { label: 'Service name', value: span.serviceName },\n          { label: 'Span name', value: span.spanName },\n          { label: t(`Span ID`), value: span.spanId },\n          { label: t(`Parent ID`), value: span.parentId || 'none' },\n        ].map(({ label, value }) => (\n          <Grid key={label} item xs={6}>\n            <Typography\n              variant=\"caption\"\n              color=\"textSecondary\"\n              className={classes.basicInfoLabel}\n            >\n              {label}\n            </Typography>\n            <Typography variant=\"body1\" className={classes.basicInfoValue}>\n              {value}\n            </Typography>\n          </Grid>\n        ))}\n      </Grid>\n      {span.annotations.length > 0 && (\n        <>\n          <Divider className={classes.divider} />\n          <AnnotationViewer minTimestamp={minTimestamp} span={span} />\n        </>\n      )}\n      {span.tags.length > 0 && (\n        <>\n          <Divider className={classes.divider} />\n          <TagList span={span} />\n        </>\n      )}\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanDetailDrawer/TagList.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Box,\n  Collapse,\n  IconButton,\n  makeStyles,\n  Table,\n  TableBody,\n  TableCell,\n  TableRow,\n  Typography,\n} from '@material-ui/core';\nimport {\n  KeyboardArrowDown as KeyboardArrowDownIcon,\n  KeyboardArrowUp as KeyboardArrowUpIcon,\n} from '@material-ui/icons';\nimport React from 'react';\nimport { useToggle } from 'react-use';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\n\nconst useStyles = makeStyles((theme) => ({\n  table: {\n    tableLayout: 'fixed',\n  },\n  tableRow: {\n    '&:first-child > *': {\n      borderTop: `1px solid ${theme.palette.divider}`,\n    },\n  },\n  labelCell: {\n    color: theme.palette.text.secondary,\n    wordWrap: 'break-word',\n  },\n  valueCell: {\n    wordWrap: 'break-word',\n  },\n}));\n\ntype TagListProps = {\n  span: AdjustedSpan;\n};\n\nexport const TagList = ({ span }: TagListProps) => {\n  const classes = useStyles();\n  const [open, toggleOpen] = useToggle(true);\n\n  return (\n    <Box>\n      <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\">\n        <Typography>Tags</Typography>\n        <IconButton onClick={toggleOpen} size=\"small\">\n          {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}\n        </IconButton>\n      </Box>\n      <Collapse in={open}>\n        <Box mt={1.5}>\n          <Table size=\"small\" className={classes.table}>\n            <TableBody>\n              {span.tags.map((tag) => (\n                <TableRow key={tag.key} className={classes.tableRow}>\n                  <TableCell className={classes.labelCell}>{tag.key}</TableCell>\n                  <TableCell className={classes.valueCell}>\n                    {tag.value}\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n        </Box>\n      </Collapse>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanDetailDrawer/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { SpanDetailDrawer } from './SpanDetailDrawer';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanTable/SpanTable.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Link, makeStyles } from '@material-ui/core';\nimport { DataGrid, GridColDef } from '@material-ui/data-grid';\nimport React, { useMemo } from 'react';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\nimport { formatDuration, formatTimestamp } from '../../../util/timestamp';\n\nconst useStyles = makeStyles((theme) => ({\n  dataGrid: {\n    border: 'none',\n    borderRadius: 0,\n    backgroundColor: theme.palette.background.paper,\n    borderBottom: `1px solid ${theme.palette.divider}`,\n  },\n}));\n\ntype SpanTableProps = {\n  spans: AdjustedSpan[];\n  setSelectedSpan: (span: AdjustedSpan) => void;\n  toggleIsSpanTableOpen: () => void;\n};\n\nexport const SpanTable = ({\n  spans,\n  setSelectedSpan,\n  toggleIsSpanTableOpen,\n}: SpanTableProps) => {\n  const classes = useStyles();\n\n  const COLUMN_DEFS = useMemo<GridColDef[]>(\n    () => [\n      {\n        field: 'spanId',\n        headerName: 'Span ID',\n        width: 200,\n        renderCell: (params) => {\n          const spanId = params.value!.toString();\n          return (\n            <Link\n              component=\"button\"\n              onClick={() => {\n                setSelectedSpan(params.row as unknown as AdjustedSpan);\n                toggleIsSpanTableOpen();\n              }}\n            >\n              {spanId}\n            </Link>\n          );\n        },\n      },\n      { field: 'serviceName', headerName: 'Service name', width: 200 },\n      { field: 'spanName', headerName: 'Span name', width: 200 },\n      {\n        field: 'timestamp',\n        headerName: 'Start time',\n        valueGetter: (params) =>\n          formatTimestamp(parseInt(params.value!.toString(), 10)),\n        width: 200,\n      },\n      {\n        field: 'duration',\n        headerName: 'Duration',\n        valueGetter: (params) =>\n          formatDuration(parseInt(params.value!.toString(), 10)),\n        width: 150,\n      },\n    ],\n    [setSelectedSpan, toggleIsSpanTableOpen],\n  );\n\n  return (\n    <DataGrid\n      className={classes.dataGrid}\n      density=\"compact\"\n      rows={spans}\n      columns={COLUMN_DEFS}\n      getRowId={(span) => span.spanId}\n      hideFooter\n      pageSize={spans.length}\n      disableColumnFilter\n    />\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/SpanTable/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { SpanTable } from './SpanTable';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/TickMarkers/TickMarkers.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, makeStyles } from '@material-ui/core';\nimport React, { ReactNode, useMemo } from 'react';\nimport { formatDuration } from '../../../util/timestamp';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    height: 16,\n    position: 'relative',\n    borderRight: `1px solid ${theme.palette.divider}`,\n  },\n  tick: {\n    height: 16,\n    position: 'absolute',\n    display: 'flex',\n    alignItems: 'center',\n    fontSize: theme.typography.caption.fontSize,\n    color: theme.palette.text.secondary,\n    bottom: 0,\n    paddingLeft: theme.spacing(0.5),\n    paddingRight: theme.spacing(0.5),\n    '&:not(:last-child)': {\n      borderLeft: `1px solid ${theme.palette.divider}`,\n    },\n  },\n}));\n\nconst numOfTickMarkers = 3;\n\ntype TickMarkersProps = {\n  minTimestamp: number;\n  maxTimestamp: number;\n};\n\nexport const TickMarkers = ({\n  minTimestamp,\n  maxTimestamp,\n}: TickMarkersProps) => {\n  const classes = useStyles();\n\n  const ticks = useMemo(() => {\n    const result: ReactNode[] = [];\n    for (let i = 0; i <= numOfTickMarkers; i += 1) {\n      const timestamp = formatDuration(\n        ((maxTimestamp - minTimestamp) / numOfTickMarkers) * i + minTimestamp,\n      );\n      let left: string | undefined;\n      let right: string | undefined;\n      if (i === numOfTickMarkers) {\n        right = '0%';\n      } else {\n        left = `${(i / numOfTickMarkers) * 100}%`;\n      }\n\n      result.push(\n        <Box\n          key={i}\n          component=\"span\"\n          className={classes.tick}\n          style={{ left, right }}\n        >\n          {timestamp}\n        </Box>,\n      );\n    }\n    return result;\n  }, [classes.tick, maxTimestamp, minTimestamp]);\n\n  return <Box className={classes.root}>{ticks}</Box>;\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/TickMarkers/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { TickMarkers } from './TickMarkers';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/Timeline.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, makeStyles } from '@material-ui/core';\nimport React, { useCallback, useEffect, useRef } from 'react';\nimport AutoSizer, { Size } from 'react-virtualized-auto-sizer';\nimport { FixedSizeList as List, ListChildComponentProps } from 'react-window';\nimport { useMeasure } from 'react-use';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\nimport { MiniTimeline } from '../MiniTimeline';\nimport { TickMarkers } from '../TickMarkers';\nimport { SpanRow } from '../types';\nimport { TimelineHeader } from './TimelineHeader';\nimport { TimelineRow } from './TimelineRow';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    height: '100%',\n    display: 'flex',\n    flexDirection: 'column',\n    backgroundColor: theme.palette.background.paper,\n  },\n  miniViewerContainer: {\n    flex: '0 0',\n    padding: theme.spacing(1),\n    backgroundColor: theme.palette.grey[50],\n    borderBottom: `1px solid ${theme.palette.divider}`,\n  },\n}));\n\ntype TimelineProps = {\n  spanRows: SpanRow[];\n  selectedSpan: AdjustedSpan;\n  setSelectedSpan: (span: AdjustedSpan) => void;\n  minTimestamp: number;\n  maxTimestamp: number;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  setSelectedMinTimestamp: (value: number) => void;\n  setSelectedMaxTimestamp: (value: number) => void;\n  isSpanDetailDrawerOpen: boolean;\n  toggleIsSpanDetailDrawerOpen: () => void;\n  isMiniTimelineOpen: boolean;\n  toggleIsMiniTimelineOpen: () => void;\n  rerootedSpanId: string | undefined;\n  setRerootedSpanId: (value: string | undefined) => void;\n  toggleOpenSpan: (spanId: string) => void;\n  setClosedSpanIdMap: (value: { [spanId: string]: boolean }) => void;\n};\n\nconst rowHeight = 30;\n\nexport const Timeline = ({\n  spanRows,\n  selectedSpan,\n  setSelectedSpan,\n  minTimestamp,\n  maxTimestamp,\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n  setSelectedMinTimestamp,\n  setSelectedMaxTimestamp,\n  isSpanDetailDrawerOpen,\n  toggleIsSpanDetailDrawerOpen,\n  isMiniTimelineOpen,\n  toggleIsMiniTimelineOpen,\n  rerootedSpanId,\n  setRerootedSpanId,\n  toggleOpenSpan,\n  setClosedSpanIdMap,\n}: TimelineProps) => {\n  const classes = useStyles();\n\n  const listRef = useRef<any>();\n  const [listInnerRef, listInnerMeasure] = useMeasure();\n\n  const prevSelectedSpanId = useRef(selectedSpan.spanId);\n  useEffect(() => {\n    if (selectedSpan.spanId !== prevSelectedSpanId.current) {\n      listRef.current.scrollToItem(\n        spanRows.findIndex((r) => r.spanId === selectedSpan.spanId),\n        'center',\n      );\n    }\n  }, [selectedSpan.spanId, spanRows]);\n\n  const rowRenderer = useCallback(\n    (props: ListChildComponentProps) => {\n      const spanRow = spanRows[props.index];\n\n      return (\n        <div style={props.style}>\n          <TimelineRow\n            {...spanRow}\n            setSelectedSpan={setSelectedSpan}\n            isSelected={selectedSpan.spanId === spanRow.spanId}\n            selectedMinTimestamp={selectedMinTimestamp}\n            selectedMaxTimestamp={selectedMaxTimestamp}\n            toggleOpenSpan={toggleOpenSpan}\n            rowHeight={rowHeight}\n          />\n        </div>\n      );\n    },\n    [\n      selectedMaxTimestamp,\n      selectedMinTimestamp,\n      selectedSpan.spanId,\n      setSelectedSpan,\n      spanRows,\n      toggleOpenSpan,\n    ],\n  );\n\n  return (\n    <Box className={classes.root}>\n      {isMiniTimelineOpen && (\n        <Box className={classes.miniViewerContainer}>\n          <TickMarkers\n            minTimestamp={0}\n            maxTimestamp={maxTimestamp - minTimestamp}\n          />\n          <MiniTimeline\n            spanRows={spanRows}\n            minTimestamp={minTimestamp}\n            maxTimestamp={maxTimestamp}\n            selectedMinTimestamp={selectedMinTimestamp}\n            selectedMaxTimestamp={selectedMaxTimestamp}\n            setSelectedMinTimestamp={setSelectedMinTimestamp}\n            setSelectedMaxTimestamp={setSelectedMaxTimestamp}\n          />\n        </Box>\n      )}\n      <Box flex=\"0 0\">\n        <TimelineHeader\n          spanRows={spanRows}\n          minTimestamp={minTimestamp}\n          selectedMinTimestamp={selectedMinTimestamp}\n          selectedMaxTimestamp={selectedMaxTimestamp}\n          isSpanDetailDrawerOpen={isSpanDetailDrawerOpen}\n          toggleIsSpanDetailDrawerOpen={toggleIsSpanDetailDrawerOpen}\n          isMiniTimelineOpen={isMiniTimelineOpen}\n          toggleIsMiniTimelineOpen={toggleIsMiniTimelineOpen}\n          selectedSpan={selectedSpan}\n          rerootedSpanId={rerootedSpanId}\n          setRerootedSpanId={setRerootedSpanId}\n          absoluteListWidth={listInnerMeasure.width}\n          setClosedSpanIdMap={setClosedSpanIdMap}\n        />\n      </Box>\n      <Box flex=\"1 1\">\n        <AutoSizer>\n          {(args: Size) => (\n            <List\n              ref={listRef}\n              width={args.width}\n              height={args.height}\n              itemSize={rowHeight}\n              itemCount={spanRows.length}\n              innerRef={listInnerRef}\n            >\n              {rowRenderer}\n            </List>\n          )}\n        </AutoSizer>\n      </Box>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/TimelineHeader.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  Box,\n  Button,\n  ButtonGroup,\n  makeStyles,\n  Tooltip,\n} from '@material-ui/core';\nimport {\n  FilterCenterFocus as FilterCenterFocusIcon,\n  KeyboardArrowDown as KeyboardArrowDownIcon,\n  KeyboardArrowLeft as KeyboardArrowLeftIcon,\n  KeyboardArrowRight as KeyboardArrowRightIcon,\n  KeyboardArrowUp as KeyboardArrowUpIcon,\n  Visibility as VisibilityIcon,\n} from '@material-ui/icons';\nimport { ToggleButton } from '@material-ui/lab';\nimport React, { useCallback } from 'react';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\nimport { TickMarkers } from '../TickMarkers';\nimport { SpanRow } from '../types';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    position: 'relative',\n    backgroundColor: theme.palette.grey[100],\n    borderBottom: `1px solid ${theme.palette.divider}`,\n  },\n  rightButtonsWrapper: {\n    '& > :not(:last-child)': {\n      marginRight: theme.spacing(1),\n    },\n  },\n  button: {\n    height: 28,\n    textTransform: 'none',\n  },\n  iconButton: {\n    minWidth: 0,\n    width: 28,\n    height: 28,\n  },\n  tickMarkersWrapper: {\n    position: 'absolute',\n    left: 120,\n    bottom: 0,\n    paddingRight: theme.spacing(1),\n  },\n}));\n\ntype TimelineHeaderProps = {\n  spanRows: SpanRow[];\n  minTimestamp: number;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  isSpanDetailDrawerOpen: boolean;\n  toggleIsSpanDetailDrawerOpen: () => void;\n  isMiniTimelineOpen: boolean;\n  toggleIsMiniTimelineOpen: () => void;\n  selectedSpan: AdjustedSpan;\n  rerootedSpanId: string | undefined;\n  setRerootedSpanId: (value: string | undefined) => void;\n  absoluteListWidth: number;\n  setClosedSpanIdMap: (value: { [spanId: string]: boolean }) => void;\n};\n\nexport const TimelineHeader = ({\n  spanRows,\n  minTimestamp,\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n  isSpanDetailDrawerOpen,\n  toggleIsSpanDetailDrawerOpen,\n  isMiniTimelineOpen,\n  toggleIsMiniTimelineOpen,\n  selectedSpan,\n  rerootedSpanId,\n  setRerootedSpanId,\n  absoluteListWidth,\n  setClosedSpanIdMap,\n}: TimelineHeaderProps) => {\n  const classes = useStyles();\n\n  const handleRerootButtonClick = useCallback(() => {\n    setRerootedSpanId(selectedSpan.spanId);\n  }, [selectedSpan.spanId, setRerootedSpanId]);\n\n  const handleResetRerootButtonClick = useCallback(() => {\n    setRerootedSpanId(undefined);\n  }, [setRerootedSpanId]);\n\n  const handleCollapseAllButtonClick = useCallback(() => {\n    setClosedSpanIdMap(\n      spanRows.reduce<{ [spanId: string]: boolean }>((acc, cur) => {\n        acc[cur.spanId] = true;\n        return acc;\n      }, {}),\n    );\n  }, [setClosedSpanIdMap, spanRows]);\n\n  const handleExpandAllButtonClick = useCallback(() => {\n    setClosedSpanIdMap({});\n  }, [setClosedSpanIdMap]);\n\n  return (\n    <Box className={classes.root}>\n      <Box\n        px={2}\n        pt={1}\n        pb={3}\n        position=\"relative\"\n        display=\"flex\"\n        justifyContent=\"space-between\"\n        alignItems=\"center\"\n      >\n        <ButtonGroup>\n          <Tooltip title=\"Collapse all\">\n            <Button\n              className={classes.iconButton}\n              onClick={handleCollapseAllButtonClick}\n            >\n              <KeyboardArrowUpIcon fontSize=\"small\" />\n            </Button>\n          </Tooltip>\n          <Tooltip title=\"Expand all\">\n            <Button\n              className={classes.iconButton}\n              onClick={handleExpandAllButtonClick}\n            >\n              <KeyboardArrowDownIcon fontSize=\"small\" />\n            </Button>\n          </Tooltip>\n        </ButtonGroup>\n        <Box className={classes.rightButtonsWrapper}>\n          <Button\n            variant=\"outlined\"\n            className={classes.button}\n            onClick={handleRerootButtonClick}\n            startIcon={<FilterCenterFocusIcon fontSize=\"small\" />}\n            disabled={\n              spanRows.length > 0 && spanRows[0].spanId === selectedSpan.spanId\n            }\n          >\n            Focus on selected span\n          </Button>\n          <Button\n            variant=\"outlined\"\n            className={classes.button}\n            disabled={!rerootedSpanId}\n            onClick={handleResetRerootButtonClick}\n          >\n            Reset focus\n          </Button>\n          <Tooltip\n            title={\n              isMiniTimelineOpen ? 'Close mini timeline' : 'Open mini timeline'\n            }\n          >\n            <ToggleButton\n              value=\"openMiniTimeline\"\n              className={classes.iconButton}\n              selected={isMiniTimelineOpen}\n              onClick={toggleIsMiniTimelineOpen}\n            >\n              <VisibilityIcon fontSize=\"small\" />\n            </ToggleButton>\n          </Tooltip>\n          <Tooltip\n            title={\n              isSpanDetailDrawerOpen ? 'Close span detail' : 'Open span detail'\n            }\n          >\n            <ToggleButton\n              value=\"openSpanDetailDrawer\"\n              className={classes.iconButton}\n              selected={isSpanDetailDrawerOpen}\n              onClick={toggleIsSpanDetailDrawerOpen}\n            >\n              {isSpanDetailDrawerOpen ? (\n                <KeyboardArrowRightIcon fontSize=\"small\" />\n              ) : (\n                <KeyboardArrowLeftIcon fontSize=\"small\" />\n              )}\n            </ToggleButton>\n          </Tooltip>\n        </Box>\n      </Box>\n      <Box\n        className={classes.tickMarkersWrapper}\n        right={`calc(100% - ${absoluteListWidth}px)`}\n      >\n        <TickMarkers\n          minTimestamp={selectedMinTimestamp - minTimestamp}\n          maxTimestamp={selectedMaxTimestamp - minTimestamp}\n        />\n      </Box>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/TimelineRow.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, makeStyles } from '@material-ui/core';\nimport { ErrorOutline as ErrorOutlineIcon } from '@material-ui/icons';\nimport classNames from 'classnames';\nimport React, { MouseEvent, useCallback } from 'react';\nimport { AdjustedSpan } from '../../../models/AdjustedTrace';\nimport { SpanRow } from '../types';\nimport { TimelineRowBar } from './TimelineRowBar';\nimport { TimelineRowEdge } from './TimelineRowEdges';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    display: 'flex',\n    cursor: 'pointer',\n    '&:hover': {\n      backgroundColor: 'rgba(0, 0, 0, 0.1)',\n    },\n  },\n  rootSelected: {\n    backgroundColor: 'rgba(0, 0, 0, 0.1)',\n  },\n  text: {\n    fontSize: theme.typography.caption.fontSize,\n  },\n  errorIcon: {\n    marginRight: theme.spacing(0.5),\n    fontSize: '14px',\n  },\n}));\n\ntype TimelineRowProps = SpanRow & {\n  isSelected: boolean;\n  setSelectedSpan: (span: AdjustedSpan) => void;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  toggleOpenSpan: (spanId: string) => void;\n  rowHeight: number;\n};\n\nexport const TimelineRow = (props: TimelineRowProps) => {\n  const {\n    isSelected,\n    setSelectedSpan,\n    spanId,\n    serviceName,\n    spanName,\n    numOfChildren,\n    treeEdgeShape,\n    durationStr,\n    errorType,\n    isClosed,\n    isCollapsible,\n    selectedMinTimestamp,\n    selectedMaxTimestamp,\n    toggleOpenSpan,\n    rowHeight,\n  } = props;\n  const classes = useStyles();\n\n  const handleClick = useCallback(() => {\n    setSelectedSpan(props);\n  }, [props, setSelectedSpan]);\n\n  const handleButtonClick = useCallback(\n    (e: MouseEvent<HTMLButtonElement>) => {\n      e.stopPropagation();\n      toggleOpenSpan(spanId);\n    },\n    [spanId, toggleOpenSpan],\n  );\n\n  return (\n    <Box\n      className={classNames(classes.root, {\n        [classes.rootSelected]: isSelected,\n      })}\n      onClick={handleClick}\n    >\n      <TimelineRowEdge\n        numOfChildren={numOfChildren}\n        treeEdgeShape={treeEdgeShape}\n        isClosed={isClosed}\n        isCollapsible={isCollapsible}\n        rowHeight={rowHeight}\n        onButtonClick={handleButtonClick}\n      />\n      <Box position=\"relative\" width=\"100%\" flex=\"1 1\">\n        <Box pt={0.25} display=\"flex\" justifyContent=\"space-between\" pr={1}>\n          <Box display=\"flex\" alignItems=\"center\">\n            {errorType !== 'none' && (\n              <ErrorOutlineIcon className={classes.errorIcon} color=\"error\" />\n            )}\n            <Box className={classes.text}>{`${serviceName}: ${spanName}`}</Box>\n          </Box>\n          <Box className={classes.text}>{durationStr}</Box>\n        </Box>\n        <TimelineRowBar\n          spanRow={props}\n          rowHeight={rowHeight}\n          selectedMinTimestamp={selectedMinTimestamp}\n          selectedMaxTimestamp={selectedMaxTimestamp}\n        />\n      </Box>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/TimelineRowAnnotation.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, makeStyles } from '@material-ui/core';\nimport React, { useMemo } from 'react';\nimport { AdjustedAnnotation } from '../../../models/AdjustedTrace';\nimport { AnnotationTooltip } from '../AnnotationTooltip';\n\nconst useStyles = makeStyles((theme) => ({\n  annotationMarker: {\n    position: 'absolute',\n    backgroundColor: theme.palette.common.black,\n    height: 12,\n    width: 1,\n    top: -6,\n    cursor: 'pointer',\n    pointerEvents: 'auto',\n  },\n}));\n\ntype TimelineRowAnnotationProps = {\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n  annotation: AdjustedAnnotation;\n};\n\nexport const TimelineRowAnnotation = ({\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n  annotation,\n}: TimelineRowAnnotationProps) => {\n  const classes = useStyles();\n\n  const left = useMemo(() => {\n    if (\n      annotation.timestamp < selectedMinTimestamp ||\n      annotation.timestamp > selectedMaxTimestamp\n    ) {\n      return undefined;\n    }\n    const p =\n      ((annotation.timestamp - selectedMinTimestamp) /\n        (selectedMaxTimestamp - selectedMinTimestamp)) *\n      100;\n    if (p >= 100) {\n      return `calc(100% - 1px)`;\n    }\n    return `${p}%`;\n  }, [annotation.timestamp, selectedMaxTimestamp, selectedMinTimestamp]);\n\n  if (left === undefined) {\n    return null;\n  }\n\n  return (\n    <AnnotationTooltip annotation={annotation}>\n      <Box left={left} className={classes.annotationMarker} />\n    </AnnotationTooltip>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/TimelineRowBar.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, makeStyles, Theme } from '@material-ui/core';\nimport React, { useMemo } from 'react';\nimport { selectServiceColor } from '../../../constants/color';\nimport { SpanRow } from '../types';\nimport { TimelineRowAnnotation } from './TimelineRowAnnotation';\n\nconst useStyles = makeStyles<Theme, { rowHeight: number; serviceName: string }>(\n  (theme) => ({\n    root: {\n      left: 0,\n      right: theme.spacing(1),\n      top: 0,\n      bottom: 0,\n      position: 'absolute',\n      transform: ({ rowHeight }) => `translateY(${(rowHeight / 4) * 3}px)`,\n      pointerEvents: 'none',\n    },\n    line: {\n      position: 'absolute',\n      left: 0,\n      right: 0,\n      borderBottom: `1px solid ${theme.palette.divider}`,\n    },\n    bar: {\n      position: 'absolute',\n      height: 6,\n      transform: `translateY(-3px)`,\n      backgroundColor: ({ serviceName }) => selectServiceColor(serviceName),\n    },\n  }),\n);\n\ntype TimelineRowBarProps = {\n  spanRow: SpanRow;\n  rowHeight: number;\n  selectedMinTimestamp: number;\n  selectedMaxTimestamp: number;\n};\n\nexport const TimelineRowBar = ({\n  spanRow,\n  rowHeight,\n  selectedMinTimestamp,\n  selectedMaxTimestamp,\n}: TimelineRowBarProps) => {\n  const classes = useStyles({ rowHeight, serviceName: spanRow.serviceName });\n\n  const { left, width } = useMemo(() => {\n    const l = spanRow.timestamp\n      ? ((spanRow.timestamp - selectedMinTimestamp) /\n          (selectedMaxTimestamp - selectedMinTimestamp)) *\n        100\n      : 0;\n\n    const r =\n      spanRow.duration && spanRow.timestamp\n        ? ((spanRow.timestamp + spanRow.duration - selectedMinTimestamp) /\n            (selectedMaxTimestamp - selectedMinTimestamp)) *\n          100\n        : 0;\n\n    let rl: number | undefined;\n    let rw: number | undefined;\n    if (l <= 0 && r <= 0) {\n      rl = undefined;\n      rw = undefined;\n    } else if (l <= 0 && r > 0) {\n      rl = 0;\n      rw = r;\n    } else if (l >= 100) {\n      rl = undefined;\n      rw = undefined;\n    } else if (r >= 100) {\n      rl = l;\n      rw = 100 - rl;\n    } else {\n      rl = l;\n      rw = r - l;\n    }\n    return { left: rl, width: rw };\n  }, [\n    selectedMaxTimestamp,\n    selectedMinTimestamp,\n    spanRow.duration,\n    spanRow.timestamp,\n  ]);\n\n  return (\n    <Box className={classes.root}>\n      <Box className={classes.line} />\n      {left !== undefined && width !== undefined ? (\n        <Box className={classes.bar} left={`${left}%`} width={`${width}%`} />\n      ) : null}\n      {spanRow.annotations.map((annotation) => (\n        <TimelineRowAnnotation\n          key={`${annotation.value}-${annotation.timestamp}`}\n          selectedMinTimestamp={selectedMinTimestamp}\n          selectedMaxTimestamp={selectedMaxTimestamp}\n          annotation={annotation}\n        />\n      ))}\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/TimelineRowEdges.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, Button, makeStyles } from '@material-ui/core';\nimport { Add as AddIcon, Remove as RemoveIcon } from '@material-ui/icons';\nimport React, { memo, MouseEvent, ReactNode, useMemo } from 'react';\nimport { TreeEdgeShapeType } from '../types';\n\nconst useStyles = makeStyles((theme) => ({\n  root: {\n    flex: '0 0 120px',\n    paddingLeft: theme.spacing(2),\n    display: 'flex',\n    pointerEvents: 'none',\n    position: 'relative',\n  },\n  buttonWrapper: {\n    position: 'absolute',\n    left: theme.spacing(2),\n    right: 0,\n  },\n  button: {\n    fontSize: theme.typography.pxToRem(11),\n    position: 'absolute',\n    minWidth: 0,\n    lineHeight: 1.2,\n    padding: theme.spacing(0.25, 0.5),\n    pointerEvents: 'auto',\n    display: 'flex',\n    alignItems: 'center',\n  },\n  buttonIcon: {\n    marginLeft: theme.spacing(0.25),\n    fontSize: theme.typography.pxToRem(10),\n  },\n  horizontalAndVertical: {\n    borderTop: `1px solid ${theme.palette.divider}`,\n    borderLeft: `1px solid ${theme.palette.divider}`,\n  },\n  vertical: {\n    borderLeft: `1px solid ${theme.palette.divider}`,\n  },\n  horizontal: {\n    borderTop: `1px solid ${theme.palette.divider}`,\n  },\n}));\n\nconst buttonSizePx = 14;\n\ntype TimelineRowEdgeProps = {\n  numOfChildren: number;\n  treeEdgeShape: TreeEdgeShapeType[];\n  isClosed: boolean;\n  isCollapsible: boolean;\n  rowHeight: number;\n  onButtonClick: (e: MouseEvent<HTMLButtonElement>) => void;\n};\n\nconst TimelineRowEdgeImpl = ({\n  numOfChildren,\n  treeEdgeShape,\n  isClosed,\n  isCollapsible,\n  rowHeight,\n  onButtonClick,\n}: TimelineRowEdgeProps) => {\n  const classes = useStyles();\n\n  const button = useMemo(() => {\n    if (isCollapsible) {\n      for (let i = treeEdgeShape.length - 1; i >= 0; i -= 1) {\n        if (treeEdgeShape[i] !== '-') {\n          return (\n            <Button\n              color=\"primary\"\n              variant=\"contained\"\n              className={classes.button}\n              style={{\n                left: `${(100 / (treeEdgeShape.length + 1)) * i}%`,\n                top: `${(rowHeight / 4) * 3 - buttonSizePx / 2}px`,\n                transform: 'translate(-50%)',\n              }}\n              onClick={onButtonClick}\n            >\n              {numOfChildren}\n              {isClosed ? (\n                <AddIcon className={classes.buttonIcon} />\n              ) : (\n                <RemoveIcon className={classes.buttonIcon} />\n              )}\n            </Button>\n          );\n        }\n      }\n    }\n    return null;\n  }, [\n    classes,\n    isClosed,\n    isCollapsible,\n    numOfChildren,\n    onButtonClick,\n    rowHeight,\n    treeEdgeShape,\n  ]);\n\n  const content = useMemo(() => {\n    const commonProps = {\n      height: rowHeight,\n      width: `${100 / (treeEdgeShape.length + 1)}%`,\n      style: {\n        transform: `translateY(${(rowHeight / 4) * 3}px)`,\n      },\n    };\n\n    let branch = true;\n    const tree: ReactNode[] = [];\n\n    tree.push(<Box {...commonProps} className={classes.horizontal} key=\"-1\" />);\n    for (let i = treeEdgeShape.length - 1; i >= 0; i -= 1) {\n      const shape = treeEdgeShape[i];\n      const props = { ...commonProps, key: i };\n\n      switch (shape) {\n        case 'M':\n          if (branch) {\n            tree.push(\n              <Box {...props} className={classes.horizontalAndVertical} />,\n            );\n          } else {\n            tree.push(<Box {...props} className={classes.vertical} />);\n          }\n          branch = false;\n          break;\n        case 'E':\n          branch = false;\n          tree.push(<Box {...props} className={classes.horizontal} />);\n          break;\n        case 'B':\n          if (isClosed) {\n            tree.push(<Box {...props} className={classes.horizontal} />);\n          } else {\n            tree.push(\n              <Box {...props} className={classes.horizontalAndVertical} />,\n            );\n          }\n          break;\n        default:\n          if (branch) {\n            tree.push(<Box {...props} className={classes.horizontal} />);\n          } else {\n            tree.push(<Box {...props} />);\n          }\n      }\n    }\n    tree.reverse();\n\n    return tree;\n  }, [\n    classes.horizontal,\n    classes.horizontalAndVertical,\n    classes.vertical,\n    isClosed,\n    rowHeight,\n    treeEdgeShape,\n  ]);\n\n  return (\n    <Box className={classes.root}>\n      {content}\n      <Box className={classes.buttonWrapper}>{button}</Box>\n    </Box>\n  );\n};\n\nexport const TimelineRowEdge = memo(TimelineRowEdgeImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/Timeline/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { Timeline } from './Timeline';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/TracePage.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport PropTypes from 'prop-types';\nimport React, { useEffect, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { withRouter } from 'react-router-dom';\n\nimport { setAlert } from '../App/slice';\nimport { LoadingIndicator } from '../common/LoadingIndicator';\nimport { loadTrace } from '../../slices/tracesSlice';\nimport { TracePageContent } from './TracePageContent';\n\nconst propTypes = {\n  match: PropTypes.shape({\n    params: PropTypes.shape({\n      traceId: PropTypes.string.isRequired,\n    }),\n  }).isRequired,\n};\n\nexport const TracePageImpl = React.memo(({ match }) => {\n  const { traceId } = match.params;\n\n  const { isLoading, traceSummary, error, rawTrace } = useSelector((state) => ({\n    isLoading: state.traces.traces[traceId]?.isLoading || false,\n    traceSummary: state.traces.traces[traceId]?.adjustedTrace || undefined,\n    rawTrace: state.traces.traces[traceId]?.rawTrace || undefined,\n    error: state.traces.traces[traceId]?.error || undefined,\n  }));\n\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    dispatch(loadTrace(traceId));\n  }, [traceId, dispatch]);\n\n  const firstUpdate = useRef(true);\n  useEffect(() => {\n    // In the first rendering, skip this useEffect, because isLoading\n    // is always false and traceSummary always is undefined.\n    if (firstUpdate.current) {\n      firstUpdate.current = false;\n      return;\n    }\n    if (!isLoading && !traceSummary) {\n      let message = 'No trace found';\n      if (error && error.message) {\n        message += `: ${error.message}`;\n      }\n      dispatch(\n        setAlert({\n          message,\n          severity: 'error',\n        }),\n      );\n    }\n  }, [dispatch, error, isLoading, traceSummary]);\n\n  if (isLoading) {\n    return <LoadingIndicator />;\n  }\n\n  if (!traceSummary || !rawTrace) {\n    return null;\n  }\n  return <TracePageContent trace={traceSummary} rawTrace={rawTrace} />;\n});\n\nTracePageImpl.propTypes = propTypes;\n\nexport default withRouter(TracePageImpl);\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/TracePageContent.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, Drawer } from '@material-ui/core';\nimport React, { useCallback, useEffect, useMemo, useState } from 'react';\nimport { useToggle } from 'react-use';\nimport AdjustedTrace, { AdjustedSpan } from '../../models/AdjustedTrace';\nimport Span from '../../models/Span';\nimport { Header } from './Header/Header';\nimport {\n  convertSpansToSpanTree,\n  convertSpanTreeToSpanRowsAndTimestamps,\n} from './helpers';\nimport { SpanDetailDrawer } from './SpanDetailDrawer';\nimport { SpanTable } from './SpanTable';\nimport { Timeline } from './Timeline';\n\nconst SPAN_DETAIL_DRAWER_WIDTH = '480px';\n\ntype TracePageContentProps = {\n  trace: AdjustedTrace;\n  rawTrace: Span[];\n};\n\nexport const TracePageContent = ({\n  trace,\n  rawTrace,\n}: TracePageContentProps) => {\n  const [rerootedSpanId, setRerootedSpanId] = useState<string>();\n  const [closedSpanIdMap, setClosedSpanIdMap] = useState<{\n    [spanId: string]: boolean;\n  }>({});\n  const [isSpanDetailDrawerOpen, toggleIsSpanDetailDrawerOpen] =\n    useToggle(true);\n  const [isMiniTimelineOpen, toggleIsMiniTimelineOpen] = useToggle(true);\n  const [isSpanTableOpen, toggleIsSpanTableOpen] = useToggle(false);\n\n  const roots = useMemo(\n    () => convertSpansToSpanTree(trace.spans),\n    [trace.spans],\n  );\n\n  const { spanRows, minTimestamp, maxTimestamp } = useMemo(\n    () =>\n      convertSpanTreeToSpanRowsAndTimestamps(\n        roots,\n        closedSpanIdMap,\n        rerootedSpanId,\n      ),\n    [closedSpanIdMap, rerootedSpanId, roots],\n  );\n\n  const toggleOpenSpan = useCallback((spanId: string) => {\n    setClosedSpanIdMap((prev) => ({\n      ...prev,\n      [spanId]: !prev[spanId],\n    }));\n  }, []);\n\n  const [selectedSpan, setSelectedSpan] = useState<AdjustedSpan>(spanRows[0]);\n\n  const [selectedMinTimestamp, setSelectedMinTimestamp] =\n    useState(minTimestamp);\n  const [selectedMaxTimestamp, setSelectedMaxTimestamp] =\n    useState(maxTimestamp);\n  useEffect(() => {\n    setSelectedMinTimestamp(minTimestamp);\n    setSelectedMaxTimestamp(maxTimestamp);\n  }, [maxTimestamp, minTimestamp]);\n\n  return (\n    <Box display=\"flex\" flexDirection=\"column\" height=\"calc(100vh - 64px)\">\n      <Box flex=\"0 0\">\n        <Header\n          trace={trace}\n          rawTrace={rawTrace}\n          toggleIsSpanTableOpen={toggleIsSpanTableOpen}\n        />\n      </Box>\n      <Box flex=\"1 1\" display=\"flex\" overflow=\"hidden\">\n        <Box flex=\"1 1\">\n          <Timeline\n            spanRows={spanRows}\n            selectedSpan={selectedSpan}\n            setSelectedSpan={setSelectedSpan}\n            minTimestamp={minTimestamp}\n            maxTimestamp={maxTimestamp}\n            selectedMinTimestamp={selectedMinTimestamp}\n            selectedMaxTimestamp={selectedMaxTimestamp}\n            setSelectedMinTimestamp={setSelectedMinTimestamp}\n            setSelectedMaxTimestamp={setSelectedMaxTimestamp}\n            isSpanDetailDrawerOpen={isSpanDetailDrawerOpen}\n            toggleIsSpanDetailDrawerOpen={toggleIsSpanDetailDrawerOpen}\n            isMiniTimelineOpen={isMiniTimelineOpen}\n            toggleIsMiniTimelineOpen={toggleIsMiniTimelineOpen}\n            rerootedSpanId={rerootedSpanId}\n            setRerootedSpanId={setRerootedSpanId}\n            toggleOpenSpan={toggleOpenSpan}\n            setClosedSpanIdMap={setClosedSpanIdMap}\n          />\n        </Box>\n        {isSpanDetailDrawerOpen && (\n          <Box\n            flex={`0 0 ${SPAN_DETAIL_DRAWER_WIDTH}`}\n            height=\"100%\"\n            overflow=\"auto\"\n          >\n            {selectedSpan && (\n              <SpanDetailDrawer\n                minTimestamp={minTimestamp}\n                span={selectedSpan}\n              />\n            )}\n          </Box>\n        )}\n      </Box>\n      <Drawer\n        anchor=\"right\"\n        open={isSpanTableOpen}\n        onClose={toggleIsSpanTableOpen}\n      >\n        <Box width=\"70vw\" height=\"100vh\">\n          <SpanTable\n            spans={trace.spans}\n            setSelectedSpan={setSelectedSpan}\n            toggleIsSpanTableOpen={toggleIsSpanTableOpen}\n          />\n        </Box>\n      </Drawer>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/helpers.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n// Create tests for convertSpansToSpanTree\n// Define input data for a test that has two spans, parent and child, and parent span has\n// a parent span ID that is not in the list of spans\n// Define input data for a test that has two spans, parent and child, and parent span has\n// a parent span ID that is in the list of spans\n\nimport { convertSpansToSpanTree } from './helpers';\nimport { describe, it, expect } from 'vitest';\n\ndescribe('convertSpansToSpanTree', () => {\n  it('should return an empty array when there are no spans', () => {\n    const spans = [];\n    const result = convertSpansToSpanTree(spans);\n    expect(result).toEqual([]);\n  });\n\n  it('should return an array with correct root span', () => {\n    const spans = [\n      {\n        spanId: 'a',\n        spanName: 'root',\n        parentId: null,\n        childIds: ['b'],\n      },\n      {\n        spanId: 'b',\n\n        spanName: 'child',\n        parentId: 'a',\n        childIds: [],\n      },\n    ];\n\n    const result = convertSpansToSpanTree(spans);\n    expect(result.length).toEqual(1);\n    expect(result[0].spanId).toEqual('a');\n  });\n\n  // This test is consistent with 'should work when missing root span' in\n  // span-cleaner.test.js\n  it('should return an array with the root element which is missing parent', () => {\n    const spans = [\n      {\n        spanId: 'a',\n        spanName: 'missing parent',\n        parentId: 'm',\n        childIds: ['b'],\n      },\n      {\n        spanId: 'b',\n        spanName: 'child',\n        parentId: 'a',\n        childIds: [],\n      },\n    ];\n\n    const result = convertSpansToSpanTree(spans);\n    expect(result.length).toEqual(1);\n    expect(result[0].spanId).toEqual('a');\n  });\n\n  it('should return an array with two root elements which are missing parent', () => {\n    const spans = [\n      {\n        spanId: 'a',\n        spanName: 'missing parent',\n        parentId: 'm',\n        childIds: [],\n      },\n      {\n        spanId: 'b',\n        spanName: 'missing parent',\n        parentId: 'm',\n        childIds: [],\n      },\n    ];\n\n    const result = convertSpansToSpanTree(spans);\n    expect(result.length).toEqual(2);\n    expect(result[0].spanId).toEqual('a');\n    expect(result[1].spanId).toEqual('b');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/helpers.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { AdjustedSpan } from '../../models/AdjustedTrace';\nimport { SpanRow, TreeEdgeShapeType } from './types';\n\ntype SpanTreeNode = AdjustedSpan & {\n  children?: SpanTreeNode[];\n  maxDepth: number;\n};\n\nexport const convertSpansToSpanTree = (\n  spans: AdjustedSpan[],\n): SpanTreeNode[] => {\n  const idToSpan = spans.reduce<{ [id: string]: AdjustedSpan }>((acc, cur) => {\n    acc[cur.spanId] = cur;\n    return acc;\n  }, {});\n  const unconsumedSpans = { ...idToSpan };\n\n  const roots = spans.filter((span) => {\n    return !span.parentId || !spans.find((p) => p.spanId === span.parentId);\n  });\n  roots.forEach((root) => {\n    delete unconsumedSpans[root.spanId];\n  });\n\n  function fn(span: AdjustedSpan, depth: number): SpanTreeNode {\n    const childSpans = span.childIds\n      .map((childId) => unconsumedSpans[childId])\n      .filter((s) => !!s);\n    childSpans.forEach((child) => {\n      delete unconsumedSpans[child.spanId];\n    });\n    const children = childSpans.map((childSpan) => fn(childSpan, depth + 1));\n\n    return {\n      ...span,\n      children: children.length > 0 ? children : undefined,\n      depth,\n      maxDepth:\n        children.length > 0\n          ? Math.max(...children.map((child) => child.maxDepth)) + 1\n          : 0,\n    };\n  }\n\n  return roots.map((root) => fn(root, 0));\n};\n\nconst spanTreeToSpans = (roots: SpanTreeNode[]) => {\n  const spans: AdjustedSpan[] = [];\n\n  function fn(node: SpanTreeNode) {\n    spans.push(node);\n    node.children?.forEach(fn);\n  }\n  roots.forEach(fn);\n\n  return spans;\n};\n\nconst extractPartialTree = (roots: SpanTreeNode[], rerootedSpanId: string) => {\n  let isFinished = false;\n  let partialTree: SpanTreeNode[] = [];\n  let spans: AdjustedSpan[] = [];\n\n  function findRerootedSpan(node: SpanTreeNode) {\n    if (node.spanId === rerootedSpanId) {\n      isFinished = true;\n      partialTree = [node];\n      spans = spanTreeToSpans(partialTree);\n    } else {\n      node.children?.forEach(findRerootedSpan);\n    }\n  }\n\n  // eslint-disable-next-line no-restricted-syntax\n  for (const root of roots) {\n    findRerootedSpan(root);\n    if (isFinished) {\n      break;\n    }\n  }\n\n  return {\n    roots: partialTree,\n    spans,\n  };\n};\n\nexport const convertSpanTreeToSpanRowsAndTimestamps = (\n  roots: SpanTreeNode[],\n  closedSpanIdMap: { [spanId: string]: boolean },\n  rerootedSpanId?: string,\n) => {\n  // If rerootedSpanId is specified, calculate the partial tree.\n  let partialRoots: SpanTreeNode[];\n  if (rerootedSpanId) {\n    const result = extractPartialTree(roots, rerootedSpanId);\n    partialRoots = result.roots;\n  } else {\n    partialRoots = roots;\n  }\n\n  let minTimestamp = Number.MAX_SAFE_INTEGER;\n  let maxTimestamp = Number.MIN_SAFE_INTEGER;\n\n  const rows = partialRoots.flatMap((root) => {\n    const spanRows: SpanRow[] = [];\n\n    // This function creates and appends span rows recursively, and\n    // returns the number of nodes included.\n    function fn(\n      index: number,\n      siblings: SpanTreeNode[],\n      isParentClosed: boolean,\n      parentTreeEdgeShape?: TreeEdgeShapeType[],\n    ): number {\n      const node = siblings[index];\n      const isClosed = closedSpanIdMap[node.spanId] || false;\n      let treeEdgeShape: TreeEdgeShapeType[] = [];\n\n      if (node.timestamp !== undefined) {\n        minTimestamp = Math.min(minTimestamp, node.timestamp);\n      }\n      if (node.timestamp !== undefined && node.duration !== undefined) {\n        maxTimestamp = Math.max(maxTimestamp, node.timestamp + node.duration);\n      }\n\n      let spanRowIndex: number | undefined;\n      if (!isParentClosed) {\n        // If parentTreeEdgeShape is undefined, initialize treeEdgeShape with '-'.\n        if (!parentTreeEdgeShape) {\n          for (let i = 0; i < root.maxDepth; i += 1) {\n            treeEdgeShape[i] = '-';\n          }\n          // If the node has children, the first element will be 'B'.\n          if (node.children) {\n            treeEdgeShape[0] = 'B';\n          }\n        } else {\n          const relativeDepth = node.depth - root.depth;\n          treeEdgeShape = [...parentTreeEdgeShape];\n\n          if (\n            relativeDepth >= 2 &&\n            parentTreeEdgeShape[relativeDepth - 2] === 'E'\n          ) {\n            treeEdgeShape[relativeDepth - 2] = '-';\n          }\n\n          if (relativeDepth >= 1) {\n            if (index === siblings.length - 1) {\n              treeEdgeShape[relativeDepth - 1] = 'E';\n            } else {\n              treeEdgeShape[relativeDepth - 1] = 'M';\n            }\n          }\n          if (node.children) {\n            treeEdgeShape[relativeDepth] = 'B';\n          }\n        }\n        spanRowIndex =\n          spanRows.push({\n            ...node,\n            treeEdgeShape,\n            isClosed,\n            isCollapsible: !!node.children,\n            numOfChildren: 0,\n          }) - 1;\n      }\n\n      let numOfChildren = 0;\n      if (node.children) {\n        for (let i = 0; i < node.children.length; i += 1) {\n          numOfChildren += fn(\n            i,\n            node.children,\n            isClosed || isParentClosed,\n            treeEdgeShape,\n          );\n        }\n      }\n\n      if (spanRowIndex !== undefined) {\n        spanRows[spanRowIndex].numOfChildren = numOfChildren;\n      }\n\n      return numOfChildren + 1;\n    }\n    fn(0, [root], false, undefined);\n\n    return spanRows;\n  });\n\n  return {\n    minTimestamp,\n    maxTimestamp,\n    spanRows: rows,\n  };\n};\n\nexport const adjustPercentValue = (value: number) => {\n  if (value <= 0) {\n    return 0;\n  }\n  if (value >= 100) {\n    return 100;\n  }\n  return value;\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/index.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { default } from './TracePage';\n"
  },
  {
    "path": "zipkin-lens/src/components/TracePage/types.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { AdjustedSpan } from '../../models/AdjustedTrace';\n\n/*\n      0  1  2\n      v  v  v\n    0 +---------- SERVICE_A\n      |\n    1 |--+------- SERVICE_B\n      |  |\n    2 |  |--+---- SERVICE_C\n      |  |  |\n    3 |  |  |---- SERVICE_D\n      |  |\n    4 |  |--+---- SERVICE_E\n      |     |\n    5 |     |---- SERVICE_F\n      |\n    6 |---------- SERVICE_G \n\n  [\n    [B, -, -], // SERVICE_A\n    [M, B, -], // SERVICE_B\n    [M, M, B], // SERVICE_C\n    [M, M, E], // SERVICE_D\n    [M, E, B], // SERVICE_E\n    [M, -, E], // SERVICE_F\n    [E, -, -], // SERVICE_G\n  ]\n*/\nexport type TreeEdgeShapeType = 'B' | 'M' | 'E' | '-';\n\nexport type SpanRow = AdjustedSpan & {\n  treeEdgeShape: TreeEdgeShapeType[];\n  isClosed: boolean;\n  isCollapsible: boolean;\n  numOfChildren: number;\n};\n\nexport type ServiceTreeNode = {\n  serviceName: string;\n};\n\nexport type ServiceTreeEdge = {\n  spans: AdjustedSpan[];\n  sourceServiceName: string;\n  targetServiceName: string;\n  hasPair: boolean;\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/UiConfig/UiConfig.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport PropTypes from 'prop-types';\nimport React, { useContext } from 'react';\n\nimport { UI_CONFIG } from '../../constants/api';\nimport fetchResource from '../../util/fetch-resource';\n\nimport { defaultConfig } from './constants';\n\nconst ConfigContext = React.createContext();\n\nconst configResource = fetchResource(\n  fetch(UI_CONFIG).then((response) => response.json()),\n);\n\nconst propTypes = {\n  children: PropTypes.element.isRequired,\n};\n\nexport const UiConfig = ({ children }) => {\n  const response = configResource.read();\n  Object.keys(defaultConfig).forEach((key) => {\n    if (!response[key]) {\n      response[key] = defaultConfig[key];\n    }\n  });\n\n  return (\n    <ConfigContext.Provider value={response}>{children}</ConfigContext.Provider>\n  );\n};\n\nUiConfig.propTypes = propTypes;\n\nexport const UiConfigContext = ConfigContext;\nexport const UiConfigConsumer = ConfigContext.Consumer;\n\nexport const useUiConfig = () => useContext(UiConfigContext);\n"
  },
  {
    "path": "zipkin-lens/src/components/UiConfig/UiConfig.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { render, screen } from '@testing-library/react';\nimport { afterEach, it, describe, expect, vi } from 'vitest';\nimport React, { Suspense } from 'react';\n\nimport { UI_CONFIG } from '../../constants/api';\nimport { defaultConfig } from './constants';\n\nafterEach(() => {\n  vi.restoreAllMocks();\n  vi.resetModules();\n});\n\nconst renderUiConfig = async () => {\n  const { UiConfigConsumer, UiConfig } = await import('./UiConfig');\n  render(\n    <Suspense fallback=\"Suspended\">\n      <UiConfig>\n        <UiConfigConsumer>\n          {(value) => <div>{JSON.stringify(value)}</div>}\n        </UiConfigConsumer>\n      </UiConfig>\n    </Suspense>,\n  );\n};\n\ndescribe('<UiConfig />', () => {\n  it('fetches config, suspends until response, renders after response', async () => {\n    // Prepare the expected config object\n    const config = { ...defaultConfig, defaultLookback: 100 };\n    const configJson = JSON.stringify(config);\n\n    // Create a promise that can be resolved later\n    let resolve;\n    const configPromise = new Promise((r) => (resolve = r));\n    const fetchSpy = vi\n      .spyOn(global, 'fetch')\n      .mockImplementationOnce(() => configPromise);\n\n    // Render component and check suspension state\n    await renderUiConfig();\n    expect(screen.getByText('Suspended')).not.toBeNull();\n    expect(fetchSpy).toHaveBeenCalledWith(UI_CONFIG);\n\n    resolve(new Response(configJson));\n    // We need to get off the processing loop to allow the promise to complete and resolve the\n    // config.\n    await new Promise((res) => setTimeout(res, 10));\n\n    // Check that the expected JSON is rendered\n    expect(screen.getByText(configJson)).not.toBeNull();\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/components/UiConfig/constants.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport const defaultConfig = {\n  environment: '',\n  queryLimit: 10,\n  defaultLookback: 15 * 60 * 1000, // 15 minutes\n  searchEnabled: true,\n  dependency: {\n    lowErrorRate: 0.5, // 50% of calls in error turns line yellow\n    highErrorRate: 0.75, // 75% of calls in error turns line red\n    enabled: true,\n  },\n};\n"
  },
  {
    "path": "zipkin-lens/src/components/UiConfig/index.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport * from './UiConfig';\n"
  },
  {
    "path": "zipkin-lens/src/components/common/ExplainBox.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { IconDefinition } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { Box, Typography } from '@material-ui/core';\nimport React from 'react';\n\ninterface ExplainBoxProps {\n  icon: IconDefinition;\n  headerText: React.ReactNode;\n  text: React.ReactNode;\n}\n\nconst ExplainBox = React.memo<ExplainBoxProps>(({ icon, headerText, text }) => {\n  return (\n    <Box\n      height=\"100%\"\n      width=\"100%\"\n      display=\"flex\"\n      alignItems=\"center\"\n      justifyContent=\"center\"\n      flexDirection=\"column\"\n      color=\"text.secondary\"\n    >\n      <FontAwesomeIcon icon={icon} size=\"10x\" />\n      <Box mt={3} mb={2}>\n        <Typography variant=\"h4\">{headerText}</Typography>\n      </Box>\n      <Typography variant=\"body1\">{text}</Typography>\n    </Box>\n  );\n});\n\nexport default ExplainBox;\n"
  },
  {
    "path": "zipkin-lens/src/components/common/LoadingIndicator.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Box, CircularProgress } from '@material-ui/core';\nimport React from 'react';\n\nexport const LoadingIndicator = () => (\n  <Box\n    display=\"flex\"\n    justifyContent=\"center\"\n    mt={10}\n    data-testid=\"loading-indicator\"\n  >\n    <CircularProgress />\n  </Box>\n);\n"
  },
  {
    "path": "zipkin-lens/src/components/common/ServiceBadge.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport PropTypes from 'prop-types';\nimport React, { useMemo, useCallback } from 'react';\nimport { faTimes } from '@fortawesome/free-solid-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { makeStyles, ThemeProvider } from '@material-ui/styles';\nimport Box from '@material-ui/core/Box';\nimport Paper from '@material-ui/core/Paper';\n\nimport { selectServiceTheme } from '../../constants/color';\n\nconst useStyles = makeStyles((theme) => ({\n  paper: {\n    overflow: 'hidden',\n    display: 'flex',\n    flexShrink: 0,\n  },\n  buttonBase: {\n    height: '1.8rem',\n    paddingRight: '0.5rem',\n    paddingLeft: '0.5rem',\n    display: 'flex',\n    alignItems: 'center',\n    color: theme.palette.common.white,\n    backgroundColor: theme.palette.primary.dark,\n  },\n  clickableButton: {\n    cursor: 'pointer',\n    transition: theme.transitions.create(['background-color'], {\n      duration: theme.transitions.duration.short,\n    }),\n    '&:hover': {\n      backgroundColor: theme.palette.primary.main,\n    },\n  },\n}));\n\nconst propTypes = {\n  serviceName: PropTypes.string.isRequired,\n  count: PropTypes.number,\n  onClick: PropTypes.func,\n  onDelete: PropTypes.func,\n};\n\nconst defaultProps = {\n  count: null,\n  onClick: null,\n  onDelete: null,\n};\n\nconst ServiceBadgeImpl = ({ serviceName, count, onClick, onDelete }) => {\n  const classes = useStyles();\n\n  const label = useMemo(\n    () => `${serviceName}${count ? ` (${count})` : ''}`,\n    [count, serviceName],\n  );\n\n  const handleClick = useCallback(() => {\n    onClick(serviceName);\n  }, [onClick, serviceName]);\n\n  return (\n    <Paper className={classes.paper}>\n      <Box\n        className={`${classes.buttonBase} ${\n          onClick ? classes.clickableButton : ''\n        }`}\n        onClick={handleClick}\n        data-testid=\"badge\"\n      >\n        {label}\n      </Box>\n      {onDelete ? (\n        <Box\n          className={`${classes.buttonBase} ${classes.clickableButton}`}\n          onClick={onDelete}\n          data-testid=\"delete-button\"\n        >\n          <FontAwesomeIcon icon={faTimes} />\n        </Box>\n      ) : null}\n    </Paper>\n  );\n};\n\nServiceBadgeImpl.propTypes = propTypes;\nServiceBadgeImpl.defaultProps = defaultProps;\n\nconst ServiceBadge = ({ serviceName, ...props }) => {\n  const theme = selectServiceTheme(serviceName);\n\n  return (\n    <ThemeProvider theme={theme}>\n      <ServiceBadgeImpl serviceName={serviceName} {...props} />\n    </ThemeProvider>\n  );\n};\n\nServiceBadge.propTypes = propTypes;\nServiceBadge.defaultProps = defaultProps;\n\nexport default ServiceBadge;\n"
  },
  {
    "path": "zipkin-lens/src/components/common/ServiceBadge.test.jsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, afterEach } from 'vitest';\nimport React from 'react';\nimport { render, cleanup, screen } from '@testing-library/react';\nimport ServiceBadge from './ServiceBadge';\n\ndescribe('<ServiceBadge />', () => {\n  afterEach(cleanup);\n  describe('should render a label correctly', () => {\n    it('only serviceName', () => {\n      const { getByTestId } = render(<ServiceBadge serviceName=\"serviceA\" />);\n      const item = getByTestId('badge');\n      expect(item.textContent).toBe('serviceA');\n    });\n\n    it('with count', () => {\n      const { getByTestId } = render(\n        <ServiceBadge serviceName=\"serviceA\" count={8} />,\n      );\n      const item = getByTestId('badge');\n      expect(item.textContent).toBe('serviceA (8)');\n    });\n  });\n\n  it('should render delete button when onDelete is set', () => {\n    // eslint-disable-next-line no-empty-function\n    render(<ServiceBadge serviceName=\"serviceA\" onDelete={() => undefined} />);\n    const items = screen.getByTestId('delete-button');\n    expect(items.children.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/constants/api.test.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable global-require */\n\nimport { afterEach, it, describe, expect, vi } from 'vitest';\n\n// To test BASE_PATH we can't import it above, as it would return a constant\n// value for all tests. Instead, we use `await import` later.\ndescribe('BASE_PATH', () => {\n  afterEach(() => {\n    vi.resetModules();\n  });\n\n  it('defaults to /zipkin with no base tag', async () => {\n    const { BASE_PATH } = await import('./api');\n    expect(BASE_PATH).toEqual('/zipkin');\n  });\n\n  it('is set to base tag when present', async () => {\n    const base = document.createElement('base');\n    base.setAttribute('href', '/coolzipkin/');\n    document.head.append(base);\n\n    const { BASE_PATH } = await import('./api');\n    expect(BASE_PATH).toEqual('/coolzipkin');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/constants/api.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nconst extractBasePath = () => {\n  const base = document.getElementsByTagName('base');\n  if (base.length === 0) {\n    return '/zipkin';\n  }\n  return base[0]?.getAttribute('href')?.replace(/\\/+$/, '');\n};\n\nexport const BASE_PATH = extractBasePath();\n\nexport const ZIPKIN_API = `${BASE_PATH}/api/v2`;\nexport const UI_CONFIG = `${BASE_PATH}/config.json`;\nexport const SERVICES = `${ZIPKIN_API}/services`;\nexport const REMOTE_SERVICES = `${ZIPKIN_API}/remoteServices`;\nexport const SPANS = `${ZIPKIN_API}/spans`;\nexport const TRACES = `${ZIPKIN_API}/traces`;\nexport const TRACE = `${ZIPKIN_API}/trace`;\nexport const DEPENDENCIES = `${ZIPKIN_API}/dependencies`;\nexport const AUTOCOMPLETE_KEYS = `${ZIPKIN_API}/autocompleteKeys`;\nexport const AUTOCOMPLETE_VALUES = `${ZIPKIN_API}/autocompleteValues`;\n"
  },
  {
    "path": "zipkin-lens/src/constants/color.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { createTheme } from '@material-ui/core/styles';\nimport * as colors from '@material-ui/core/colors';\nimport { getTheme } from '../util/theme';\n\nexport const primaryColor = '#005B8A';\nexport const secondaryColor = '#c8001d';\nexport const errorColor = '#c8001d';\n\nexport const THEME = [\n  {\n    theme: createTheme({\n      palette: {\n        type: 'light',\n        primary: {\n          main: primaryColor,\n          contrastText: '#fff',\n        },\n        secondary: {\n          main: secondaryColor,\n        },\n        error: {\n          main: errorColor,\n          contrastText: '#ffffff',\n        },\n      },\n    }),\n    name: 'light',\n    label: 'Light',\n    servicePalette: [\n      '#f44336',\n      '#e91e63',\n      '#9c27b0',\n      '#673ab7',\n      '#3f51b5',\n      '#2196f3',\n      '#03a9f4',\n      '#00bcd4',\n      '#009688',\n      '#4caf50',\n      '#8bc34a',\n      '#cddc39',\n      '#ffeb3b',\n      '#ffc107',\n      '#ff9800',\n      '#ff5722',\n      '#795548',\n      '#9e9e9e',\n      '#607d8b',\n    ],\n  },\n  {\n    name: 'dark',\n    label: 'Dark',\n    servicePalette: [\n      '#f44336',\n      '#e91e63',\n      '#9c27b0',\n      '#673ab7',\n      '#3f51b5',\n      '#2196f3',\n      '#03a9f4',\n      '#00bcd4',\n      '#009688',\n      '#4caf50',\n      '#8bc34a',\n      '#cddc39',\n      '#ffeb3b',\n      '#ffc107',\n      '#ff9800',\n      '#ff5722',\n      '#795548',\n      '#9e9e9e',\n      '#607d8b',\n    ],\n    theme: createTheme({\n      palette: {\n        type: 'dark',\n        primary: {\n          main: '#1984BB',\n          contrastText: '#fff',\n        },\n        secondary: {\n          main: '#f50024',\n          contrastText: '#ffffff',\n        },\n        error: {\n          main: '#f50024',\n          contrastText: '#ffffff',\n        },\n        background: {\n          paper: '#424242',\n        },\n        grey: {\n          /*\n             Note: Gray colors with shades 50 and 100 are initially used as background colors\n             in certain sections. However, due to the specific theme requirements,\n             these are overridden with darker shades for better visual compatibility\n             and theme coherence.\n          */\n          50: '#4c4c4c',\n          100: '#424242',\n        },\n      },\n    }),\n  },\n];\n\nexport const darkTheme = createTheme({\n  palette: {\n    type: 'dark',\n    primary: {\n      main: primaryColor,\n      contrastText: '#fff',\n    },\n    error: {\n      main: '#f50024',\n      contrastText: '#ffffff',\n    },\n  },\n});\n\nfunction getCurrentTheme() {\n  const currentThemeName = getTheme();\n  const currentTheme = THEME.find((x) => x.name === currentThemeName);\n  console.log(`${currentTheme?.name} => ${currentThemeName}`);\n  if (currentTheme) {\n    return currentTheme.theme;\n  }\n  return THEME[0].theme;\n}\n\nfunction getCurrentThemeServicePalette() {\n  const currentThemeName = getTheme();\n  const currentTheme = THEME.find((x) => x.name === currentThemeName);\n  if (currentTheme) {\n    return currentTheme.servicePalette;\n  }\n  return THEME[0].servicePalette;\n}\n\nexport const theme = getCurrentTheme();\n\nexport const allColors = [\n  colors.red,\n  colors.pink,\n  colors.purple,\n  colors.deepPurple,\n  colors.indigo,\n  colors.blue,\n  colors.lightBlue,\n  colors.cyan,\n  colors.teal,\n  colors.green,\n  colors.lightGreen,\n  colors.lime,\n  colors.yellow,\n  colors.amber,\n  colors.orange,\n  colors.deepOrange,\n  colors.brown,\n  colors.grey,\n  colors.blueGrey,\n];\n\nexport const allColorThemes = allColors.map((color) =>\n  createTheme({\n    palette: {\n      primary: {\n        main: color[500],\n      },\n    },\n  }),\n);\n\n/* eslint no-bitwise: [\"error\", { \"allow\": [\"<<\", \"|=\"] }] */\nconst generateHash = (str: string) => {\n  let hash = 0;\n  if (str.length === 0) return hash;\n  for (let i = 0; i < str.length; i += 1) {\n    const c = str.charCodeAt(i);\n    hash = (hash << 5) - hash + c;\n    hash |= 0; // Convert to 32bit integer\n  }\n  return Math.abs(hash); // Only positive number.\n};\n\nexport const selectServiceTheme = (serviceName: string) => {\n  const hash = generateHash(serviceName);\n  const selectedServicePalette = getCurrentThemeServicePalette();\n  const themePalette = selectedServicePalette.map((color) =>\n    createTheme({\n      palette: {\n        primary: {\n          main: color,\n        },\n      },\n    }),\n  );\n  return themePalette[hash % selectedServicePalette.length];\n};\n\nexport const selectServiceColor = (serviceName: string) =>\n  selectServiceTheme(serviceName).palette.primary.dark;\n\nexport const selectColorByErrorType = (errorType: string) => {\n  switch (errorType) {\n    case 'transient':\n      return theme.palette.error.main;\n    case 'critical':\n      return theme.palette.error.main;\n    default:\n      return theme.palette.primary.main;\n  }\n};\n\nexport const selectColorByInfoClass = (infoClass: string) => {\n  switch (infoClass) {\n    case 'trace-error-transient':\n      return selectColorByErrorType('transient');\n    case 'trace-error-critical':\n      return selectColorByErrorType('critical');\n    default:\n      return selectColorByErrorType('none');\n  }\n};\n"
  },
  {
    "path": "zipkin-lens/src/index.css",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n@import 'vizceral-react/dist/vizceral.css';\n"
  },
  {
    "path": "zipkin-lens/src/index.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { I18nextProvider } from 'react-i18next';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './components/App';\nimport './index.css';\n\nimport i18n from './translations/i18n';\n\nReactDOM.render(\n  <I18nextProvider i18n={i18n}>\n    <App />\n  </I18nextProvider>,\n  document.getElementById('root'),\n);\n"
  },
  {
    "path": "zipkin-lens/src/models/AdjustedTrace.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { ServiceNameAndSpanCount } from './TraceSummary';\n\nexport type AdjustedAnnotation = {\n  value: string;\n  timestamp: number;\n  endpoint: string; // Ex. 'fooo' or 'unknown' on null span.localEndpoint\n  relativeTime?: string;\n};\n\nexport type AdjustedSpan = {\n  spanId: string;\n  spanName: string; // span.name or 'unknown' on null\n  serviceName: string; // span.localEndpoint.serviceName or 'unknown' on null\n  parentId?: string;\n  childIds: string[];\n  serviceNames: string[];\n  timestamp: number;\n  duration: number;\n  durationStr: string;\n  tags: {\n    key: string;\n    value: string;\n  }[];\n  annotations: AdjustedAnnotation[];\n  errorType: string;\n  depth: number;\n  width: number;\n  left: number;\n};\n\ntype AdjustedTrace = {\n  traceId: string;\n  serviceNameAndSpanCounts: ServiceNameAndSpanCount[];\n  duration: number;\n  durationStr: string;\n  // the root-most span, when the root is missing\n  rootSpan: {\n    serviceName: string; // span.localEndpoint.serviceName or 'unknown' on null\n    spanName: string; // span.name or 'unknown' on null\n  };\n  spans: AdjustedSpan[];\n};\n\nexport default AdjustedTrace;\n"
  },
  {
    "path": "zipkin-lens/src/models/Annotation.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n// Refer to https://github.com/openzipkin/zipkin-js/blob/master/packages/zipkin/src/model.js\n\n// Same type as Annotation in the OpenApi/Swagger model https://zipkin.io/zipkin-api/#\ntype Annotation = {\n  timestamp: number;\n  value: string;\n};\n\nexport default Annotation;\n"
  },
  {
    "path": "zipkin-lens/src/models/Dependencies.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\ntype Dependencies = {\n  parent: string;\n  child: string;\n  callCount: number;\n  errorCount?: number;\n}[];\n\nexport default Dependencies;\n"
  },
  {
    "path": "zipkin-lens/src/models/Endpoint.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n// Refer to https://github.com/openzipkin/zipkin-js/blob/master/packages/zipkin/src/model.js\n\n// Same type as Endpoint in the OpenApi/Swagger model https://zipkin.io/zipkin-api/#\ntype Endpoint = {\n  serviceName?: string;\n  ipv4?: string;\n  ipv6?: string;\n  port?: number;\n};\n\nexport default Endpoint;\n"
  },
  {
    "path": "zipkin-lens/src/models/Span.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport Annotation from './Annotation';\nimport Endpoint from './Endpoint';\n\n// Refer to https://github.com/openzipkin/zipkin-js/blob/master/packages/zipkin/src/model.js\n\n// Same type as Span in the OpenApi/Swagger model https://zipkin.io/zipkin-api/#\ntype Span = {\n  id: string;\n  traceId: string;\n  name?: string;\n  parentId?: string;\n  kind?: 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';\n  timestamp?: number;\n  duration?: number;\n  debug?: boolean;\n  shared?: boolean;\n  localEndpoint?: Endpoint;\n  remoteEndpoint?: Endpoint;\n  annotations?: Annotation[];\n  tags?: { [key: string]: string };\n};\n\nexport default Span;\n"
  },
  {
    "path": "zipkin-lens/src/models/TraceSummary.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport type ServiceNameAndSpanCount = {\n  serviceName: string;\n  spanCount: number;\n};\n\ntype TraceSummary = {\n  traceId: string;\n  timestamp: number;\n  duration: number;\n  serviceSummaries: ServiceNameAndSpanCount[];\n  infoClass?: string;\n  spanCount: number;\n  width: number;\n  root: {\n    serviceName: string;\n    spanName: string;\n  };\n};\n\nexport default TraceSummary;\n"
  },
  {
    "path": "zipkin-lens/src/prop-types/index.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport PropTypes from 'prop-types';\n\nexport const spanTagPropTypes = PropTypes.shape({\n  key: PropTypes.string.isRequired,\n  value: PropTypes.string.isRequired,\n});\n\nexport const spanTagsPropTypes = PropTypes.arrayOf(spanTagPropTypes);\n\nexport const spanAnnotationPropTypes = PropTypes.shape({\n  value: PropTypes.string.isRequired,\n  timestamp: PropTypes.number.isRequired,\n  endpoint: PropTypes.string.isRequired,\n  relativeTime: PropTypes.string.isRequired,\n});\n\nexport const spanAnnotationsPropTypes = PropTypes.arrayOf(\n  spanAnnotationPropTypes,\n);\n\n// TODO: Verify which fields we should enforce here, as some are optional per\n// https://github.com/openzipkin/zipkin-api/blob/master/zipkin2-api.yaml\nexport const detailedSpanPropTypes = PropTypes.shape({\n  spanId: PropTypes.string.isRequired,\n  spanName: PropTypes.string.isRequired,\n  parentId: PropTypes.string,\n  childIds: PropTypes.arrayOf(PropTypes.string).isRequired,\n  serviceName: PropTypes.string.isRequired,\n  serviceNames: PropTypes.arrayOf(PropTypes.string).isRequired,\n  timestamp: PropTypes.number.isRequired,\n  duration: PropTypes.number,\n  durationStr: PropTypes.string,\n  tags: spanTagsPropTypes.isRequired,\n  annotations: spanAnnotationsPropTypes.isRequired,\n  errorType: PropTypes.string.isRequired,\n  // Data for Trace Timeline\n  depth: PropTypes.number.isRequired,\n  width: PropTypes.number.isRequired,\n  left: PropTypes.number.isRequired,\n});\n\nexport const detailedSpansPropTypes = PropTypes.arrayOf(detailedSpanPropTypes);\n\nexport const spanServiceNameSummary = PropTypes.shape({\n  serviceName: PropTypes.string.isRequired,\n  spanCount: PropTypes.number.isRequired,\n});\n\nexport const spanServiceNameSummaries = PropTypes.arrayOf(\n  spanServiceNameSummary,\n);\n\nexport const traceSummaryPropTypes = PropTypes.shape({\n  traceId: PropTypes.string.isRequired,\n  timestamp: PropTypes.number.isRequired,\n  duration: PropTypes.number.isRequired,\n  durationStr: PropTypes.string.isRequired,\n  servicePercentage: PropTypes.number,\n  serviceSummaries: spanServiceNameSummaries.isRequired,\n  infoClass: PropTypes.string,\n  spanCount: PropTypes.number.isRequired,\n  width: PropTypes.number.isRequired,\n});\n\nexport const traceSummariesPropTypes = PropTypes.arrayOf(traceSummaryPropTypes);\n\nexport const detailedTraceSummaryPropTypes = PropTypes.shape({\n  traceId: PropTypes.string.isRequired,\n  spans: detailedSpansPropTypes.isRequired,\n  serviceNameAndSpanCounts: spanServiceNameSummaries.isRequired,\n  duration: PropTypes.number.isRequired,\n  durationStr: PropTypes.string.isRequired,\n  rootSpan: PropTypes.shape({\n    serviceName: PropTypes.string.isRequired,\n    spanName: PropTypes.string.isRequired,\n  }).isRequired,\n});\n"
  },
  {
    "path": "zipkin-lens/src/reducers/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { combineReducers } from 'redux';\n\nimport appSlice from '../components/App/slice';\nimport autocompleteKeysSlice from '../slices/autocompleteKeysSlice';\nimport autocompleteValuesSlice from '../slices/autocompleteValuesSlice';\nimport dependenciesSlice from '../slices/dependenciesSlice';\nimport remoteServicesSlice from '../slices/remoteServicesSlice';\nimport servicesSlice from '../slices/servicesSlice';\nimport spansSlice from '../slices/spansSlice';\nimport tracesSlice from '../slices/tracesSlice';\n\nconst createReducer = () =>\n  combineReducers({\n    [appSlice.name]: appSlice.reducer,\n    [autocompleteKeysSlice.name]: autocompleteKeysSlice.reducer,\n    [autocompleteValuesSlice.name]: autocompleteValuesSlice.reducer,\n    [dependenciesSlice.name]: dependenciesSlice.reducer,\n    [remoteServicesSlice.name]: remoteServicesSlice.reducer,\n    [servicesSlice.name]: servicesSlice.reducer,\n    [spansSlice.name]: spansSlice.reducer,\n    [tracesSlice.name]: tracesSlice.reducer,\n  });\n\nexport default createReducer;\n"
  },
  {
    "path": "zipkin-lens/src/setupTests.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport '@testing-library/jest-dom';\nimport fetchMock from 'fetch-mock';\n\nimport { beforeAll, beforeEach, vi, afterAll } from 'vitest';\nimport { UI_CONFIG } from './constants/api';\n\n// Mock out UI Config fetch with an empty config as a baseline, tests can remock as needed.\nfetchMock.mock(UI_CONFIG, {});\n\n// Mock out browser refresh.\nconst { location } = window;\n\n// Allow redefining browser language.\nconst { language } = window.navigator;\n\nbeforeAll(() => {\n  window.location = {\n    ...location,\n    reload: vi.fn(),\n  } as any; // Only partial mock so don't enforce full type.\n});\n\nbeforeEach(() => {\n  // Set english as browser locale by default.\n  Object.defineProperty(window.navigator, 'language', {\n    value: 'en-US',\n    configurable: true,\n  });\n});\n\nafterAll(() => {\n  // Restore overrides for good measure.\n  Object.defineProperty(window.navigator, 'language', {\n    value: language,\n    configurable: true,\n  });\n  window.location = location;\n});\n"
  },
  {
    "path": "zipkin-lens/src/slices/autocompleteKeysSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\n\nexport const loadAutocompleteKeys = createAsyncThunk(\n  'autocompleteKeys/fetch',\n  async () => {\n    const resp = await fetch(api.AUTOCOMPLETE_KEYS);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const json = await resp.json();\n    return json as string[];\n  },\n);\n\nexport interface AutocompleteKeysState {\n  isLoading: boolean;\n  autocompleteKeys: string[];\n  error?: SerializedError;\n}\n\nconst initialState: AutocompleteKeysState = {\n  isLoading: false,\n  autocompleteKeys: [],\n  error: undefined,\n};\n\nconst autocompleteKeysSlice = createSlice({\n  name: 'autocompleteKeys',\n  initialState,\n  reducers: {},\n  extraReducers: (builder) => {\n    builder.addCase(loadAutocompleteKeys.pending, (state) => {\n      state.isLoading = true;\n      state.autocompleteKeys = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadAutocompleteKeys.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.autocompleteKeys = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadAutocompleteKeys.rejected, (state, action) => {\n      state.isLoading = false;\n      state.autocompleteKeys = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport default autocompleteKeysSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/autocompleteValuesSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\n\nexport const loadAutocompleteValues = createAsyncThunk(\n  'autocompleteValues/fetch',\n  async (autocompleteKey: string) => {\n    const params = new URLSearchParams();\n    params.set('key', autocompleteKey);\n    const resp = await fetch(`${api.AUTOCOMPLETE_VALUES}?${params.toString()}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const json = await resp.json();\n    return json as string[];\n  },\n);\n\nexport interface AutocompleteValuesState {\n  isLoading: boolean;\n  autocompleteValues: string[];\n  error?: SerializedError;\n}\n\nconst initialState: AutocompleteValuesState = {\n  isLoading: false,\n  autocompleteValues: [],\n  error: undefined,\n};\n\nconst autocompleteValuesSlice = createSlice({\n  name: 'autocompleteValues',\n  initialState,\n  reducers: {},\n  extraReducers: (builder) => {\n    builder.addCase(loadAutocompleteValues.pending, (state) => {\n      state.isLoading = true;\n      state.autocompleteValues = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadAutocompleteValues.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.autocompleteValues = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadAutocompleteValues.rejected, (state, action) => {\n      state.isLoading = false;\n      state.autocompleteValues = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport default autocompleteValuesSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/dependenciesSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\nimport Dependencies from '../models/Dependencies';\n\nexport const loadDependencies = createAsyncThunk(\n  'dependencies/fetch',\n  async (params: { lookback?: number; endTs: number }) => {\n    const ps = new URLSearchParams();\n    if (params.lookback) {\n      ps.set('lookback', params.lookback.toString());\n    }\n    ps.set('endTs', params.endTs.toString());\n\n    const resp = await fetch(`${api.DEPENDENCIES}?${ps.toString()}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const json = await resp.json();\n    return json as Dependencies;\n  },\n);\n\nexport interface DependenciesState {\n  isLoading: boolean;\n  dependencies: Dependencies;\n  error?: SerializedError;\n}\n\nconst initialState: DependenciesState = {\n  isLoading: false,\n  dependencies: [],\n  error: undefined,\n};\n\nconst dependenciesSlice = createSlice({\n  name: 'dependencies',\n  initialState,\n  reducers: {\n    clearDependencies: (state) => {\n      state.isLoading = false;\n      state.dependencies = [];\n      state.error = undefined;\n    },\n  },\n  extraReducers: (builder) => {\n    builder.addCase(loadDependencies.pending, (state) => {\n      state.isLoading = true;\n      state.dependencies = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadDependencies.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.dependencies = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadDependencies.rejected, (state, action) => {\n      state.isLoading = false;\n      state.dependencies = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport const { clearDependencies } = dependenciesSlice.actions;\n\nexport default dependenciesSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/remoteServicesSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\n\nexport const loadRemoteServices = createAsyncThunk(\n  'remoteServices/fetch',\n  async (serviceName: string) => {\n    const params = new URLSearchParams();\n    params.set('serviceName', serviceName);\n    const resp = await fetch(`${api.REMOTE_SERVICES}?${params.toString()}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const json = await resp.json();\n    return json as string[];\n  },\n);\n\nexport interface RemoteServicesState {\n  isLoading: boolean;\n  remoteServices: string[];\n  error?: SerializedError;\n}\n\nconst initialState: RemoteServicesState = {\n  isLoading: false,\n  remoteServices: [],\n  error: undefined,\n};\n\nconst remoteServicesSlice = createSlice({\n  name: 'remoteServices',\n  initialState,\n  reducers: {}, // SearchBar.tsx issues load on serviceName change, so no need to clear\n  extraReducers: (builder) => {\n    builder.addCase(loadRemoteServices.pending, (state) => {\n      state.isLoading = true;\n      state.remoteServices = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadRemoteServices.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.remoteServices = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadRemoteServices.rejected, (state, action) => {\n      state.isLoading = false;\n      state.remoteServices = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport default remoteServicesSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/servicesSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\n\nexport const loadServices = createAsyncThunk('services/fetch', async () => {\n  const resp = await fetch(api.SERVICES);\n  if (!resp.ok) {\n    throw Error(resp.statusText);\n  }\n  const json = await resp.json();\n  return json as string[];\n});\n\nexport interface ServicesState {\n  isLoading: boolean;\n  services: string[];\n  error?: SerializedError;\n}\n\nconst initialState: ServicesState = {\n  isLoading: false,\n  services: [],\n  error: undefined,\n};\n\nconst servicesSlice = createSlice({\n  name: 'services',\n  initialState,\n  reducers: {},\n  extraReducers: (builder) => {\n    builder.addCase(loadServices.pending, (state) => {\n      state.isLoading = true;\n      state.services = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadServices.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.services = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadServices.rejected, (state, action) => {\n      state.isLoading = false;\n      state.services = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport default servicesSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/spansSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/* eslint-disable no-param-reassign */\n\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\n\nexport const loadSpans = createAsyncThunk(\n  'spans/fetch',\n  async (serviceName: string) => {\n    const params = new URLSearchParams();\n    params.set('serviceName', serviceName);\n    const resp = await fetch(`${api.SPANS}?${params.toString()}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const json = await resp.json();\n    return json as string[];\n  },\n);\n\nexport interface SpansState {\n  isLoading: boolean;\n  spans: string[];\n  error?: SerializedError;\n}\n\nconst initialState: SpansState = {\n  isLoading: false,\n  spans: [],\n  error: undefined,\n};\n\nconst spansSlice = createSlice({\n  name: 'spans',\n  initialState,\n  reducers: {}, // SearchBar.tsx issues load on serviceName change, so no need to clear\n  extraReducers: (builder) => {\n    builder.addCase(loadSpans.pending, (state) => {\n      state.isLoading = true;\n      state.spans = [];\n      state.error = undefined;\n    });\n    builder.addCase(loadSpans.fulfilled, (state, action) => {\n      state.isLoading = false;\n      state.spans = action.payload;\n      state.error = undefined;\n    });\n    builder.addCase(loadSpans.rejected, (state, action) => {\n      state.isLoading = false;\n      state.spans = [];\n      state.error = action.error;\n    });\n  },\n});\n\nexport default spansSlice;\n"
  },
  {
    "path": "zipkin-lens/src/slices/tracesSlice.test.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, afterEach } from 'vitest';\nimport configureStore from 'redux-mock-store';\nimport thunk from 'redux-thunk';\nimport fetchMock from 'fetch-mock';\n\nimport { loadTrace, searchTraces, TracesState } from './tracesSlice';\nimport * as api from '../constants/api';\nimport TraceSummary from '../models/TraceSummary';\nimport Span from '../models/Span';\nimport {\n  treeCorrectedForClockSkew,\n  detailedTraceSummary as buildDetailedTraceSummary,\n} from '../zipkin';\nimport AdjustedTrace from '../models/AdjustedTrace';\n\nconst frontend = {\n  serviceName: 'frontend',\n  ipv4: '172.17.0.13',\n};\n\nconst backend = {\n  serviceName: 'backend',\n  ipv4: '172.17.0.9',\n};\n\nconst httpTrace: Span[] = [\n  {\n    traceId: 'bb1f0e21882325b8',\n    parentId: 'bb1f0e21882325b8',\n    id: 'c8c50ebd2abc179e',\n    kind: 'CLIENT',\n    name: 'get',\n    timestamp: 1541138169297572,\n    duration: 111121,\n    localEndpoint: frontend,\n    annotations: [\n      { value: 'ws', timestamp: 1541138169337695 },\n      { value: 'wr', timestamp: 1541138169368570 },\n    ],\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n    },\n  },\n  {\n    traceId: 'bb1f0e21882325b8',\n    id: 'bb1f0e21882325b8',\n    kind: 'SERVER',\n    name: 'get /',\n    timestamp: 1541138169255688,\n    duration: 168731,\n    localEndpoint: frontend,\n    remoteEndpoint: {\n      ipv4: '110.170.201.178',\n      port: 63678,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/',\n      'mvc.controller.class': 'Frontend',\n      'mvc.controller.method': 'callBackend',\n    },\n  },\n  {\n    traceId: 'bb1f0e21882325b8',\n    parentId: 'bb1f0e21882325b8',\n    id: 'c8c50ebd2abc179e',\n    kind: 'SERVER',\n    name: 'get /api',\n    timestamp: 1541138169377997, // this is actually skewed right, but we can't correct it\n    duration: 26326,\n    localEndpoint: backend,\n    remoteEndpoint: {\n      ipv4: '172.17.0.13',\n      port: 63679,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n      'mvc.controller.class': 'Backend',\n      'mvc.controller.method': 'printDate',\n    },\n    shared: true,\n  },\n];\n\ndescribe('tracesSlice', () => {\n  describe('searchTraces', () => {\n    afterEach(() => {\n      fetchMock.reset();\n    });\n\n    it('should return the existing state, if the search criteria have not been changed', () => {\n      const mockStore = configureStore([thunk]);\n\n      const traceSummaries: TraceSummary[] = [\n        {\n          traceId: '12345',\n          timestamp: 123456789,\n          duration: 1234,\n          serviceSummaries: [],\n          spanCount: 10,\n          width: 10,\n          root: {\n            serviceName: 'Example Service',\n            spanName: 'Example Span',\n          },\n        },\n      ];\n\n      const query = {\n        serviceName: 'Example Service',\n        spanName: 'Example Span',\n      };\n      const queryStr = new URLSearchParams(query).toString();\n\n      const initialState: { traces: TracesState } = {\n        traces: {\n          traces: {},\n          search: {\n            isLoading: false,\n            error: undefined,\n            prevQuery: queryStr,\n            traceSummaries,\n          },\n        },\n      };\n\n      const store = mockStore(initialState);\n\n      return store.dispatch(searchTraces(query)).then(() => {\n        expect(store.getActions().length).toBe(2);\n        expect(store.getActions()[0].type).toBe('traces/search/pending');\n        expect(store.getActions()[1].type).toBe('traces/search/fulfilled');\n        const [, { payload }] = store.getActions();\n        expect(payload.traceSummaries).toBe(traceSummaries);\n        expect(payload.query).toBe(queryStr);\n      });\n    });\n\n    it('should fetch new traces, if the search criteria have been changed', () => {\n      const mockStore = configureStore([thunk]);\n\n      const prevTraceSummaries: TraceSummary[] = [\n        {\n          traceId: '12345',\n          timestamp: 123456789,\n          duration: 1234,\n          serviceSummaries: [],\n          spanCount: 10,\n          width: 10,\n          root: {\n            serviceName: 'Previous Example Service',\n            spanName: 'Previous Example Span',\n          },\n        },\n      ];\n\n      const prevQuery = {\n        serviceName: 'Previous Example Service',\n        spanName: 'Previous Example Span',\n      };\n      const prevQueryStr = new URLSearchParams(prevQuery).toString();\n\n      const initialState: { traces: TracesState } = {\n        traces: {\n          traces: {},\n          search: {\n            isLoading: false,\n            error: undefined,\n            prevQuery: prevQueryStr,\n            traceSummaries: prevTraceSummaries,\n          },\n        },\n      };\n\n      fetchMock.get(`${api.TRACES}?serviceName=Example+Service`, {\n        status: 200,\n        body: [httpTrace],\n      });\n\n      const store = mockStore(initialState);\n\n      const newQuery = {\n        serviceName: 'Example Service',\n      };\n      const newQueryStr = new URLSearchParams(newQuery).toString();\n\n      return store.dispatch(searchTraces(newQuery)).then(() => {\n        expect(store.getActions().length).toBe(2);\n        expect(store.getActions()[0].type).toBe('traces/search/pending');\n        expect(store.getActions()[1].type).toBe('traces/search/fulfilled');\n        const [, { payload }] = store.getActions();\n        expect(payload.traceSummaries).not.toBe(prevTraceSummaries);\n        expect(payload.query).toBe(newQueryStr);\n      });\n    });\n  });\n\n  describe('loadTrace', () => {\n    it('should return the existing state, if the trace already exists', () => {\n      const mockStore = configureStore([thunk]);\n\n      const skewCorrectedTrace = treeCorrectedForClockSkew(httpTrace);\n      const adjustedTrace: AdjustedTrace = buildDetailedTraceSummary(\n        skewCorrectedTrace,\n      ) as unknown as AdjustedTrace;\n\n      const initialState: { traces: TracesState } = {\n        traces: {\n          traces: {\n            bb1f0e21882325b8: {\n              isLoading: false,\n              error: undefined,\n              rawTrace: httpTrace,\n              adjustedTrace,\n              skewCorrectedTrace,\n            },\n          },\n          search: {\n            isLoading: false,\n            error: undefined,\n            prevQuery: undefined,\n            traceSummaries: [],\n          },\n        },\n      };\n\n      const store = mockStore(initialState);\n\n      return store.dispatch(loadTrace('bb1f0e21882325b8')).then(() => {\n        expect(store.getActions().length).toBe(2);\n        expect(store.getActions()[0].type).toBe('traces/load/pending');\n        expect(store.getActions()[1].type).toBe('traces/load/fulfilled');\n        const [, { payload }] = store.getActions();\n        expect(payload.rawTrace).toBe(httpTrace);\n        expect(payload.skewCorrectedTrace).toBe(skewCorrectedTrace);\n        expect(payload.adjustedTrace).toBe(adjustedTrace);\n      });\n    });\n\n    it(\"should calculate adjustedTrace by buildDetailedTraceSummary, if the trace's skewCorrectedTrace already exists\", () => {\n      const mockStore = configureStore([thunk]);\n\n      const skewCorrectedTrace = treeCorrectedForClockSkew(httpTrace);\n      const adjustedTrace: AdjustedTrace = buildDetailedTraceSummary(\n        skewCorrectedTrace,\n      ) as any;\n\n      const initialState: { traces: TracesState } = {\n        traces: {\n          traces: {\n            bb1f0e21882325b8: {\n              isLoading: false,\n              error: undefined,\n              rawTrace: httpTrace,\n              adjustedTrace: undefined,\n              skewCorrectedTrace,\n            },\n          },\n          search: {\n            isLoading: false,\n            error: undefined,\n            prevQuery: undefined,\n            traceSummaries: [],\n          },\n        },\n      };\n\n      const store = mockStore(initialState);\n\n      return store.dispatch(loadTrace('bb1f0e21882325b8')).then(() => {\n        expect(store.getActions().length).toBe(2);\n        expect(store.getActions()[0].type).toBe('traces/load/pending');\n        expect(store.getActions()[1].type).toBe('traces/load/fulfilled');\n        const [, { payload }] = store.getActions();\n        expect(payload.rawTrace).toBe(httpTrace);\n        expect(payload.skewCorrectedTrace).toBe(skewCorrectedTrace);\n        expect(payload.adjustedTrace).toEqual(adjustedTrace);\n      });\n    });\n\n    it('should fetch the trace, if the trace data does not exist', () => {\n      const mockStore = configureStore([thunk]);\n\n      const skewCorrectedTrace = treeCorrectedForClockSkew(httpTrace);\n      const adjustedTrace: AdjustedTrace = buildDetailedTraceSummary(\n        skewCorrectedTrace,\n      ) as any;\n\n      const initialState: { traces: TracesState } = {\n        traces: {\n          traces: {},\n          search: {\n            isLoading: false,\n            error: undefined,\n            prevQuery: undefined,\n            traceSummaries: [],\n          },\n        },\n      };\n\n      fetchMock.get(`${api.TRACE}/bb1f0e21882325b8`, {\n        status: 200,\n        body: httpTrace,\n      });\n\n      const store = mockStore(initialState);\n\n      return store.dispatch(loadTrace('bb1f0e21882325b8')).then(() => {\n        expect(store.getActions().length).toBe(2);\n        expect(store.getActions()[0].type).toBe('traces/load/pending');\n        expect(store.getActions()[1].type).toBe('traces/load/fulfilled');\n        const [, { payload }] = store.getActions();\n        expect(payload.rawTrace).toEqual(httpTrace);\n        expect(payload.skewCorrectedTrace).toEqual(skewCorrectedTrace);\n        expect(payload.adjustedTrace).toEqual(adjustedTrace);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/slices/tracesSlice.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n  SerializedError,\n  createAsyncThunk,\n  createSlice,\n} from '@reduxjs/toolkit';\n\nimport * as api from '../constants/api';\nimport AdjustedTrace from '../models/AdjustedTrace';\nimport Span from '../models/Span';\nimport TraceSummary from '../models/TraceSummary';\nimport { ensureV2TraceData } from '../util/trace';\n\nimport {\n  treeCorrectedForClockSkew,\n  traceSummary as buildTraceSummary,\n  traceSummaries as buildTraceSummaries,\n  detailedTraceSummary as buildDetailedTraceSummary,\n} from '../zipkin';\n\nexport const searchTraces = createAsyncThunk(\n  'traces/search',\n  async (params: { [key: string]: string }, thunkApi) => {\n    const ps = new URLSearchParams(params);\n\n    // We need to import RootState in order to give the type to getState.\n    // Importing RootState will result in a cyclic import.\n    // So use any type to avoid this.\n    const { search, traces }: TracesState = (thunkApi.getState() as any).traces;\n    // If the query is the same as the previous query, it will not fetch again.\n    if (search.prevQuery === ps.toString()) {\n      return {\n        traces,\n        traceSummaries: search.traceSummaries,\n        query: ps.toString(),\n      };\n    }\n\n    const resp = await fetch(`${api.TRACES}?${ps.toString()}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const rawTraces: Span[][] = await resp.json();\n\n    const newTraces = rawTraces.reduce(\n      (acc, rawTrace) => {\n        const [{ traceId }] = rawTrace;\n        const skewCorrectedTrace = treeCorrectedForClockSkew(rawTrace);\n        acc[traceId] = {\n          rawTrace,\n          skewCorrectedTrace,\n        };\n        return acc;\n      },\n      {} as {\n        [key: string]: {\n          rawTrace: Span[];\n          skewCorrectedTrace: any;\n        };\n      },\n    );\n\n    const traceSummaries: TraceSummary[] = buildTraceSummaries(\n      ps.get('serviceName'),\n      Object.keys(newTraces).map((traceId) =>\n        buildTraceSummary(newTraces[traceId].skewCorrectedTrace),\n      ),\n    );\n\n    return {\n      traces: newTraces,\n      traceSummaries,\n      query: ps.toString(),\n    };\n  },\n);\n\nexport const loadTrace = createAsyncThunk(\n  'traces/load',\n  async (traceId: string, thunkApi) => {\n    // We need to import RootState in order to give the type to getState.\n    // Importing RootState will result in a cyclic import.\n    // So use any type to avoid this.\n    const { traces }: TracesState = (thunkApi.getState() as any).traces;\n\n    if (traces[traceId]) {\n      const { rawTrace, skewCorrectedTrace } = traces[traceId];\n      let { adjustedTrace } = traces[traceId];\n      if (adjustedTrace) {\n        // this trace has already been calculated by buildDetailedTraceSummary\n        return traces[traceId];\n      }\n      if (skewCorrectedTrace) {\n        adjustedTrace = buildDetailedTraceSummary(\n          skewCorrectedTrace,\n        ) as unknown as AdjustedTrace;\n        return {\n          rawTrace,\n          skewCorrectedTrace,\n          adjustedTrace,\n        };\n      }\n    }\n\n    const resp = await fetch(`${api.TRACE}/${traceId}`);\n    if (!resp.ok) {\n      throw Error(resp.statusText);\n    }\n    const rawTrace: Span[] = await resp.json();\n    const skewCorrectedTrace = treeCorrectedForClockSkew(rawTrace);\n    const adjustedTrace: AdjustedTrace = buildDetailedTraceSummary(\n      skewCorrectedTrace,\n    ) as unknown as AdjustedTrace;\n    return {\n      rawTrace,\n      skewCorrectedTrace,\n      adjustedTrace,\n    };\n  },\n);\n\nconst readFileAsync = (blob: Blob) => {\n  return new Promise<any>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      resolve(reader.result);\n    };\n    reader.onerror = () => {\n      reject(reader.error);\n    };\n    reader.readAsText(blob);\n  });\n};\n\nexport const loadJsonTrace = createAsyncThunk(\n  'traces/loadJson',\n  async (blob: Blob) => {\n    const rawTraceStr = await readFileAsync(blob);\n    const rawTrace: Span[] = JSON.parse(rawTraceStr);\n    ensureV2TraceData(rawTrace);\n    const [{ traceId }] = rawTrace;\n    const skewCorrectedTrace = treeCorrectedForClockSkew(rawTrace);\n    const adjustedTrace: AdjustedTrace = buildDetailedTraceSummary(\n      skewCorrectedTrace,\n    ) as unknown as AdjustedTrace;\n    return {\n      traceId,\n      trace: {\n        rawTrace,\n        skewCorrectedTrace,\n        adjustedTrace,\n      },\n    };\n  },\n);\n\nexport interface TracesState {\n  traces: {\n    [traceId: string]: {\n      // When fetching a specific trace, these isLoading and error states are used.\n      // They are not used in the search.\n      isLoading?: boolean;\n      error?: SerializedError;\n\n      rawTrace?: Span[];\n      adjustedTrace?: AdjustedTrace;\n      // This is a trace data with only the clock-skew modified.\n      // It is the intermediate data used to optimize the conversion process of the trace.\n      skewCorrectedTrace?: any;\n    };\n  };\n  search: {\n    // When searching, isLoading and error states are used.\n    // They are not used when fetching a specific trace.\n    isLoading: boolean;\n    error?: SerializedError;\n    // Save the previous query to avoid doing the same query unnecessarily.\n    prevQuery?: string;\n    traceSummaries: TraceSummary[];\n  };\n}\n\nconst initialState: TracesState = {\n  traces: {},\n  search: {\n    isLoading: false,\n    error: undefined,\n    prevQuery: undefined,\n    traceSummaries: [],\n  },\n};\n\nconst tracesSlice = createSlice({\n  name: 'traces',\n  initialState,\n  reducers: {\n    clearSearch: (state) => {\n      state.search.isLoading = false;\n      state.search.error = undefined;\n      state.search.prevQuery = undefined;\n      state.search.traceSummaries = [];\n    },\n  },\n  extraReducers: (builder) => {\n    builder.addCase(searchTraces.pending, (state) => {\n      const newSearchState = {\n        ...state.search,\n        isLoading: true,\n        error: undefined,\n      };\n      state.search = newSearchState;\n    });\n\n    builder.addCase(searchTraces.fulfilled, (state, action) => {\n      const { traces } = action.payload;\n      const newTraces = { ...state.traces };\n      Object.keys(traces).forEach((traceId) => {\n        newTraces[traceId] = traces[traceId];\n      });\n      const newSearchState = {\n        isLoading: false,\n        error: undefined,\n        prevQuery: action.payload.query,\n        traceSummaries: action.payload.traceSummaries,\n      };\n      state.search = newSearchState;\n      state.traces = newTraces;\n    });\n\n    builder.addCase(searchTraces.rejected, (state, action) => {\n      const newSearchState = {\n        ...state.search,\n        isLoading: false,\n        error: action.error,\n      };\n      state.search = newSearchState;\n    });\n\n    builder.addCase(loadTrace.pending, (state, action) => {\n      const traceId = action.meta.arg;\n      const newTraces = { ...state.traces };\n      if (!newTraces[traceId]) {\n        newTraces[traceId] = {};\n      }\n      newTraces[traceId].isLoading = true;\n      newTraces[traceId].error = undefined;\n      state.traces = newTraces;\n    });\n\n    builder.addCase(loadTrace.fulfilled, (state, action) => {\n      const traceId = action.meta.arg;\n      const newTraces = { ...state.traces };\n      newTraces[traceId] = { ...action.payload };\n      newTraces[traceId].isLoading = false;\n      newTraces[traceId].error = undefined;\n      state.traces = newTraces;\n    });\n\n    builder.addCase(loadTrace.rejected, (state, action) => {\n      const traceId = action.meta.arg;\n      const newTraces = { ...state.traces };\n      newTraces[traceId].isLoading = false;\n      newTraces[traceId].error = action.error;\n      state.traces = newTraces;\n    });\n\n    // It's easier to handle isLoading and error statuses on the component side,\n    // so don't change them here.\n    builder.addCase(loadJsonTrace.fulfilled, (state, action) => {\n      const { traceId, trace } = action.payload;\n      const newTraces = { ...state.traces };\n      newTraces[traceId] = trace;\n      state.traces = newTraces;\n    });\n  },\n});\n\nexport default tracesSlice;\n\nexport const { clearSearch } = tracesSlice.actions;\n"
  },
  {
    "path": "zipkin-lens/src/store/configure-store.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { createStore, applyMiddleware } from 'redux';\nimport thunk from 'redux-thunk';\n\nimport createReducer from '../reducers';\n\nexport default function configureStore(config) {\n  return createStore(createReducer(config), applyMiddleware(thunk));\n}\n"
  },
  {
    "path": "zipkin-lens/src/store/index.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport createReducer from '../reducers';\n\nexport type RootState = ReturnType<ReturnType<typeof createReducer>>;\n"
  },
  {
    "path": "zipkin-lens/src/test/data/malformed.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport default [\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'd65495313f944801',\n    id: 'd5cb70daead478dc',\n    name: 'kong.header_filter',\n    timestamp: 1570005203864000,\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'd65495313f944801',\n    id: '80b8efd52d66af27',\n    name: 'kong.body_filter',\n    timestamp: 1570005203864000,\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'ed395cb866d02665',\n    id: '2a49858b1f1d506b',\n    name: 'kong.rewrite',\n    timestamp: 1570005203829000,\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'd65495313f944801',\n    id: '3e53779bdde7c111',\n    name: 'kong.access',\n    timestamp: 1570005203829000,\n    duration: 16000,\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'ed395cb866d02665',\n    id: 'd65495313f944801',\n    kind: 'CLIENT',\n    name: 'kong.proxy',\n    timestamp: 1570005203829000,\n    duration: 35000,\n    localEndpoint: {\n      serviceName: 'rewards',\n    },\n    remoteEndpoint: {\n      port: 80,\n    },\n    tags: {\n      'kong.route': 'aaa20d62-813a-428f-aa40-447324cd30af',\n      'kong.service': '872eae11-4571-490b-86d2-59834b93f870',\n      'peer.hostname': '',\n    },\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    id: 'ed395cb866d02665',\n    kind: 'SERVER',\n    name: 'kong.request',\n    timestamp: 1570005203829000,\n    duration: 35000,\n    remoteEndpoint: {\n      port: 3771,\n    },\n    tags: {\n      component: 'kong',\n      'http.method': 'GET',\n      'http.status_code': '200',\n      'http.url': '',\n      'kong.node.id': '37714786-d34b-4513-a459-8020a1f917de',\n    },\n  },\n  {\n    traceId: '6cf5624e3452d1bc202a57dad35921c0',\n    parentId: 'd65495313f944801',\n    id: '862d0f978033cc53',\n    name: 'kong.balancer',\n    timestamp: 1570005203845000,\n    remoteEndpoint: {\n      port: 80,\n    },\n    tags: {\n      'kong.balancer.try': '1',\n    },\n  },\n];\n"
  },
  {
    "path": "zipkin-lens/src/test/data/skew.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport default [\n  {\n    traceId: '1e223ff1f80f1c69',\n    parentId: '74280ae0c10d8062',\n    id: '43210ae0c10d1234',\n    name: 'async',\n    timestamp: 1470150004008762,\n    duration: 65000,\n    localEndpoint: {\n      serviceName: 'serviceb',\n      ipv4: '192.0.0.0',\n    },\n  },\n  {\n    traceId: '1e223ff1f80f1c69',\n    parentId: 'bf396325699c84bf',\n    id: '74280ae0c10d8062',\n    kind: 'SERVER',\n    name: 'post',\n    timestamp: 1470150004008761,\n    duration: 93577,\n    localEndpoint: {\n      serviceName: 'serviceb',\n      ipv4: '192.0.0.0',\n    },\n    shared: true,\n  },\n  {\n    traceId: '1e223ff1f80f1c69',\n    id: 'bf396325699c84bf',\n    kind: 'SERVER',\n    name: 'get',\n    timestamp: 1470150004071068,\n    duration: 99411,\n    localEndpoint: {\n      serviceName: 'servicea',\n      ipv4: '127.0.0.0',\n    },\n  },\n  {\n    traceId: '1e223ff1f80f1c69',\n    parentId: 'bf396325699c84bf',\n    id: '74280ae0c10d8062',\n    kind: 'CLIENT',\n    name: 'post',\n    timestamp: 1470150004074202,\n    duration: 94539,\n    localEndpoint: {\n      serviceName: 'servicea',\n      ipv4: '127.0.0.0',\n    },\n  },\n];\n"
  },
  {
    "path": "zipkin-lens/src/test/util/render-with-default-settings.tsx",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport MomentUtils from '@date-io/moment';\nimport { ThemeProvider as MuiThemeProvider } from '@material-ui/styles';\nimport MuiPickersUtilsProvider from '@material-ui/pickers/MuiPickersUtilsProvider';\n// @ts-ignore\nimport { createMemoryHistory, History } from 'history';\nimport React from 'react';\nimport { Provider } from 'react-redux';\nimport { Router } from 'react-router-dom';\nimport { ThemeProvider } from 'styled-components';\n\nimport { render } from '@testing-library/react';\nimport { UiConfigContext } from '../../components/UiConfig';\nimport { theme } from '../../constants/color';\nimport configureStore from '../../store/configure-store';\n\ninterface RenderProps {\n  route?: string;\n  history?: History;\n  uiConfig?: object;\n}\n\nexport default (\n  ui: React.ReactElement,\n  {\n    route = '/',\n    history = createMemoryHistory({ initialEntries: [route] }),\n    uiConfig = {},\n  }: RenderProps = {},\n) => {\n  const store = configureStore({});\n\n  const filledConfig = {\n    // Defaults copied from ZipkinUiCOnfiguration.java\n    environment: '',\n    queryLimit: 10,\n    defaultLookback: 15 * 60 * 1000,\n    searchEnabled: true,\n    dependency: {\n      enabled: true,\n      lowErrorRate: 0.5,\n      highErrorRate: 0.75,\n    },\n    ...uiConfig,\n  };\n\n  const wrapper: React.FunctionComponent = ({ children }) => (\n    <Provider store={store}>\n      <Router history={history as any}>\n        <MuiPickersUtilsProvider utils={MomentUtils}>\n          <ThemeProvider theme={theme}>\n            <MuiThemeProvider theme={theme}>\n              <UiConfigContext.Provider value={filledConfig}>\n                {children}\n              </UiConfigContext.Provider>\n            </MuiThemeProvider>\n          </ThemeProvider>\n        </MuiPickersUtilsProvider>\n      </Router>\n    </Provider>\n  );\n  return {\n    ...render(ui, { wrapper }),\n    history,\n    store,\n  };\n};\n"
  },
  {
    "path": "zipkin-lens/src/translations/en/messages.d.ts",
    "content": "import type { Messages } from '@lingui/core';\n          declare const messages: Messages;\n          export { messages };\n          "
  },
  {
    "path": "zipkin-lens/src/translations/en/translations.json",
    "content": "{\n  \"{0} Results\": \"{0} Results\",\n  \"1 Result\": \"1 Result\",\n  \"Address\": \"Address\",\n  \"Archive Trace\": \"Archive Trace\",\n  \"Dependencies\": \"Dependencies\",\n  \"Download JSON\": \"Download JSON\",\n  \"Duration\": \"Duration\",\n  \"End Time\": \"End Time\",\n  \"Find a trace\": \"Find a trace\",\n  \"Limit\": \"Limit\",\n  \"Parent ID\": \"Parent ID\",\n  \"Please select criteria in the search bar. Then, click the search button.\": \"Please select criteria in the search bar. Then, click the search button.\",\n  \"Please select the start and end time. Then, click the search button.\": \"Please select the start and end time. Then, click the search button.\",\n  \"Root\": \"Root\",\n  \"Run Query\": \"Run Query\",\n  \"Search Dependencies\": \"Search Dependencies\",\n  \"Search Traces\": \"Search Traces\",\n  \"Searching has been disabled via the searchEnabled property. You can still view specific traces of which you know the trace id by entering it in the \\\"Trace ID...\\\" textbox on the top-right.\": \"Searching has been disabled via the searchEnabled property. You can still view specific traces of which you know the trace id by entering it in the \\\"Trace ID...\\\" textbox on the top-right.\",\n  \"Services\": \"Services\",\n  \"Span ID\": \"Span ID\",\n  \"Spans\": \"Spans\",\n  \"Start Time\": \"Start Time\",\n  \"Support\": \"Support\",\n  \"Total Spans\": \"Total Spans\",\n  \"Trace ID\": \"Trace ID\",\n  \"Upload JSON\": \"Upload JSON\",\n  \"View Logs\": \"View Logs\",\n  \"Zipkin\": \"Zipkin\"\n}\n"
  },
  {
    "path": "zipkin-lens/src/translations/es/messages.d.ts",
    "content": "import type { Messages } from '@lingui/core';\n          declare const messages: Messages;\n          export { messages };\n          "
  },
  {
    "path": "zipkin-lens/src/translations/es/translations.json",
    "content": "{\n  \"{0} Results\": \"{0} Resultados\",\n  \"1 Result\": \"1 Resultado\",\n  \"Address\": \"Dirección\",\n  \"Archive Trace\": \"Archivar Traza\",\n  \"Dependencies\": \"Dependencias\",\n  \"Download JSON\": \"Descargar JSON\",\n  \"Duration\": \"Duración\",\n  \"End Time\": \"Tiempo de Fin\",\n  \"Find a trace\": \"Encuentra un rastro\",\n  \"Limit\": \"Límite\",\n  \"Parent ID\": \"ID de Traza Padre\",\n  \"Please select criteria in the search bar. Then, click the search button.\": \"Por favor, selecciona un criterio en la barra de búsqueda. Luego, haz clic en el botón buscar.\",\n  \"Please select the start and end time. Then, click the search button.\": \"Por favor, selecciona el tiempo de inicio y fin. Luego, haz clic en el botón buscar.\",\n  \"Root\": \"Raíz\",\n  \"Run Query\": \"Ejecutar Consulta\",\n  \"Search Dependencies\": \"Buscar Dependencias\",\n  \"Search Traces\": \"Buscar Trazas\",\n  \"Searching has been disabled via the searchEnabled property. You can still view specific traces of which you know the trace id by entering it in the \\\"Trace ID...\\\" textbox on the top-right.\": \"\",\n  \"Services\": \"Servicios\",\n  \"Span ID\": \"ID de Tramo\",\n  \"Spans\": \"Tramos\",\n  \"Start Time\": \"Tiempo de Inicio\",\n  \"Support\": \"Soporte\",\n  \"Total Spans\": \"Tramos Totales\",\n  \"Trace ID\": \"ID de Traza\",\n  \"Upload JSON\": \"Subir JSON\",\n  \"View Logs\": \"Ver Logs\",\n  \"Zipkin\": \"\"\n}\n"
  },
  {
    "path": "zipkin-lens/src/translations/fr/messages.d.ts",
    "content": "import type { Messages } from '@lingui/core';\n          declare const messages: Messages;\n          export { messages };\n          "
  },
  {
    "path": "zipkin-lens/src/translations/fr/translations.json",
    "content": "{\n  \"{0} Results\": \"{0} Résultats\",\n  \"1 Result\": \"1 Résultat\",\n  \"Address\": \"Adresse\",\n  \"Archive Trace\": \"Trace Archivée\",\n  \"Dependencies\": \"Dépendances\",\n  \"Download JSON\": \"Télécharger JSON\",\n  \"Duration\": \"Durée\",\n  \"End Time\": \"Fin\",\n  \"Find a trace\": \"Trouver une trace\",\n  \"Limit\": \"Limite\",\n  \"Parent ID\": \"ID Parent\",\n  \"Please select criteria in the search bar. Then, click the search button.\": \"S'il vous plaît sélectionnez un critère dans la barre de recherche. Puis cliquez sur le bouton de recherche\",\n  \"Please select the start and end time. Then, click the search button.\": \"S'il vous plaît sélectionnez la date de début et de fin. Puis cliquez sur le bouton de recherche\",\n  \"Root\": \"Racine\",\n  \"Run Query\": \"Exécuter la requête\",\n  \"Search Dependencies\": \"Rechercher les dépendances\",\n  \"Search Traces\": \"Rechercher les traces\",\n  \"Searching has been disabled via the searchEnabled property. You can still view specific traces of which you know the trace id by entering it in the \\\"Trace ID...\\\" textbox on the top-right.\": \"La recherche a été désactivée via la propriété searchEnabled. Vous pouvez toujours afficher des traces spécifiques dont vous connaissez l'identifiant de trace en le saisissant dans la zone de texte \\\"ID Trace ... \\\" en haut à droite.\",\n  \"Services\": \"Services\",\n  \"Span ID\": \"ID Section\",\n  \"Spans\": \"Sections\",\n  \"Start Time\": \"Début\",\n  \"Support\": \"Support\",\n  \"Total Spans\": \"Sections Total\",\n  \"Trace ID\": \"ID Trace\",\n  \"Upload JSON\": \"Charger un JSON\",\n  \"View Logs\": \"Voir les journaux\",\n  \"Zipkin\": \"Zipkin\"\n}\n"
  },
  {
    "path": "zipkin-lens/src/translations/i18n.ts",
    "content": "import i18n from 'i18next'\nimport {initReactI18next} from \"react-i18next\";\nimport LanguageDetector from 'i18next-browser-languagedetector'\nimport messages_fr from './fr/translations.json'\nimport messages_es from './es/translations.json'\nimport messages_zh from './zh-cn/translations.json'\nimport messages_en from './en/translations.json'\n\nexport const FALLBACK_LOCALE = 'en';\n\nconst resources = {\n  es: {\n    translation: messages_es\n  },\n  fr:{\n    translation: messages_fr\n  },\n  zh_cn: {\n    translation: messages_zh\n  },\n  en:{\n    translation: messages_en\n  }\n}\n\ni18n\n  .use(initReactI18next)\n  .use(LanguageDetector)\n  .init(\n    {\n      resources,\n      fallbackLng: FALLBACK_LOCALE,\n    }\n  )\n\nexport default i18n\n"
  },
  {
    "path": "zipkin-lens/src/translations/zh-cn/messages.d.ts",
    "content": "import type { Messages } from '@lingui/core';\n          declare const messages: Messages;\n          export { messages };\n          "
  },
  {
    "path": "zipkin-lens/src/translations/zh-cn/translations.json",
    "content": "{\n  \"{0} Results\": \"{0} 条结果\",\n  \"1 Result\": \"1 条结果\",\n  \"Address\": \"地址\",\n  \"Archive Trace\": \"存档Trace\",\n  \"Dependencies\": \"依赖\",\n  \"Download JSON\": \"下载JSON数据\",\n  \"Duration\": \"持续时间\",\n  \"End Time\": \"终止时间\",\n  \"Find a trace\": \"找到一个痕迹\",\n  \"Limit\": \"数量\",\n  \"Parent ID\": \"Parent ID\",\n  \"Please select criteria in the search bar. Then, click the search button.\": \"请在搜索栏中选择条件， 然后点击按钮进行查找\",\n  \"Please select the start and end time. Then, click the search button.\": \"请选择起始时间和终止时间， 然后点击按钮进行查找\",\n  \"Root\": \"Root\",\n  \"Run Query\": \"运行查询\",\n  \"Search Dependencies\": \"查询依赖\",\n  \"Search Traces\": \"查询trace链路\",\n  \"Searching has been disabled via the searchEnabled property. You can still view specific traces of which you know the trace id by entering it in the \\\"Trace ID...\\\" textbox on the top-right.\": \"已通过searchEnabled属性禁用搜索，通过在右上角的 \\\"Trace ID...\\\"文本框中输入trace id，您仍然可以查看已知trace id的跟踪信息\",\n  \"Services\": \"服务\",\n  \"Span ID\": \"Span ID\",\n  \"Spans\": \"Spans\",\n  \"Start Time\": \"开始时间\",\n  \"Support\": \"支持\",\n  \"Total Spans\": \"Span总数\",\n  \"Trace ID\": \"Trace ID\",\n  \"Upload JSON\": \"上传JSON数据\",\n  \"View Logs\": \"查看日志\",\n  \"Zipkin\": \"\"\n}\n"
  },
  {
    "path": "zipkin-lens/src/types/redux-thunk.d.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { ThunkAction } from 'redux-thunk';\n\n// Please see: https://github.com/reduxjs/redux-thunk/pull/278\ndeclare module 'redux' {\n  /*\n   * Overload to add thunk support to Redux's dispatch() function.\n   * Useful for react-redux or any other library which could use this type.\n   */\n  export interface Dispatch<A extends Action = AnyAction> {\n    <TReturnType = any, TState = any, TExtraThunkArg = any>(\n      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, A>,\n    ): TReturnType;\n  }\n}\n"
  },
  {
    "path": "zipkin-lens/src/types/styled-components.d.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { Theme } from '@material-ui/core';\nimport 'styled-components';\n\ndeclare module 'styled-components' {\n  // eslint-disable-next-line @typescript-eslint/no-empty-interface\n  export interface DefaultTheme extends Theme {}\n}\n"
  },
  {
    "path": "zipkin-lens/src/types/vizceral-react.d.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\ndeclare module 'vizceral-react' {\n  export default class extends React.Component<any, any> {\n    public vizceral: any;\n  }\n}\n"
  },
  {
    "path": "zipkin-lens/src/util/fetch-resource.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/**\n * Converts a promise to a resource as used in React Suspense nomenclature. A resource provides a\n * read() method which will either suspend rendering until the promise is resolved or return / throw\n * the result of the promise if it is already resolved. This allows React Suspense to continue\n * rendering work while the promise is being resolved.\n */\nexport default (promise) => {\n  let response;\n  let error;\n\n  // In Javascript, there is no way to synchronously know whether a promise is resolved. Even if\n  // it's already resolved, we are guaranteed to suspend once. Since it's unlikely the promise has\n  // resolved at this point anyways, it's not a huge deal though.\n  promise.then(\n    (resp) => {\n      response = resp;\n    },\n    (err) => {\n      error = err;\n    },\n  );\n\n  return {\n    read() {\n      if (error) {\n        throw error;\n      } else if (response) {\n        return response;\n      } else {\n        // Throwing a promise is how to tell React to suspend rendering until it is resolved.\n        throw promise;\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "zipkin-lens/src/util/fetch-resource.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { expect, test } from 'vitest';\nimport fetchResource from './fetch-resource';\n\nconst newPromise = () => {\n  let resolve;\n  let reject;\n  const promise = new Promise((resolve0, reject0) => {\n    resolve = resolve0;\n    reject = reject0;\n  });\n  return { promise, resolve, reject };\n};\n\ntest('promise thrown when not resolved', async () => {\n  const { promise } = newPromise();\n  const resource = fetchResource(promise);\n\n  // The shady practice of throwing a promise does not work well with Jest's matchers, so we extract\n  // the thrown object ourselves.\n  let thrown;\n  try {\n    resource.read();\n  } catch (e) {\n    thrown = e;\n  }\n\n  expect(thrown).toEqual(promise);\n});\n\ntest('response returned after resolved', async () => {\n  const { promise, resolve } = newPromise();\n  const resource = fetchResource(promise);\n\n  resolve('resolved');\n\n  await promise;\n\n  expect(resource.read()).toEqual('resolved');\n});\n\ntest('error thrown after rejected', async () => {\n  const { promise, reject } = newPromise();\n  const resource = fetchResource(promise);\n\n  const error = new Error('rejected');\n  reject(error);\n\n  try {\n    await promise;\n  } catch (e) {\n    // Ignore\n  }\n\n  expect(() => resource.read()).toThrow(error);\n});\n"
  },
  {
    "path": "zipkin-lens/src/util/theme.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst localeThemeKey = 'zipkinThemeOverride';\n\nexport function getTheme(): string {\n  const override = localStorage.getItem(localeThemeKey);\n  if (override) {\n    return override;\n  }\n  return 'light';\n}\n\nexport function setTheme(theme: string) {\n  localStorage.setItem(localeThemeKey, theme);\n}\n"
  },
  {
    "path": "zipkin-lens/src/util/timestamp.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport moment from 'moment';\n\nexport const formatDuration = (duration) => {\n  if (duration === 0 || typeof duration === 'undefined') {\n    return '0ms';\n  }\n  if (duration < 1000) {\n    return `${duration.toFixed(3)}μs`;\n  }\n  if (duration < 1000000) {\n    return `${(duration / 1000).toFixed(3)}ms`;\n  }\n  return `${(duration / 1000000).toFixed(3)}s`;\n};\n\nexport const formatTimestamp = (timestamp) =>\n  moment(timestamp / 1000).format('MM/DD HH:mm:ss.SSS');\n\n// moment.js only supports millisecond precision, however our timestamps have\n// microsecond precision. So we use moment.js to generate the human readable time\n// with just milliseconds and then append the last 3 digits of the timestamp\n// which are the microseconds.\n// NOTE: a.timestamp % 1000 would save a string conversion but drops leading zeros.\nexport const formatTimestampMicros = (timestamp) =>\n  `${formatTimestamp(timestamp)}_${timestamp.toString().slice(-3)}`;\n"
  },
  {
    "path": "zipkin-lens/src/util/trace.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport const ensureV2TraceData = (trace) => {\n  if (!Array.isArray(trace) || trace.length === 0) {\n    throw new Error('input is not a list');\n  }\n  const [first] = trace;\n  if (!first.traceId || !first.id) {\n    throw new Error('List<Span> implies at least traceId and id fields');\n  }\n  if (\n    first.binaryAnnotations ||\n    (!first.localEndpoint && !first.remoteEndpoint && !first.tags)\n  ) {\n    throw new Error(\n      'v1 format is not supported. For help, contact https://gitter.im/openzipkin/zipkin',\n    );\n  }\n};\n\nexport const hasRootSpan = (trace) => {\n  switch (trace.length) {\n    case 0:\n      return false;\n    case 1:\n      return true;\n    default:\n      if (trace[0].depth < trace[1].depth) {\n        return true;\n      }\n      return false;\n  }\n};\n"
  },
  {
    "path": "zipkin-lens/src/util/trace.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport { ensureV2TraceData } from './trace';\nimport v2Trace from '../../testdata/yelp.json';\n\ndescribe('ensureV2', () => {\n  it('does not throw on v2 format', () => {\n    ensureV2TraceData(v2Trace);\n  });\n\n  it('should raise error if not a list', () => {\n    let error;\n    try {\n      ensureV2TraceData();\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toEqual('input is not a list');\n\n    expect(() => {\n      ensureV2TraceData({ traceId: 'a', id: 'b' });\n    }).toThrow(error.message);\n  });\n\n  it('should raise error if missing trace ID or span ID', () => {\n    let error;\n    try {\n      ensureV2TraceData([{ traceId: 'a' }]);\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toEqual(\n      'List<Span> implies at least traceId and id fields',\n    );\n\n    expect(() => {\n      ensureV2TraceData([{ id: 'b' }]);\n    }).toThrow(error.message);\n  });\n\n  it('should raise error if in v1 format', () => {\n    let error;\n    try {\n      ensureV2TraceData([{ traceId: 'a', id: 'b', binaryAnnotations: [] }]);\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toEqual(\n      'v1 format is not supported. For help, contact https://gitter.im/openzipkin/zipkin',\n    );\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/vite-env.d.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/clock-skew.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { SpanNode, SpanNodeBuilder } from './span-node';\n\nclass ClockSkew {\n  constructor(params) {\n    const { endpoint, skew } = params;\n    this._endpoint = endpoint;\n    this._skew = skew;\n  }\n\n  get endpoint() {\n    return this._endpoint;\n  }\n\n  get skew() {\n    return this._skew;\n  }\n}\n\nexport function ipsMatch(a, b) {\n  // export for testing\n  if (!a || !b) return false;\n  if (a.ipv6 && b.ipv6 && a.ipv6 === b.ipv6) {\n    return true;\n  }\n  if (!a.ipv4 && !b.ipv4) return false;\n  return a.ipv4 === b.ipv4;\n}\n\n// If any annotation has an IP with skew associated, adjust accordingly.\nfunction adjustTimestamps(span, skew) {\n  if (!ipsMatch(skew.endpoint, span.localEndpoint)) return span;\n\n  const result = { ...span };\n  if (span.timestamp) result.timestamp = span.timestamp - skew.skew;\n  const annotationLength = span.annotations.length;\n  if (annotationLength > 0) result.annotations = [];\n  for (let i = 0; i < annotationLength; i += 1) {\n    const a = span.annotations[i];\n    result.annotations[i] = {\n      timestamp: a.timestamp - skew.skew,\n      value: a.value,\n    };\n  }\n  return result;\n}\n\n/* Uses span kind to determine if there's clock skew. */\nexport function getClockSkew(node) {\n  // export for testing\n  const parent = node.parent ? node.parent.span : undefined;\n  const child = node.span;\n  if (!parent) return undefined;\n\n  // skew is only detectable client to server\n  if (parent.kind !== 'CLIENT' || child.kind !== 'SERVER') return undefined;\n\n  let oneWay = false;\n  const clientTimestamp = parent.timestamp;\n  const serverTimestamp = child.timestamp;\n  if (!clientTimestamp || !serverTimestamp) return undefined;\n\n  // skew is when the server happens before the client\n  if (serverTimestamp > clientTimestamp) return undefined;\n\n  const clientDuration = parent.duration;\n  const serverDuration = child.duration;\n  if (!clientDuration || !serverDuration) oneWay = true;\n\n  const server = child.localEndpoint;\n  if (!server) return undefined;\n  const client = parent.localEndpoint;\n  if (!client) return undefined;\n\n  // There's no skew if the RPC is going to itself\n  if (ipsMatch(server, client)) return undefined;\n\n  if (oneWay) {\n    const latency = serverTimestamp - clientTimestamp;\n\n    // the only way there is skew is when the client appears to be after the server\n    if (latency > 0) return undefined;\n    // We can't currently do better than push the client and server apart by minimum duration (1)\n    return new ClockSkew({ endpoint: server, skew: latency - 1 });\n  }\n  // If the client finished before the server (async), we still know the server must have happened\n  // after the client. So, push 1us.\n  if (clientDuration < serverDuration) {\n    const skew = serverTimestamp - clientTimestamp - 1;\n    return new ClockSkew({ endpoint: server, skew });\n  }\n\n  // We assume latency is half the difference between the client and server duration.\n  const latency = (clientDuration - serverDuration) / 2;\n\n  // We can't see skew when send happens before receive\n  if (latency < 0) return undefined;\n\n  const skew = serverTimestamp - latency - clientTimestamp;\n  if (skew !== 0) return new ClockSkew({ endpoint: server, skew });\n\n  return undefined;\n}\n\n/*\n * Recursively adjust the timestamps on the span trace. Root span is the reference point, all\n * children's timestamps gets adjusted based on that span's timestamps.\n */\nfunction adjust(node, skewFromParent) {\n  // adjust skew for the endpoint brought over from the parent span\n  if (skewFromParent) {\n    node.setSpan(adjustTimestamps(node.span, skewFromParent));\n  }\n\n  // Is there any skew in the current span?\n  let skew = getClockSkew(node);\n  if (skew) {\n    // the current span's skew may be a different endpoint than its parent, so adjust again.\n    node.setSpan(adjustTimestamps(node.span, skew));\n  } else if (skewFromParent) {\n    // Assumes we are on the same host: propagate skew from our parent\n    skew = skewFromParent;\n  }\n  // propagate skew to any children\n  node.children.forEach((child) => adjust(child, skew));\n}\n\nexport function treeCorrectedForClockSkew(spans, debug = false) {\n  if (spans.length === 0) return new SpanNode();\n\n  const trace = new SpanNodeBuilder({ debug }).build(spans);\n\n  if (!trace.span) {\n    if (debug) {\n      /* eslint-disable no-console */\n      console.log(\n        `skipping clock skew adjustment due to missing root span: traceId=${spans[0].traceId}`,\n      );\n    }\n    return trace;\n  }\n\n  const childrenOfRoot = trace.children;\n  for (let i = 0; i < childrenOfRoot.length; i += 1) {\n    const next = childrenOfRoot[i].span;\n    if (next.parentId || next.shared) continue;\n\n    const { traceId } = next;\n    const spanId = next.id;\n    const rootSpanId = trace.span.id;\n    if (debug) {\n      /* eslint-disable no-console */\n      const prefix = 'skipping redundant root span';\n      console.log(\n        `${prefix}: traceId=${traceId}, rootSpanId=${rootSpanId}, spanId=${spanId}`,\n      );\n    }\n    return trace;\n  }\n\n  adjust(trace);\n  return trace;\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/clock-skew.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport {\n  ipsMatch,\n  getClockSkew,\n  treeCorrectedForClockSkew,\n} from './clock-skew';\nimport { SpanNode, SpanNodeBuilder } from './span-node';\nimport { clean } from './span-cleaner';\nimport skewedTrace from '../test/data/skew';\n\nexport const frontend = {\n  serviceName: 'frontend',\n  ipv4: '172.17.0.13',\n};\n\nexport const backend = {\n  serviceName: 'backend',\n  ipv4: '172.17.0.9',\n};\n\ndescribe('ipsMatch', () => {\n  const ipv6 = { ipv6: '2001:db8::c001' };\n  const ipv4 = { ipv4: '192.168.99.101' };\n  const both = {\n    ipv4: '192.168.99.101',\n    ipv6: '2001:db8::c001',\n  };\n\n  it('IPs should not match when undefined', () => {\n    expect(ipsMatch(undefined, undefined)).toEqual(false);\n    expect(ipsMatch(undefined, ipv4)).toEqual(false);\n    expect(ipsMatch(ipv4, undefined)).toEqual(false);\n  });\n\n  it('IPs should not match unless both sides have an IP', () => {\n    const noIp = { serviceName: 'foo' };\n    expect(ipsMatch(noIp, ipv4)).toEqual(false);\n    expect(ipsMatch(noIp, ipv6)).toEqual(false);\n    expect(ipsMatch(ipv4, noIp)).toEqual(false);\n    expect(ipsMatch(ipv6, noIp)).toEqual(false);\n  });\n\n  it('IPs should not match when IPs are different', () => {\n    const differentIpv6 = { ipv6: '2001:db8::c002' };\n    const differentIpv4 = { ipv4: '192.168.99.102' };\n\n    expect(ipsMatch(differentIpv4, ipv4)).toEqual(false);\n    expect(ipsMatch(differentIpv6, ipv6)).toEqual(false);\n    expect(ipsMatch(ipv4, differentIpv4)).toEqual(false);\n    expect(ipsMatch(ipv6, differentIpv6)).toEqual(false);\n  });\n\n  it('IPs should match when ipv4 or ipv6 match', () => {\n    expect(ipsMatch(ipv4, ipv4)).toEqual(true);\n    expect(ipsMatch(both, ipv4)).toEqual(true);\n    expect(ipsMatch(both, ipv6)).toEqual(true);\n    expect(ipsMatch(ipv6, ipv6)).toEqual(true);\n    expect(ipsMatch(ipv4, both)).toEqual(true);\n    expect(ipsMatch(ipv6, both)).toEqual(true);\n  });\n});\n\ndescribe('getClockSkew', () => {\n  /*\n   * Instrumentation bugs might result in spans that look like clock skew is at play. When skew\n   * appears on the same host, we assume it is an instrumentation bug (rather than make it worse by\n   * adjusting it!)\n   */\n  it('clock skew should only correct across different hosts', () => {\n    const parent = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'CLIENT',\n        localEndpoint: frontend,\n        timestamp: 20,\n        duration: 20,\n      }),\n    );\n    const child = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'SERVER',\n        localEndpoint: frontend,\n        timestamp: 10, // skew\n        duration: 10,\n        shared: true,\n      }),\n    );\n    parent.addChild(child);\n\n    expect(getClockSkew(child)).toBeUndefined();\n  });\n\n  it('clock skew should only exist when client is behind server', () => {\n    const parent = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'CLIENT',\n        localEndpoint: frontend,\n        timestamp: 20,\n        duration: 20,\n      }),\n    );\n    const child = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'SERVER',\n        localEndpoint: backend,\n        timestamp: 30, // no skew\n        duration: 10,\n        shared: true,\n      }),\n    );\n    parent.addChild(child);\n\n    expect(getClockSkew(child)).toBeUndefined();\n  });\n\n  it('clock skew should be attributed to the server endpoint', () => {\n    const parent = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'CLIENT',\n        localEndpoint: frontend,\n        timestamp: 20,\n        duration: 20,\n      }),\n    );\n    const child = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'SERVER',\n        localEndpoint: backend,\n        timestamp: 10, // skew\n        duration: 10,\n        shared: true,\n      }),\n    );\n    parent.addChild(child);\n\n    // Skew correction pushes the server side forward, so the skew endpoint is the server\n    expect(getClockSkew(child).endpoint).toEqual(backend);\n  });\n\n  it('clock skew should be attributed to the server endpoint even if missing shared flag', () => {\n    const parent = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'CLIENT',\n        localEndpoint: frontend,\n        timestamp: 20,\n        duration: 20,\n      }),\n    );\n    const child = new SpanNode(\n      clean({\n        traceId: '1',\n        parentId: '2',\n        id: '3',\n        kind: 'SERVER',\n        localEndpoint: backend,\n        timestamp: 10, // skew\n        duration: 10,\n      }),\n    );\n    parent.addChild(child);\n\n    // Skew correction pushes the server side forward, so the skew endpoint is the server\n    expect(getClockSkew(child).endpoint).toEqual(backend);\n  });\n\n  /*\n   * Skew is relative to the server receive and centered by the difference between the server\n   * duration and the client duration.\n   */\n  it('skew includes half the difference of client and server duration', () => {\n    const client = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      kind: 'CLIENT',\n      localEndpoint: frontend,\n      timestamp: 20,\n      duration: 20,\n    });\n    const server = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      kind: 'SERVER',\n      localEndpoint: backend,\n      timestamp: 10, // skew\n      duration: 10,\n      shared: true,\n    });\n    const parent = new SpanNode(client);\n    const child = new SpanNode(server);\n    parent.addChild(child);\n\n    expect(getClockSkew(child).skew).toEqual(\n      server.timestamp -\n        client.timestamp - // how much the server is behind\n        (client.duration - server.duration) / 2, // center the server by splitting what's left\n    );\n  });\n\n  // Sets the server to 1us past the client\n  it('skew on one-way spans assumes latency is at least 1us', () => {\n    const client = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      kind: 'CLIENT',\n      localEndpoint: frontend,\n      timestamp: 20,\n    });\n    const server = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      kind: 'SERVER',\n      localEndpoint: backend,\n      timestamp: 10, // skew\n      shared: true,\n    });\n    const parent = new SpanNode(client);\n    const child = new SpanNode(server);\n    parent.addChild(child);\n\n    expect(getClockSkew(child).skew).toEqual(\n      server.timestamp -\n        client.timestamp - // how much server is behind\n        1, // assume it takes at least 1us to get to the server\n    );\n  });\n\n  // It is still impossible to reach the server before the client sends a request.\n  it('skew on async server spans assumes latency is at least 1us', () => {\n    const client = clean({\n      traceId: '1',\n      id: '2',\n      kind: 'CLIENT',\n      localEndpoint: frontend,\n      timestamp: 20,\n      duration: 5, // stops before server does\n    });\n    const server = clean({\n      traceId: '1',\n      id: '2',\n      kind: 'SERVER',\n      localEndpoint: backend,\n      timestamp: 10, // skew\n      duration: 10,\n      shared: true,\n    });\n    const parent = new SpanNode(client);\n    const child = new SpanNode(server);\n    parent.addChild(child);\n\n    expect(getClockSkew(child).skew).toEqual(\n      server.timestamp -\n        client.timestamp - // how much server is behind\n        1, // assume it takes at least 1us to get to the server\n    );\n  });\n});\n\n// breadth first traversal, comparing timestamps between the parent and each child\nfunction expectChildrenHappenAfterParent(root) {\n  const queue = root.queueRootMostSpans();\n  while (queue.length > 0) {\n    const parent = queue.shift();\n\n    parent.children.forEach((child) => {\n      expect(child.span.timestamp).toBeGreaterThan(parent.span.timestamp);\n      queue.push(child);\n    });\n  }\n}\n\n// breadth first traversal, comparing timestamps between children\nfunction expectChildrenToBeInOrder(root) {\n  const queue = root.queueRootMostSpans();\n  while (queue.length > 0) {\n    const current = queue.shift();\n\n    const childCount = current.children.length;\n    if (childCount === 0) continue;\n\n    let lastChild = current.children[0].span;\n    for (let i = 1; i < childCount; i += 1) {\n      const currentChild = current.children[i].span;\n      expect(currentChild.timestamp).toBeGreaterThan(lastChild.timestamp);\n      lastChild = currentChild;\n    }\n\n    // process the next level of the tree\n    current.children.forEach((child) => queue.push(child));\n  }\n}\n\n// originally zipkin2.internal.CorrectForClockSkewTest.java\ndescribe('treeCorrectedForClockSkew', () => {\n  it('should correct skew', () => {\n    const corrected = treeCorrectedForClockSkew(skewedTrace);\n    expectChildrenHappenAfterParent(corrected);\n    expectChildrenToBeInOrder(corrected);\n  });\n\n  it('should skip when headless', () => {\n    const headless = [];\n    skewedTrace.forEach((span) => {\n      if (span.parentId) headless.push(span); // everything but root!\n    });\n    const notCorrected = treeCorrectedForClockSkew(headless);\n    expect(notCorrected).toEqual(new SpanNodeBuilder({}).build(headless));\n  });\n\n  it('should skip on duplicate root', () => {\n    const duplicate = [];\n    skewedTrace.forEach((span) => duplicate.push(span));\n    duplicate.push(\n      clean({\n        traceId: skewedTrace[0].traceId,\n        id: 'cafebabe',\n        name: 'curtain',\n      }),\n    );\n    const notCorrected = treeCorrectedForClockSkew(duplicate);\n    expect(notCorrected).toEqual(new SpanNodeBuilder({}).build(duplicate));\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/dependency-linker.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { getServiceName } from './span-row';\n\n// In javascript, dict keys can't be objects\nfunction keyString(parent, child) {\n  return `${parent}░${child}`; // cassandra storage uses the same delimiter\n}\n\nfunction link(callCounts, errorCounts) {\n  const result = [];\n  Object.keys(callCounts).forEach((key) => {\n    const parentChild = key.split('░');\n    const nextLink = {\n      parent: parentChild[0],\n      child: parentChild[1],\n      callCount: callCounts[key],\n    };\n\n    const errorCount = errorCounts[key] || 0;\n    if (errorCount) nextLink.errorCount = errorCount;\n    result.push(nextLink);\n  });\n  return result;\n}\n\n/*\n * A dependency link is an edge between two services. This parses a span tree into dependency links\n * used by Web UI. Ex. http://zipkin/dependency\n *\n * This implementation traverses the tree, and creates links for RPC and messaging spans. RPC links\n * are only between SERVER spans. One exception is at the bottom of the trace tree. CLIENT spans\n * that record their remoteEndpoint are included, as this accounts for uninstrumented services.\n * Spans with kind unset, but remoteEndpoint set are treated the same as client spans.\n */\nexport class DependencyLinker {\n  constructor(params) {\n    const { debug = false } = params;\n    this._debug = debug;\n    this._callCounts = {}; // parent_child -> callCount\n    this._errorCounts = {}; // parent_child -> errorCount\n  }\n\n  _firstRemoteAncestor(node) {\n    let ancestor = node.parent;\n    while (ancestor) {\n      const maybeRemote = ancestor.span;\n      if (maybeRemote && maybeRemote.kind) {\n        if (this._debug)\n          console.log(`found remote ancestor ${JSON.stringify(maybeRemote)}`);\n        return maybeRemote;\n      }\n      ancestor = ancestor.parent;\n    }\n    return undefined;\n  }\n\n  _addLink(parent, child, isError) {\n    if (this.debug) {\n      console.log(\n        `incrementing ${isError ? 'error ' : ''}link ${parent} -> ${child}`,\n      ); // eslint-disable-line prefer-template\n    }\n    const key = keyString(parent, child);\n    this._callCounts[key] = (this._callCounts[key] || 0) + 1;\n    if (!isError) return;\n    this._errorCounts[key] = (this._errorCounts[key] || 0) + 1;\n  }\n\n  /** The input should be a root span node, not a list of spans. */\n  putTrace(traceTree) {\n    const debug = this._debug;\n    if (debug) console.log('traversing trace tree, breadth-first');\n    const queue = traceTree.queueRootMostSpans();\n    while (queue.length > 0) {\n      const current = queue.shift();\n      current.children.forEach((n) => queue.push(n));\n\n      const currentSpan = current.span;\n      if (debug) console.log(`processing ${JSON.stringify(currentSpan)}`);\n\n      let { kind } = currentSpan;\n      // When processing links to a client span, we prefer the server's name. If we have no child\n      // spans, we proceed to use the name the client chose.\n      if (kind === 'CLIENT' && current.children.length > 0) {\n        continue;\n      }\n\n      const serviceName = getServiceName(currentSpan.localEndpoint);\n      const remoteServiceName = getServiceName(currentSpan.remoteEndpoint);\n      if (!kind) {\n        // Treat unknown type of span as a client span if we know both sides\n        if (serviceName && remoteServiceName) {\n          kind = 'CLIENT';\n        } else {\n          if (debug) console.log('non remote span; skipping');\n          continue;\n        }\n      }\n\n      let child;\n      let parent;\n      switch (kind) {\n        case 'SERVER':\n        case 'CONSUMER':\n          child = serviceName;\n          parent = remoteServiceName;\n          if (current === traceTree) {\n            // we are the root-most span.\n            if (!parent) {\n              if (debug)\n                console.log('The client of the root span is unknown; skipping');\n              continue;\n            }\n          }\n          break;\n        case 'CLIENT':\n        case 'PRODUCER':\n          parent = serviceName;\n          child = remoteServiceName;\n          break;\n        default:\n          if (debug) console.log('unknown kind; skipping');\n          continue;\n      }\n\n      let isError = currentSpan.tags.error !== undefined;\n      if (kind === 'PRODUCER' || kind === 'CONSUMER') {\n        if (!parent || !child) {\n          if (debug)\n            console.log('cannot link messaging span to its broker; skipping');\n        } else {\n          this._addLink(parent, child, isError);\n        }\n        continue;\n      }\n\n      // Local spans may be between the current node and its remote parent\n      const remoteAncestor = this._firstRemoteAncestor(current);\n      let remoteAncestorName;\n      if (remoteAncestor)\n        remoteAncestorName = getServiceName(remoteAncestor.localEndpoint);\n      if (remoteAncestor && remoteAncestorName) {\n        // Some users accidentally put the remote service name on client annotations.\n        // Check for this and backfill a link from the nearest remote to that service as necessary.\n        if (\n          kind === 'CLIENT' &&\n          serviceName &&\n          remoteAncestorName !== serviceName\n        ) {\n          if (debug) console.log('detected missing link to client span');\n          this._addLink(remoteAncestorName, serviceName, false); // we don't know if it is an error\n        }\n\n        if (kind === 'SERVER' || !parent) parent = remoteAncestorName;\n\n        // When an RPC is split between spans, we skip the child (server side). If our parent is a\n        // client, we need to check it for errors.\n        if (\n          !isError &&\n          remoteAncestor.kind === 'CLIENT' &&\n          currentSpan.parentId &&\n          currentSpan.parentId === remoteAncestor.id\n        ) {\n          isError = isError || remoteAncestor.tags.error !== undefined;\n        }\n      }\n\n      if (!parent || !child) {\n        if (debug) console.log('cannot find remote ancestor; skipping');\n        continue;\n      }\n\n      this._addLink(parent, child, isError);\n    }\n  }\n\n  link() {\n    return link(this._callCounts, this._errorCounts);\n  }\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/dependency-linker.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { DependencyLinker } from './dependency-linker';\nimport { SpanNodeBuilder } from './span-node';\n\nconst debug = false; // switch to enable console output during tests\n// in reverse order as reporting is more likely to occur this way\nconst trace = [\n  {\n    traceId: 'a',\n    parentId: 'b',\n    id: 'c',\n    kind: 'CLIENT',\n    localEndpoint: { serviceName: 'app' },\n    remoteEndpoint: { serviceName: 'db' },\n    tags: { error: true },\n  },\n  {\n    traceId: 'a',\n    parentId: 'a',\n    id: 'b',\n    kind: 'SERVER',\n    localEndpoint: { serviceName: 'app' },\n    remoteEndpoint: { serviceName: 'web' },\n    shared: true,\n  },\n  {\n    traceId: 'a',\n    parentId: 'a',\n    id: 'b',\n    kind: 'CLIENT',\n    localEndpoint: { serviceName: 'web' },\n    remoteEndpoint: { serviceName: 'app' },\n  },\n  {\n    traceId: 'a',\n    id: 'a',\n    kind: 'SERVER',\n    localEndpoint: { serviceName: 'web' },\n  },\n];\n\n// originally zipkin2.internal.DependencyLinkerTest.java\ndescribe('DependencyLinker', () => {\n  let dependencyLinker;\n\n  beforeEach(() => {\n    dependencyLinker = new DependencyLinker({ debug });\n  });\n\n  function putTrace(spans) {\n    dependencyLinker.putTrace(new SpanNodeBuilder({ debug }).build(spans));\n  }\n\n  it('should return empty by default', () => {\n    expect(dependencyLinker.link()).toEqual([]);\n  });\n\n  it('should link a trace', () => {\n    putTrace(trace);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 1 },\n      {\n        parent: 'app',\n        child: 'db',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  it('sum calls for each call to putTrace', () => {\n    putTrace(trace);\n    putTrace(trace);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 2 },\n      {\n        parent: 'app',\n        child: 'db',\n        callCount: 2,\n        errorCount: 2,\n      },\n    ]);\n  });\n\n  function withLateParent() {\n    const result = trace.slice(0);\n    const missingParent = { ...trace[2] };\n    delete missingParent.parentId;\n    result[2] = missingParent;\n    return result;\n  }\n\n  /*\n   * This test shows that if a parent ID is stored late (ex because it wasn't propagated), the span\n   * can resolve once it is.\n   */\n  it('should link spans when server is missing its parentId', () => {\n    putTrace(withLateParent());\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 1 },\n      {\n        parent: 'app',\n        child: 'db',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  /*\n   * This test shows that if a parent ID is stored late (ex because it wasn't propagated), the span\n   * can resolve even if the client side is never sent.\n   */\n  it('should link spans when client it never sent', () => {\n    putTrace(withLateParent().filter((s) => s !== trace[1])); // client span never sent\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 1 },\n      {\n        parent: 'app',\n        child: 'db',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  it('should link messaging spans by broker', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n        remoteEndpoint: { serviceName: 'kafka' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n        remoteEndpoint: { serviceName: 'kafka' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'producer', child: 'kafka', callCount: 1 },\n      { parent: 'kafka', child: 'consumer', callCount: 1 },\n    ]);\n  });\n\n  it('should not conflate messaging when they have different brokers', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n        remoteEndpoint: { serviceName: 'kafka1' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n        remoteEndpoint: { serviceName: 'kafka2' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'producer', child: 'kafka1', callCount: 1 },\n      { parent: 'kafka2', child: 'consumer', callCount: 1 },\n    ]);\n  });\n\n  it('should not link messaging spans missing broker', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([]);\n  });\n\n  /* Shows we don't assume there's a direct link between producer and consumer. */\n  it('should not link producer spans when missing broker', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n        remoteEndpoint: { serviceName: 'kafka' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'kafka', child: 'consumer', callCount: 1 },\n    ]);\n  });\n\n  it('should not link consumer spans when missing broker', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n        remoteEndpoint: { serviceName: 'kafka' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'producer', child: 'kafka', callCount: 1 },\n    ]);\n  });\n\n  /* When a server is the child of a producer span, make a link as it is really an RPC */\n  it('should interpret producer -> server as RPC', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'producer', child: 'server', callCount: 1 },\n    ]);\n  });\n\n  /*\n   * Servers most often join a span vs create a child. Make sure this works when a producer is used\n   * instead of a client.\n   */\n  it('should interpret producer -> server as RPC, even sharing ID', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'PRODUCER',\n        localEndpoint: { serviceName: 'producer' },\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n        shared: true,\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'producer', child: 'server', callCount: 1 },\n    ]);\n  });\n\n  /*\n   * Client might be used for historical reasons instead of PRODUCER. Don't link as the server-side\n   * is authoritative.\n   */\n  it('should not interpret client as producer', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CONSUMER',\n        localEndpoint: { serviceName: 'consumer' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([]);\n  });\n\n  /*\n   * A root span can be a client-originated trace or a server receipt which knows its peer. In these\n   * cases, the peer is known and kind establishes the direction.\n   */\n  it('should link spans directed by kind', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n        remoteEndpoint: { serviceName: 'client' },\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n        remoteEndpoint: { serviceName: 'server' },\n        shared: true,\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'client', child: 'server', callCount: 1 },\n    ]);\n  });\n\n  it('link calls to uninstrumented servers', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'frontend', child: 'backend', callCount: 2 },\n    ]);\n  });\n\n  it('link calls to uninstrumented servers, including errors', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n        tags: { error: '' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'frontend',\n        child: 'backend',\n        callCount: 2,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  it('link incoming calls, using last RPC parent as service name', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'c',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        tags: { error: '' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'frontend',\n        child: 'backend',\n        callCount: 2,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  /*\n   * Spans don't always include both the client and server service. When you know the kind, you can\n   * link these without duplicating call count.\n   */\n  it('should link single host spans as one call', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'client', child: 'server', callCount: 1 },\n    ]);\n  });\n\n  it('should link single host spans as one error', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n        tags: { error: '' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n        tags: { error: '' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'client',\n        child: 'server',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  it('should link shared RPC span as one error', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n        tags: { error: '' },\n      },\n      {\n        traceId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n        tags: { error: '' },\n        shared: true,\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'client',\n        child: 'server',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  it('should prefer server name in RPC link', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n        remoteEndpoint: { serviceName: 'elephant' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'client',\n        child: 'server',\n        callCount: 1,\n      },\n    ]);\n  });\n\n  /**\n   * Spans are sometimes intermediated by an unknown type of span. Prefer the nearest server when\n   * accounting for them.\n   */\n  it('tolerates missing localEndpoint between server and client', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'd',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        remoteEndpoint: { serviceName: 'backend' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'frontend', child: 'backend', callCount: 2 },\n    ]);\n  });\n\n  it('should not link leaf nodes when remote service name is unknown', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'd',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([]);\n  });\n\n  it('should create links when missing intermediate endpoint data', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b', // possibly missing client/server span\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'backend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'd',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'backend' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'frontend', child: 'backend', callCount: 2 },\n    ]);\n  });\n\n  it('should not attribute errors from uninstrumented links', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      // missing rpc span between here\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'backend' },\n        tags: { error: '' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'd',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'backend' },\n        tags: { error: '' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'frontend', child: 'backend', callCount: 2 },\n    ]);\n  });\n\n  /* Tag indicates a failed span, not an annotation */\n  it('should not count annotation error as errorCount', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'client' },\n        annotations: [{ timestamp: 1, value: 'error' }],\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server' },\n        annotations: [{ timestamp: 1, value: 'error' }],\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      {\n        parent: 'client',\n        child: 'server',\n        callCount: 1,\n      },\n    ]);\n  });\n\n  it('should link loopback', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'frontend', child: 'frontend', callCount: 1 },\n    ]);\n  });\n\n  it('should treat remote service names missing kind as RPC', () => {\n    putTrace([\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        localEndpoint: { serviceName: 'web' },\n        remoteEndpoint: { serviceName: 'app' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        localEndpoint: { serviceName: 'app' },\n        remoteEndpoint: { serviceName: 'db' },\n        tags: { error: true },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 1 },\n      {\n        parent: 'app',\n        child: 'db',\n        callCount: 1,\n        errorCount: 1,\n      },\n    ]);\n  });\n\n  /* We cannot link if we don't know both service names. */\n  it('should not link root RPC spans missing both service names', () => {\n    [\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        remoteEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'CLIENT',\n        remoteEndpoint: { serviceName: 'frontend' },\n      },\n    ].forEach((root) => {\n      putTrace([root]);\n      expect(dependencyLinker.link()).toEqual([]);\n    });\n  });\n\n  it('should not link peer spans on different hosts missing parentID', () => {\n    const parentId = 'a'; // missing\n    putTrace([\n      {\n        traceId: 'a',\n        parentId,\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server1' },\n      },\n      {\n        traceId: 'a',\n        parentId,\n        id: 'c',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'server2' },\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([]);\n  });\n\n  it('should tolerate missing root span', () => {\n    const parentId = 'a'; // missing\n    putTrace([\n      {\n        traceId: 'a',\n        parentId,\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'web' },\n        remoteEndpoint: { serviceName: 'app' },\n      },\n      {\n        traceId: 'a',\n        parentId,\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'app' },\n        remoteEndpoint: { serviceName: 'web' },\n        shared: true,\n      },\n    ]);\n\n    expect(dependencyLinker.link()).toEqual([\n      { parent: 'web', child: 'app', callCount: 1 },\n    ]);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/index.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nexport { treeCorrectedForClockSkew } from './clock-skew';\nexport { getServiceName } from './span-row';\nexport { traceSummary, traceSummaries, detailedTraceSummary } from './trace';\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-cleaner.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport isEqual from 'lodash/isEqual';\nimport sortBy from 'lodash/sortBy';\nimport unionWith from 'lodash/unionWith';\n\nexport function normalizeTraceId(traceId) {\n  if (traceId.length > 16) {\n    const result = traceId.padStart(32, '0');\n    // undo prefix if it will result in a 64-bit trace ID\n    if (result.startsWith('0000000000000000')) return result.substring(16);\n    return result;\n  }\n  return traceId.padStart(16, '0');\n}\n\nfunction isEndpoint(endpoint) {\n  return endpoint && Object.keys(endpoint).length > 0;\n}\n\n// This cleans potential dirty v2 inputs, like normalizing IDs etc. It does not affect the input\nexport function clean(span) {\n  const res = {\n    traceId: normalizeTraceId(span.traceId),\n  };\n\n  // take care not to create self-referencing spans even if the input data is incorrect\n  const id = span.id.padStart(16, '0');\n  if (span.parentId) {\n    const parentId = span.parentId.padStart(16, '0');\n    if (parentId !== id) res.parentId = parentId;\n  }\n  res.id = id;\n\n  if (span.name && span.name !== '' && span.name !== 'unknown')\n    res.name = span.name;\n  if (span.kind) res.kind = span.kind;\n\n  if (span.timestamp) res.timestamp = span.timestamp;\n  if (span.duration) res.duration = span.duration;\n\n  if (isEndpoint(span.localEndpoint))\n    res.localEndpoint = { ...span.localEndpoint };\n  if (isEndpoint(span.remoteEndpoint))\n    res.remoteEndpoint = { ...span.remoteEndpoint };\n\n  res.annotations = span.annotations ? span.annotations.slice(0) : [];\n  if (res.annotations.length > 1) {\n    res.annotations = sortBy(unionWith(res.annotations, isEqual), [\n      'timestamp',\n      'value',\n    ]);\n  }\n\n  res.tags = span.tags || {};\n\n  if (span.debug) res.debug = true;\n  // shared is for the server side, unset it if accidentally set on the client side\n  if (span.shared && span.kind !== 'CLIENT') res.shared = true;\n\n  return res;\n}\n\n// exposed for testing. assumes spans are already clean\nexport function merge(left, right) {\n  const res = {\n    traceId: right.traceId.length > 16 ? right.traceId : left.traceId,\n  };\n\n  const parentId = left.parentId || right.parentId;\n  if (parentId) res.parentId = parentId;\n\n  res.id = left.id;\n  const name = left.name || right.name;\n  if (name) res.name = name;\n\n  const kind = left.kind || right.kind;\n  if (kind) res.kind = kind;\n\n  const timestamp = left.timestamp || right.timestamp;\n  if (timestamp) res.timestamp = timestamp;\n  const duration = left.duration || right.duration;\n  if (duration) res.duration = duration;\n\n  const localEndpoint = { ...left.localEndpoint, ...right.localEndpoint };\n  if (isEndpoint(localEndpoint)) res.localEndpoint = localEndpoint;\n  const remoteEndpoint = { ...left.remoteEndpoint, ...right.remoteEndpoint };\n  if (isEndpoint(remoteEndpoint)) res.remoteEndpoint = remoteEndpoint;\n\n  if (left.annotations.length === 0) {\n    res.annotations = right.annotations;\n  } else if (right.annotations.length === 0) {\n    res.annotations = left.annotations;\n  } else {\n    res.annotations = sortBy(\n      unionWith(left.annotations, right.annotations, isEqual),\n      ['timestamp', 'value'],\n    );\n  }\n\n  res.tags = { ...left.tags, ...right.tags };\n\n  if (left.debug || right.debug) res.debug = true;\n  if (left.shared || right.shared) res.shared = true;\n  return res;\n}\n\n// compares potentially undefined input\nexport function compare(a, b) {\n  if (!a && !b) return 0;\n  if (!a) return -1;\n  if (!b) return 1;\n  return (a > b) - (a < b);\n}\n\nfunction isUndefined(ref) {\n  return typeof ref === 'undefined';\n}\n\n/*\n * Put spans with null endpoints first, so that their data can be attached to the first span with\n * the same ID and endpoint. It is possible that a server can get the same request on a different\n * port. Not addressing this.\n */\nfunction compareEndpoint(left, right) {\n  // handle nulls first\n  if (isUndefined(left)) return -1;\n  if (isUndefined(right)) return 1;\n\n  const byService = compare(left.serviceName, right.serviceName);\n  if (byService !== 0) return byService;\n  const byIpV4 = compare(left.ipv4, right.ipv4);\n  if (byIpV4 !== 0) return byIpV4;\n  return compare(left.ipv6, right.ipv6);\n}\n\n// false or null first (client first)\nfunction compareShared(left, right) {\n  const leftNotShared = isUndefined(left.shared) || !left.shared;\n  const rightNotShared = isUndefined(right.shared) || !right.shared;\n\n  if (leftNotShared && rightNotShared) {\n    return left.kind === 'CLIENT' ? -1 : 1;\n  }\n  if (leftNotShared) return -1;\n  if (rightNotShared) return 1;\n  return 0;\n}\n\nexport function cleanupComparator(left, right) {\n  // exported for testing\n  const bySpanId = compare(left.id, right.id);\n  if (bySpanId !== 0) return bySpanId;\n  const byShared = compareShared(left, right);\n  if (byShared !== 0) return byShared;\n  return compareEndpoint(left.localEndpoint, right.localEndpoint);\n}\n\nfunction tryMerge(current, endpoint) {\n  if (!endpoint) return true;\n  if (\n    current.serviceName &&\n    endpoint.serviceName &&\n    current.serviceName !== endpoint.serviceName\n  ) {\n    return false;\n  }\n  if (current.ipv4 && endpoint.ipv4 && current.ipv4 !== endpoint.ipv4) {\n    return false;\n  }\n  if (current.ipv6 && endpoint.ipv6 && current.ipv6 !== endpoint.ipv6) {\n    return false;\n  }\n  if (current.port && endpoint.port && current.port !== endpoint.port) {\n    return false;\n  }\n  if (!current.serviceName) {\n    current.serviceName = endpoint.serviceName; // eslint-disable-line no-param-reassign\n  }\n  if (!current.ipv4) current.ipv4 = endpoint.ipv4; // eslint-disable-line no-param-reassign\n  if (!current.ipv6) current.ipv6 = endpoint.ipv6; // eslint-disable-line no-param-reassign\n  if (!current.port) current.port = endpoint.portAsInt; // eslint-disable-line no-param-reassign\n  return true;\n}\n\n// sort by timestamp, then name, root/shared first in case of skew\nexport function spanComparator(a, b) {\n  // exported for testing\n  if (!a.parentId && b.parentId) {\n    // a is root\n    return -1;\n  }\n  if (a.parentId && !b.parentId) {\n    // b is root\n    return 1;\n  }\n\n  // order client first in case of shared spans (shared is always server)\n  if (a.id === b.id) return compareShared(a, b);\n\n  // Either a and b are root or neither are. sort by shared timestamp, then name\n  return compare(a.timestamp, b.timestamp) || compare(a.name, b.name);\n}\n\n/*\n * Spans can be sent in multiple parts. Also client and server spans can share the same ID. This\n * merges both scenarios.\n */\nexport function mergeV2ById(spans) {\n  let { length } = spans;\n  if (length === 0) return spans;\n\n  const result = [];\n\n  // Let's cleanup any spans and pick the longest ID\n  let traceId;\n  spans.forEach((span) => {\n    const cleaned = clean(span);\n    if (!traceId || traceId.length !== 32) traceId = cleaned.traceId;\n    result.push(cleaned);\n  });\n\n  if (length <= 1) return result;\n  result.sort(cleanupComparator);\n\n  // Now start any fixes or merging\n  let last;\n  for (let i = 0; i < length; i += 1) {\n    let span = result[i];\n\n    // Choose the longest trace ID\n    if (span.traceId.length !== traceId.length) {\n      span.traceId = traceId;\n    }\n\n    const localEndpoint = span.localEndpoint ? { ...span.localEndpoint } : {};\n    while (i + 1 < length) {\n      const next = result[i + 1];\n      if (next.id !== span.id) break;\n\n      // This cautiously merges with the next span, if we think it was sent in multiple pieces.\n      if (\n        span.shared === next.shared &&\n        tryMerge(localEndpoint, next.localEndpoint)\n      ) {\n        span = merge(span, next);\n\n        // remove the merged element\n        length -= 1;\n        result.splice(i + 1, 1);\n        continue;\n      }\n      break;\n    }\n\n    // Zipkin and B3 originally used the same span ID between client and server. Some\n    // instrumentation are inconsistent about adding the shared flag on the server side. Since we\n    // have the entire trace, and it is ordered client-first, we can correct a missing shared flag.\n    if (last && last.id === span.id) {\n      // Backfill missing shared flag as some instrumentation doesn't add it\n      if (last.kind === 'CLIENT' && span.kind === 'SERVER' && !span.shared) {\n        span.shared = true;\n      }\n\n      // handle a shared RPC server span that wasn't propagated its parent span ID\n      if (span.shared && !span.parentId && last.parentId) {\n        span.parentId = last.parentId;\n      }\n    }\n\n    last = span;\n    result[i] = span;\n  }\n\n  const sorted = result.sort(spanComparator);\n\n  // Look to see if there is a root span, in case we need to correct its shared flag\n  if (sorted[0].parentId || !sorted[0].shared) return sorted;\n\n  // Sorting puts root spans first. If there's only one root, remove any shared flag.\n  if (sorted.length === 1 || sorted[1].parentId) {\n    delete sorted[0].shared;\n  }\n\n  return sorted;\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-cleaner.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport { clean, cleanupComparator, merge, mergeV2ById } from './span-cleaner';\nimport yelpTrace from '../../testdata/yelp.json';\n\n// endpoints from zipkin2.TestObjects\nconst frontend = {\n  serviceName: 'frontend',\n  ipv4: '127.0.0.1',\n  port: 8080,\n};\n\nconst backend = {\n  serviceName: 'backend',\n  ipv4: '192.168.99.101',\n  port: 9000,\n};\n\nconst clientSpan = {\n  traceId: '0000000000000001',\n  id: '0000000000000002',\n  name: 'get',\n  kind: 'CLIENT',\n  timestamp: 1472470996199000,\n  duration: 207000,\n  localEndpoint: frontend,\n  remoteEndpoint: backend,\n  annotations: [\n    { timestamp: 1472470996238000, value: 'ws' },\n    { timestamp: 1472470996403000, value: 'wr' },\n  ],\n  tags: {\n    'http.path': '/api',\n    'clnt/finagle.version': '6.45.0',\n  },\n};\n\nconst serverSpan = {\n  traceId: '0000000000000001',\n  id: '0000000000000002',\n  kind: 'SERVER',\n  timestamp: 1472470996308713,\n  duration: 10319,\n  localEndpoint: backend,\n  remoteEndpoint: frontend,\n  annotations: [],\n  tags: {},\n  shared: true,\n};\n\nconst oneOfEach = {\n  // has every field set\n  traceId: '7180c278b62e8f6a216a2aea45d08fc9',\n  parentId: '0000000000000001',\n  id: '0000000000000002',\n  kind: 'SERVER',\n  name: 'get',\n  timestamp: 1,\n  duration: 3,\n  localEndpoint: backend,\n  remoteEndpoint: frontend,\n  annotations: [{ timestamp: 2, value: 'foo' }],\n  tags: { 'http.path': '/api' },\n  shared: true,\n  debug: true,\n};\n\ndescribe('clean', () => {\n  it('should remove shared flag if set on client', () => {\n    const cleaned = clean({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      kind: 'CLIENT',\n      shared: true,\n    });\n\n    expect(cleaned.shared).toBeUndefined();\n  });\n});\n\ndescribe('merge', () => {\n  it('should work on redundant data', () => {\n    const merged = merge(oneOfEach, oneOfEach);\n    expect(merged).toEqual(oneOfEach);\n  });\n\n  it('should merge flags', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: {},\n        debug: true,\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      annotations: [],\n      tags: {},\n      debug: true,\n      shared: true,\n    });\n  });\n\n  it('should merge annotations', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [\n          { timestamp: 1, value: 'a' },\n          { timestamp: 1, value: 'b' },\n        ],\n        tags: {},\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [\n          { timestamp: 1, value: 'b' },\n          { timestamp: 1, value: 'c' },\n        ],\n        tags: {},\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      annotations: [\n        { timestamp: 1, value: 'a' },\n        { timestamp: 1, value: 'b' },\n        { timestamp: 1, value: 'c' },\n      ],\n      tags: {},\n    });\n  });\n\n  it('should merge tags', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: { 1: 'a', 2: 'a' },\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: { 2: 'a', 3: 'a' },\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      annotations: [],\n      tags: { 1: 'a', 2: 'a', 3: 'a' },\n    });\n  });\n\n  it('should merge local endpoint', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        localEndpoint: frontend,\n        annotations: [],\n        tags: {},\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      localEndpoint: frontend,\n      annotations: [],\n      tags: {},\n    });\n  });\n\n  it('should merge local endpoint - partial', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        localEndpoint: { serviceName: 'a' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        localEndpoint: { ipv4: '192.168.99.101' },\n        annotations: [],\n        tags: {},\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      localEndpoint: { serviceName: 'a', ipv4: '192.168.99.101' },\n      annotations: [],\n      tags: {},\n    });\n  });\n\n  it('should merge remote endpoint', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        remoteEndpoint: frontend,\n        annotations: [],\n        tags: {},\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      remoteEndpoint: frontend,\n      annotations: [],\n      tags: {},\n    });\n  });\n\n  it('should merge remote endpoint - partial', () => {\n    const merged = merge(\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        remoteEndpoint: { serviceName: 'a' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        remoteEndpoint: { ipv4: '192.168.99.101' },\n        annotations: [],\n        tags: {},\n      },\n    );\n\n    expect(merged).toEqual({\n      traceId: '0000000000000001',\n      id: '0000000000000002',\n      remoteEndpoint: { serviceName: 'a', ipv4: '192.168.99.101' },\n      annotations: [],\n      tags: {},\n    });\n  });\n});\n\ndescribe('mergeV2ById', () => {\n  it('should cleanup spans', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '22222222222222222', // longer than 64-bit\n        parentId: 'a',\n        id: '3',\n        name: '', // empty name should be scrubbed\n        duration: 0, // zero duration should be scrubbed\n      },\n      {\n        traceId: '22222222222222222',\n        parentId: 'a',\n        remoteEndpoint: {}, // empty\n        id: 'a', // self-referencing\n        kind: 'SERVER',\n        timestamp: 1,\n        duration: 10,\n        localEndpoint: frontend,\n      },\n      {\n        traceId: '22222222222222222',\n        parentId: 'a',\n        id: 'b',\n        timestamp: 2,\n        kind: 'CLIENT',\n        name: 'unknown', // unknown name should be scrubbed\n        localEndpoint: frontend,\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '00000000000000022222222222222222',\n        id: '000000000000000a',\n        kind: 'SERVER',\n        timestamp: 1,\n        duration: 10,\n        localEndpoint: {\n          serviceName: 'frontend',\n          ipv4: '127.0.0.1',\n          port: 8080,\n        },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '00000000000000022222222222222222',\n        parentId: '000000000000000a',\n        id: '0000000000000003',\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '00000000000000022222222222222222',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'CLIENT',\n        timestamp: 2,\n        localEndpoint: {\n          serviceName: 'frontend',\n          ipv4: '127.0.0.1',\n          port: 8080,\n        },\n        annotations: [],\n        tags: {},\n      },\n    ]);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.mergeTraceIdHigh\n  it('should prefer 128bit trace ID', () => {\n    const left = {\n      traceId: '463ac35c9f6413ad48485a3953bb6124',\n      id: '3',\n    };\n\n    const right = {\n      traceId: '48485a3953bb6124',\n      id: '3',\n    };\n\n    const leftFirst = mergeV2ById([left, right]);\n    const rightFirst = mergeV2ById([right, left]);\n\n    [leftFirst, rightFirst].forEach((spans) => {\n      spans.forEach((span) => expect(span.traceId).toEqual(left.traceId));\n    });\n  });\n\n  /*\n   * This test shows that if a parent ID is stored late (ex because it wasn't propagated), it can be\n   * backfilled during cleanup.\n   */\n  it('should backfill missing parent id on shared span', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        shared: true,\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        id: '000000000000000a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n    ]);\n  });\n\n  // some instrumentation send 64-bit length, while others 128-bit or padded.\n  it('should merge mixed-length IDs', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '11111111111111112222222222222222', // 128-bit\n        id: '000000000000000a',\n      },\n      {\n        traceId: '00000000000000002222222222222222', // padded\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n      },\n      {\n        traceId: '2222222222222222', // truncated\n        parentId: '000000000000000b',\n        id: '000000000000000c',\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '11111111111111112222222222222222',\n        id: '000000000000000a',\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '11111111111111112222222222222222',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '11111111111111112222222222222222',\n        parentId: '000000000000000b',\n        id: '000000000000000c',\n        annotations: [],\n        tags: {},\n      },\n    ]);\n  });\n\n  /* Let's pretend people use crappy data, but only on the first hop. */\n  it('should merge when missing endpoints', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        id: 'a',\n        tags: { 'span.kind': 'SERVER', service: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        timestamp: 1,\n        tags: { 'span.kind': 'CLIENT', service: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        duration: 10,\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        id: '000000000000000a',\n        annotations: [],\n        tags: { 'span.kind': 'SERVER', service: 'frontend' },\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        timestamp: 1,\n        duration: 10,\n        annotations: [],\n        tags: { 'span.kind': 'CLIENT', service: 'frontend' },\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n    ]);\n  });\n\n  /*\n   * If a client request is proxied by something that does transparent retried. It can be the case\n   * that two servers share the same ID (accidentally!)\n   */\n  it('should not merge shared spans on different IPs', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        timestamp: 1,\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.5' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        duration: 10,\n        localEndpoint: { serviceName: 'frontend' },\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        id: '000000000000000a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'CLIENT',\n        timestamp: 1,\n        duration: 10,\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.5' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n    ]);\n  });\n\n  // Same as above, but the late reported data has no parent id or endpoint\n  it('should put random data on first span with endpoint', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        timestamp: 1,\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.5' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        id: 'b',\n        duration: 10,\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        id: '000000000000000a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'CLIENT',\n        timestamp: 1,\n        duration: 10,\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.5' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n    ]);\n  });\n\n  // not a good idea to send parts of a local endpoint separately, but this helps ensure data isn't\n  // accidentally partitioned in a overly fine grain\n  it('should merge incomplete endpoints', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        id: 'a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        localEndpoint: { ipv4: '1.2.3.4' },\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: 'a',\n        id: 'b',\n        localEndpoint: { ipv4: '1.2.3.5' },\n        shared: true,\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        id: '000000000000000a',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'frontend' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'CLIENT',\n        localEndpoint: { serviceName: 'frontend', ipv4: '1.2.3.4' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: '000000000000000a',\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.5' },\n        annotations: [],\n        tags: {},\n        shared: true,\n      },\n    ]);\n  });\n\n  // spans are reported depth first, so it is possible to see incomplete trees with no root.\n  it('should work when missing root span', () => {\n    const missingParentId = '000000000000000a';\n    const spans = mergeV2ById([\n      {\n        traceId: 'a',\n        parentId: missingParentId,\n        id: 'b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n      },\n      {\n        traceId: 'a',\n        parentId: missingParentId,\n        id: 'c',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '000000000000000a',\n        parentId: missingParentId,\n        id: '000000000000000b',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend', ipv4: '1.2.3.4' },\n        annotations: [],\n        tags: {},\n      },\n      {\n        traceId: '000000000000000a',\n        parentId: missingParentId,\n        id: '000000000000000c',\n        kind: 'SERVER',\n        localEndpoint: { serviceName: 'backend' },\n        annotations: [],\n        tags: {},\n      },\n    ]);\n  });\n\n  it('should merge incomplete data', () => {\n    // let's pretend the client flushed before completion\n    const spans = mergeV2ById([\n      serverSpan,\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        duration: 207000,\n        remoteEndpoint: backend,\n        annotations: [{ timestamp: 1472470996403000, value: 'wr' }],\n      },\n      {\n        traceId: '0000000000000001',\n        id: '0000000000000002',\n        name: 'get',\n        kind: 'CLIENT',\n        timestamp: 1472470996199000,\n        localEndpoint: frontend,\n        annotations: [{ timestamp: 1472470996238000, value: 'ws' }],\n        tags: {\n          'http.path': '/api',\n          'clnt/finagle.version': '6.45.0',\n        },\n      },\n    ]);\n    expect(spans).toEqual([clientSpan, serverSpan]);\n  });\n\n  it('should order and de-dupe annotations', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '1111111111111111',\n        id: '000000000000000a',\n        annotations: [\n          { timestamp: 2, value: 'b' },\n          { timestamp: 2, value: 'b' },\n          { timestamp: 1, value: 'a' },\n        ],\n      },\n      {\n        traceId: '1111111111111111',\n        id: '000000000000000a',\n      },\n      {\n        traceId: '1111111111111111',\n        id: '000000000000000a',\n        annotations: [\n          { timestamp: 1, value: 'a' },\n          { timestamp: 3, value: 'b' },\n        ],\n      },\n    ]);\n\n    expect(spans).toEqual([\n      {\n        traceId: '1111111111111111',\n        id: '000000000000000a',\n        annotations: [\n          { timestamp: 1, value: 'a' },\n          { timestamp: 2, value: 'b' },\n          { timestamp: 3, value: 'b' },\n        ],\n        tags: {},\n      },\n    ]);\n  });\n\n  it('should order spans by shared, timestamp then name', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '0000000000000002',\n        name: 'c',\n        timestamp: 3,\n      },\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '3',\n        name: 'b',\n        timestamp: 2,\n      },\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '0000000000000004',\n        name: 'a',\n        timestamp: 1,\n        shared: true,\n      },\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '0000000000000004',\n        name: 'a',\n        timestamp: 2,\n      },\n    ]);\n\n    expect(spans.map((s) => `${s.id}-${!!s.shared}-${s.timestamp}`)).toEqual([\n      '0000000000000004-false-2', // unshared is first even if later!\n      '0000000000000004-true-1',\n      '0000000000000003-false-2',\n      '0000000000000002-false-3',\n    ]);\n  });\n\n  it('should order root first even if skewed timestamp', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000001',\n        name: 'c',\n        timestamp: 3,\n      },\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000002',\n        parentId: '0000000000000001',\n        name: 'b',\n        timestamp: 2, // happens before its parent\n      },\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000003',\n        parentId: '0000000000000001',\n        name: 'b',\n        timestamp: 3,\n      },\n    ]);\n\n    expect(spans.map((s) => s.id)).toEqual([\n      '0000000000000001',\n      '0000000000000002',\n      '0000000000000003',\n    ]);\n  });\n\n  // in the case of shared spans, root could be a client\n  it('should order earliest root first', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000001',\n        name: 'server',\n        timestamp: 1,\n        shared: true,\n      },\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000001',\n        name: 'client',\n        timestamp: 1,\n      },\n    ]);\n\n    expect(spans.map((s) => s.name)).toEqual(['client', 'server']);\n  });\n\n  // If instrumentation accidentally added shared flag on a server root span, delete it so that\n  // downstream code can process the tree properly\n  it('should delete accidental shared flag', () => {\n    const spans = mergeV2ById(yelpTrace);\n\n    expect(spans.length).toEqual(yelpTrace.length);\n    expect(spans[0].parentId).toBeUndefined();\n    expect(spans[0].shared).toBeUndefined();\n  });\n\n  it('should not delete valid shared flag on root span', () => {\n    const spans = mergeV2ById([\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000001',\n        kind: 'SERVER',\n        timestamp: 2,\n        shared: true,\n      },\n      {\n        traceId: '1111111111111111',\n        id: '0000000000000001',\n        kind: 'CLIENT',\n        timestamp: 1,\n      },\n    ]);\n\n    expect(spans.length).toEqual(2);\n    expect(spans[1].shared).toEqual(true);\n  });\n});\n\ndescribe('cleanupComparator', () => {\n  // some instrumentation don't add shared flag to servers\n  it('should order server after client', () => {\n    const spans = [\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '0000000000000004',\n        name: 'a',\n        kind: 'SERVER',\n      },\n      {\n        traceId: '1111111111111111',\n        parentId: '0000000000000001',\n        id: '0000000000000004',\n        name: 'a',\n        kind: 'CLIENT',\n      },\n    ];\n\n    expect(\n      spans.sort(cleanupComparator).map((s) => `${s.id}-${s.kind}`),\n    ).toEqual(['0000000000000004-CLIENT', '0000000000000004-SERVER']);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-node.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { compare, mergeV2ById } from './span-cleaner';\n\n/*\n * Convenience type representing a trace tree. Multiple Zipkin features require a trace tree. For\n * example, looking at network boundaries to correct clock skew and aggregating requests paths imply\n * visiting the tree.\n */\n// originally zipkin2.internal.SpanNode.java\nexport class SpanNode {\n  constructor(span) {\n    this._parent = undefined; // no default\n    this._span = span; // undefined is possible when this is a synthetic root node\n    this._children = [];\n  }\n\n  // Returns the parent, or undefined if root.\n  get parent() {\n    return this._parent;\n  }\n\n  _setParent(newParent) {\n    this._parent = newParent;\n  }\n\n  // Returns the span, or undefined if a synthetic root node\n  get span() {\n    return this._span;\n  }\n\n  // Returns the children of this node\n  get children() {\n    return this._children;\n  }\n\n  // Mutable as some transformations, such as clock skew, adjust the current node in the tree.\n  setSpan(span) {\n    if (!span) throw new Error('span was undefined');\n    this._span = span;\n  }\n\n  // Adds the child IFF it isn't already a child.\n  addChild(child) {\n    if (!child) throw new Error('child was undefined');\n    if (child === this)\n      throw new Error(`circular dependency on ${this.toString()}`);\n    child._setParent(this);\n    this._children.push(child);\n  }\n\n  // throws an error if the trace was empty\n  queueRootMostSpans() {\n    const queue = [];\n    // since the input data could be headless, we first push onto the queue the root-most spans\n    if (typeof this.span === 'undefined') {\n      // synthetic root\n      this.children.forEach((child) => queue.push(child));\n    } else {\n      queue.push(this);\n    }\n    if (queue.length === 0) throw new Error('Trace was empty');\n    return queue;\n  }\n\n  // Invokes the callback for each span resulting from a breadth-first traversal at this node\n  traverse(spanCallback) {\n    const queue = this.queueRootMostSpans();\n\n    while (queue.length > 0) {\n      const current = queue.shift();\n\n      spanCallback(current.span);\n\n      const { children } = current;\n      for (let i = 0; i < children.length; i += 1) {\n        queue.push(children[i]);\n      }\n    }\n  }\n\n  toString() {\n    if (this._span) return `SpanNode(${JSON.stringify(this._span)})`;\n    return 'SpanNode()';\n  }\n}\n\n// In javascript, dict keys can't be objects\nfunction keyString(id, shared = false, endpoint) {\n  if (!shared) return id;\n  const endpointString = endpoint ? JSON.stringify(endpoint) : 'x';\n  return `${id}-${endpointString}`;\n}\n\nfunction nodeByTimestamp(a, b) {\n  return compare(a.span.timestamp, b.span.timestamp);\n}\n\nfunction sortChildren(node) {\n  if (node.children.length > 0) {\n    node.children.sort(nodeByTimestamp);\n  }\n}\n\nfunction sortTreeByTimestamp(root) {\n  const queue = [];\n  queue.push(root);\n\n  while (queue.length > 0) {\n    const current = queue.shift();\n\n    sortChildren(current);\n\n    const { children } = current;\n    for (let i = 0; i < children.length; i += 1) {\n      queue.push(children[i]);\n    }\n  }\n}\n\nexport class SpanNodeBuilder {\n  constructor(params) {\n    const { debug = false } = params;\n    this._debug = debug;\n    this._rootSpan = undefined;\n    this._keyToNode = {};\n    this._spanToParent = {};\n  }\n\n  /*\n   * We index spans by (id, shared, localEndpoint) before processing them. This latter fields\n   * (shared, endpoint) are important because in zipkin (specifically B3), a server can share\n   * (re-use) the same ID as its client. This impacts processing quite a bit when multiple servers\n   * share one span ID.\n   *\n   * In a Zipkin trace, a parent (client) and child (server) can share the same ID if in an\n   * RPC. If two different servers respond to the same client, the only way for us to tell which\n   * is which is by endpoint. Our goal is to retain full paths across multiple endpoints. Even\n   * though instrumentation should be configured in such a way that a client never sends the same\n   * span ID to multiple servers, it can happen. Accordingly, we index defensively including any\n   * endpoint data that might be available.\n   */\n  _index(span) {\n    let idKey;\n    let parentKey;\n\n    if (span.shared) {\n      // we need to classify a shared span by its endpoint in case multiple servers respond to the\n      // same ID sent by the client.\n      idKey = keyString(span.id, true, span.localEndpoint);\n      // the parent of a server span is a client, which is not ambiguous for a given span ID.\n      parentKey = span.id;\n    } else {\n      idKey = span.id;\n      parentKey = span.parentId;\n    }\n\n    this._spanToParent[idKey] = parentKey;\n  }\n\n  /**\n   * Processing is taking a span and placing it at the most appropriate place in the trace tree.\n   * For example, if this is a server span, it would be a different node, and a child of its client\n   * even if they share the same span ID.\n   *\n   * Processing is defensive of typical problems in span reporting, such as depth-first. For\n   * example, depth-first reporting implies you can see spans missing their parent. Hence, the\n   * result of processing all spans can be a virtual root node.\n   */\n  _process(span) {\n    const endpoint = span.localEndpoint;\n    const key = keyString(span.id, span.shared, span.localEndpoint);\n    const noEndpointKey = endpoint ? keyString(span.id, span.shared) : key;\n\n    let parent;\n    if (span.shared) {\n      // Shared is a server span. It will very likely be on a different endpoint than the client.\n      // Clients are not ambiguous by ID, so we don't need to qualify by endpoint.\n      parent = span.id;\n    } else if (span.parentId) {\n      // We are not a root span, and not a shared server span. Proceed in most specific to least.\n\n      // We could be the child of a shared server span (ex a local (intermediate) span on the same\n      // endpoint). This is the most specific case, so we try this first.\n      parent = keyString(span.parentId, true, endpoint);\n      if (this._spanToParent[parent]) {\n        this._spanToParent[noEndpointKey] = parent;\n      } else {\n        // If there's no shared parent, fall back to normal case which is unqualified beyond ID.\n        parent = span.parentId;\n      }\n    } else if (this._rootSpan) {\n      // we are root or don't know our parent\n      if (this._debug) {\n        const prefix = 'attributing span missing parent to root';\n        /* eslint-disable no-console */\n        console.log(\n          `${prefix}: traceId=${span.traceId}, rootId=${this._rootSpan.span.id}, id=${span.id}`,\n        );\n      }\n    }\n\n    const node = new SpanNode(span);\n    // special-case root, and attribute missing parents to it. In\n    // other words, assume that the first root is the \"real\" root.\n    if (!parent && !this._rootSpan) {\n      this._rootSpan = node;\n      delete this._spanToParent[noEndpointKey];\n    } else if (span.shared) {\n      // In the case of shared server span, we need to address it both ways, in case intermediate\n      // spans are lacking endpoint information.\n      this._keyToNode[key] = node;\n      this._keyToNode[noEndpointKey] = node;\n    } else {\n      this._keyToNode[noEndpointKey] = node;\n    }\n  }\n\n  /*\n   * Builds a trace tree by merging and processing the input or returns an empty tree.\n   *\n   * While the input can be incomplete or redundant, they must all be a part of the same trace\n   * (e.g. all share the same trace ID).\n   */\n  build(spans) {\n    if (spans.length === 0) throw new Error('Trace was empty');\n\n    // In order to make a tree, we need clean data. This will merge any duplicates so that we\n    // don't have redundant leaves on the tree.\n    const cleaned = mergeV2ById(spans);\n    const { length } = cleaned;\n    const [{ traceId }] = cleaned;\n\n    if (this._debug) {\n      /* eslint-disable no-console */\n      console.log(`building trace tree: traceId=${traceId}`);\n    }\n\n    // Next, index all the spans so that we can understand any relationships.\n    for (let i = 0; i < length; i += 1) {\n      this._index(cleaned[i]);\n    }\n\n    // Now that we've index references to all spans, we can revise any parent-child relationships.\n    // Notably, by now, we can tell which is the root-most.\n    for (let i = 0; i < length; i += 1) {\n      this._process(cleaned[i]);\n    }\n\n    if (!this._rootSpan) {\n      if (this._debug) {\n        /* eslint-disable no-console */\n        console.log(\n          `substituting dummy node for missing root span: traceId=${traceId}`,\n        );\n      }\n      this._rootSpan = new SpanNode();\n    }\n\n    // At this point, we have the most reliable parent-child relationships and can allocate spans\n    // corresponding the best place in the trace tree.\n    Object.keys(this._spanToParent).forEach((key) => {\n      const child = this._keyToNode[key];\n      const parent = this._keyToNode[this._spanToParent[key]];\n\n      if (!parent) {\n        // Handle headless by attaching spans missing parents to root\n        this._rootSpan.addChild(child);\n      } else {\n        parent.addChild(child);\n      }\n    });\n\n    sortTreeByTimestamp(this._rootSpan);\n\n    return this._rootSpan;\n  }\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-node.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport { SpanNode, SpanNodeBuilder } from './span-node';\nimport { clean } from './span-cleaner';\n\n// originally zipkin2.internal.SpanNodeTest.java\ndescribe('queueRootMostSpans', () => {\n  it('should throw error on empty trace', () => {\n    let error;\n    try {\n      new SpanNode().queueRootMostSpans();\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toBe('Trace was empty');\n  });\n\n  it('should queue root', () => {\n    const root = new SpanNode({ traceId: '1', id: '1' });\n    expect(root.queueRootMostSpans()).toEqual([root]);\n  });\n\n  it('should queue root-most', () => {\n    const root = new SpanNode();\n    const a = new SpanNode({ traceId: '1', id: 'a' });\n    root.addChild(a);\n    const b = new SpanNode({ traceId: '1', id: 'b' });\n    root.addChild(b);\n\n    expect(root.queueRootMostSpans()).toEqual([a, b]);\n  });\n});\n\ndescribe('SpanNode', () => {\n  it('should construct without a span', () => {\n    const span = { traceId: '1', id: '1' };\n    const node = new SpanNode(span);\n\n    expect(node.span).toEqual(span);\n  });\n\n  it('should have an undefined span field when there is no span', () => {\n    const node = new SpanNode();\n\n    expect(node.span).toBeUndefined();\n  });\n\n  it('should not allow setting an undefined span', () => {\n    const node = new SpanNode();\n\n    expect(() => node.setSpan()).toThrow('span was undefined');\n  });\n\n  const a = new SpanNode({ traceId: '1', id: 'a' });\n  const b = new SpanNode({ traceId: '1', id: 'b' });\n  const c = new SpanNode({ traceId: '1', id: 'c' });\n  const d = new SpanNode({ traceId: '1', id: 'd' });\n  // root(a) has children b, c, d\n  a.addChild(b);\n  a.addChild(c);\n  a.addChild(d);\n  const e = new SpanNode({ traceId: '1', id: 'e' });\n  const f = new SpanNode({ traceId: '1', id: 'f' });\n  const g = new SpanNode({ traceId: '1', id: '1' });\n  // child(b) has children e, f, g\n  b.addChild(e);\n  b.addChild(f);\n  b.addChild(g);\n  const h = new SpanNode({ traceId: '1', id: '2' });\n  // f has no children\n  // child(g) has child h\n  g.addChild(h);\n\n  /*\n   * The following tree should traverse in alphabetical order\n   *\n   *          a\n   *        / | \\\n   *       b  c  d\n   *      /|\\\n   *     e f g\n   *          \\\n   *           h\n   */\n  it('should traverse breadth first', () => {\n    const ids = [];\n    a.traverse((s) => ids.push(s.id));\n\n    expect(ids).toEqual(['a', 'b', 'c', 'd', 'e', 'f', '1', '2']);\n  });\n});\n\n// originally zipkin2.internal.SpanNodeTest.java\ndescribe('SpanNodeBuilder', () => {\n  it('should throw error on empty trace', () => {\n    let error;\n    try {\n      new SpanNodeBuilder({}).build([]);\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toBe('Trace was empty');\n  });\n\n  // Makes sure that the trace tree is constructed based on parent-child, not by parameter order.\n  it('should construct a trace tree', () => {\n    const trace = [\n      { traceId: 'a', id: 'a' },\n      { traceId: 'a', parentId: 'a', id: 'b' },\n      { traceId: 'a', parentId: 'b', id: 'c' },\n      { traceId: 'a', parentId: 'c', id: 'd' },\n    ].map(clean);\n\n    // TRACE is sorted with root span first, lets reverse them to make\n    // sure the trace is stitched together by id.\n    const root = new SpanNodeBuilder({}).build(trace.slice(0).reverse());\n\n    expect(root.span).toEqual(trace[0]);\n    expect(root.children.map((n) => n.span)).toEqual([trace[1]]);\n\n    const [child] = root.children;\n    expect(child.children.map((n) => n.span)).toEqual([trace[2]]);\n  });\n\n  // input should be merged, but this ensures we are fine anyway\n  it('should dedupe while constructing a trace tree', () => {\n    const trace = [\n      { traceId: 'a', id: 'a' },\n      { traceId: 'a', id: 'a' },\n      { traceId: 'a', id: 'a' },\n    ];\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    expect(root.span).toEqual(clean(trace[0]));\n    expect(root.children.length).toBe(0);\n  });\n\n  it('should allocate spans missing parents to root', () => {\n    const trace = [\n      { traceId: 'a', id: 'b', timestamp: 1 },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'c',\n        timestamp: 2,\n      },\n      {\n        traceId: 'a',\n        parentId: 'b',\n        id: 'd',\n        timestamp: 3,\n      },\n      { traceId: 'a', id: 'e', timestamp: 4 },\n      { traceId: 'a', id: 'f', timestamp: 5 },\n    ].map(clean);\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    const spans = [];\n    root.traverse((span) => spans.push(span));\n    expect(spans).toEqual(trace);\n    expect(root.span.id).toBe('000000000000000b');\n    expect(root.children.map((n) => n.span)).toEqual(trace.slice(1));\n  });\n\n  // spans are often reported depth-first, so it is possible to not have a root yet\n  it('should construct a trace missing a root span', () => {\n    const trace = [\n      { traceId: 'a', parentId: 'a', id: 'b' },\n      { traceId: 'a', parentId: 'a', id: 'c' },\n      { traceId: 'a', parentId: 'a', id: 'd' },\n    ].map(clean);\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    expect(root.span).toBeUndefined();\n\n    const spans = [];\n    root.traverse((span) => spans.push(span));\n    expect(spans).toEqual(trace);\n  });\n\n  // input should be well formed, but this ensures we are fine anyway\n  it('should skip on cycle', () => {\n    const trace = [{ traceId: 'a', parentId: 'b', id: 'b' }];\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    expect(root.span).toEqual({\n      traceId: '000000000000000a',\n      id: '000000000000000b',\n      annotations: [],\n      tags: {},\n    });\n    expect(root.children.length).toBe(0);\n  });\n\n  it('should order children by timestamp', () => {\n    const trace = [\n      { traceId: 'a', id: '1' },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: 'a',\n        timestamp: 2,\n      },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: 'b',\n        timestamp: 1,\n      },\n      { traceId: 'a', parentId: '1', id: 'c' },\n    ].map(clean);\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    expect(root.children.map((n) => n.span)).toEqual([\n      trace[3],\n      trace[2],\n      trace[1],\n    ]); // null first\n  });\n\n  it('should order children by timestamp when IPs change', () => {\n    const trace = [\n      {\n        traceId: '1',\n        parentId: 'a',\n        id: 'c',\n        kind: 'SERVER',\n        shared: true,\n        timestamp: 1,\n        localEndpoint: { serviceName: 'my-service', ipv4: '10.2.3.4' },\n      },\n      {\n        traceId: '1',\n        parentId: 'c',\n        id: 'b',\n        kind: 'CLIENT',\n        timestamp: 2,\n        localEndpoint: { serviceName: 'my-service', ipv4: '169.2.3.4' },\n      },\n      {\n        traceId: '1',\n        parentId: 'c',\n        id: 'a',\n        timestamp: 3,\n        localEndpoint: { serviceName: 'my-service', ipv4: '10.2.3.4' },\n      },\n    ].map(clean);\n\n    const root = new SpanNodeBuilder({}).build(trace);\n\n    const spans = [];\n    root.traverse((span) => spans.push(span));\n    expect(spans).toEqual(trace);\n  });\n\n  it('should remove incorrect shared flag on only root span', () => {\n    const a = {\n      traceId: '1',\n      id: 'a',\n      kind: 'SERVER',\n      shared: true,\n      timestamp: 1,\n      localEndpoint: { serviceName: 'routing' },\n    };\n    // intentionally missing client per #3001\n    const b = {\n      traceId: '1',\n      parentId: 'a',\n      id: 'b',\n      kind: 'SERVER',\n      shared: true,\n      timestamp: 2,\n      localEndpoint: { serviceName: 'routing' },\n    };\n    // Also, intentionally missing client per #3001\n    const c = {\n      traceId: '1',\n      parentId: 'a',\n      id: 'c',\n      kind: 'SERVER',\n      shared: true,\n      timestamp: 3,\n      localEndpoint: { serviceName: 'yelp_main/biz' },\n    };\n\n    const trace = [a, b, c].map(clean);\n\n    const root = new SpanNodeBuilder({}).build(trace);\n    expect(root.span.id).toBe('000000000000000a');\n    expect(root.span.shared).toBeUndefined();\n\n    expect(root.children.map((n) => n.span)).toEqual([trace[1], trace[2]]);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-row.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { ConstantNames } from './trace-constants';\n\n// returns 'critical' if one of the spans has an error tag or currentErrorType was already critical,\n// returns 'transient' if one of the spans has an ERROR annotation, else\n// returns currentErrorType\nexport function getErrorType(span, currentErrorType) {\n  if (currentErrorType === 'critical') return currentErrorType;\n  if (span.tags.error !== undefined) {\n    // empty error tag is ok\n    return 'critical';\n  }\n  if (span.annotations.findIndex((ann) => ann.value === 'error') !== -1) {\n    return 'transient';\n  }\n  return currentErrorType;\n}\n\nexport function formatEndpoint(endpoint) {\n  if (!endpoint) return undefined;\n  const { ipv4, ipv6, port, serviceName } = endpoint;\n  if (ipv4 || ipv6) {\n    const ip = ipv6 ? `[${ipv6}]` : ipv4; // arbitrarily prefer ipv6\n    const portString = port ? `:${port}` : '';\n    const serviceNameString = serviceName ? ` (${serviceName})` : '';\n    return ip + portString + serviceNameString;\n  }\n  return serviceName || '';\n}\n\n/*\n * Derived means not annotated directly. Ex 'Server Start' reflects the timestamp of a\n * kind=SERVER span. 'Server Finish' is timestamp+duration of the same.\n */\nfunction toAnnotationRow(a, localFormatted, isDerived = false) {\n  const res = {\n    isDerived,\n    value: ConstantNames[a.value] || a.value,\n    timestamp: a.timestamp,\n  };\n  if (localFormatted) res.endpoint = localFormatted;\n  return res;\n}\n\nfunction parseAnnotationRows(span) {\n  const localFormatted = formatEndpoint(span.localEndpoint) || undefined;\n\n  let startTs = span.timestamp || 0;\n  let endTs = startTs && span.duration ? startTs + span.duration : 0;\n  let msTs = 0;\n  let wsTs = 0;\n  let wrTs = 0;\n  let mrTs = 0;\n\n  let begin;\n  let end;\n\n  let { kind } = span;\n\n  // scan annotations in case there are better timestamps, or inferred kind\n  const annotationsToAdd = [];\n  span.annotations.forEach((a) => {\n    switch (a.value) {\n      case 'cs':\n        kind = 'CLIENT';\n        if (a.timestamp <= startTs) {\n          startTs = a.timestamp;\n        } else {\n          annotationsToAdd.push(a);\n        }\n        break;\n      case 'sr':\n        kind = 'SERVER';\n        if (a.timestamp <= startTs) {\n          startTs = a.timestamp;\n        } else {\n          annotationsToAdd.push(a);\n        }\n        break;\n      case 'ss':\n        kind = 'SERVER';\n        if (a.timestamp >= endTs) {\n          endTs = a.timestamp;\n        } else {\n          annotationsToAdd.push(a);\n        }\n        break;\n      case 'cr':\n        kind = 'CLIENT';\n        if (a.timestamp >= endTs) {\n          endTs = a.timestamp;\n        } else {\n          annotationsToAdd.push(a);\n        }\n        break;\n      case 'ms':\n        kind = 'PRODUCER';\n        msTs = a.timestamp;\n        break;\n      case 'mr':\n        kind = 'CONSUMER';\n        mrTs = a.timestamp;\n        break;\n      case 'ws':\n        wsTs = a.timestamp;\n        break;\n      case 'wr':\n        wrTs = a.timestamp;\n        break;\n      default:\n        annotationsToAdd.push(a);\n    }\n  });\n\n  switch (kind) {\n    case 'CLIENT':\n      begin = 'Client Start';\n      end = 'Client Finish';\n      break;\n    case 'SERVER':\n      begin = 'Server Start';\n      end = 'Server Finish';\n      break;\n    case 'PRODUCER':\n      begin = 'Producer Start';\n      end = 'Producer Finish';\n      if (startTs === 0 || (msTs !== 0 && msTs < startTs)) {\n        startTs = msTs;\n        msTs = 0;\n      }\n      if (endTs === 0 || (wsTs !== 0 && wsTs > endTs)) {\n        endTs = wsTs;\n        wsTs = 0;\n      }\n      break;\n    case 'CONSUMER':\n      if (startTs === 0 || (wrTs !== 0 && wrTs < startTs)) {\n        startTs = wrTs;\n        wrTs = 0;\n      }\n      if (endTs === 0 || (mrTs !== 0 && mrTs > endTs)) {\n        endTs = mrTs;\n        mrTs = 0;\n      }\n      if (endTs !== 0 || wrTs !== 0) {\n        begin = 'Consumer Start';\n        end = 'Consumer Finish';\n      } else {\n        begin = 'Consumer Start';\n      }\n      break;\n    default:\n  }\n\n  // restore sometimes special-cased annotations\n  if (msTs) annotationsToAdd.push({ timestamp: msTs, value: 'ms' });\n  if (wsTs) annotationsToAdd.push({ timestamp: wsTs, value: 'ws' });\n  if (wrTs) annotationsToAdd.push({ timestamp: wrTs, value: 'wr' });\n  if (mrTs) annotationsToAdd.push({ timestamp: mrTs, value: 'mr' });\n\n  const beginAnnotation = startTs && begin;\n  const endAnnotation = endTs && end;\n\n  const annotations = []; // prefer empty to undefined for arrays\n\n  if (beginAnnotation) {\n    annotations.push(\n      toAnnotationRow(\n        {\n          value: begin,\n          timestamp: startTs,\n        },\n        localFormatted,\n        true,\n      ),\n    );\n  }\n\n  annotationsToAdd.forEach((a) => {\n    if (beginAnnotation && a.value === begin) return;\n    if (endAnnotation && a.value === end) return;\n    annotations.push(toAnnotationRow(a, localFormatted));\n  });\n\n  if (endAnnotation) {\n    annotations.push(\n      toAnnotationRow(\n        {\n          value: end,\n          timestamp: endTs,\n        },\n        localFormatted,\n        true,\n      ),\n    );\n  }\n  return annotations;\n}\n\nfunction parseTagRows(span) {\n  const localFormatted = formatEndpoint(span.localEndpoint) || undefined;\n\n  const tagRows = []; // prefer empty to undefined for arrays\n  const keys = Object.keys(span.tags);\n  if (keys.length > 0) {\n    keys.forEach((key) => {\n      const tagRow = {\n        key: ConstantNames[key] || key,\n        value: span.tags[key],\n      };\n      if (localFormatted) tagRow.endpoints = [localFormatted];\n      tagRows.push(tagRow);\n    });\n  }\n\n  // Ensure there's at least some data that will display the local address\n  if (\n    !span.kind &&\n    span.annotations.length === 0 &&\n    localFormatted &&\n    keys.length === 0\n  ) {\n    tagRows.push({\n      key: 'Local Address',\n      value: localFormatted,\n    });\n  }\n\n  let addr;\n  switch (span.kind) {\n    case 'CLIENT':\n      addr = 'Server Address';\n      break;\n    case 'SERVER':\n      addr = 'Client Address';\n      break;\n    case 'PRODUCER':\n      addr = 'Broker Address';\n      break;\n    case 'CONSUMER':\n      addr = 'Broker Address';\n      break;\n    default:\n  }\n\n  if (span.remoteEndpoint) {\n    tagRows.push({\n      key: addr || 'Server Address', // default when we don't know the endpoint\n      value: formatEndpoint(span.remoteEndpoint),\n    });\n  }\n  return tagRows;\n}\n\n// This ensures we don't add duplicate annotations on merge\nfunction maybePushAnnotation(annotations, a) {\n  if (\n    annotations.findIndex(\n      (b) => a.timestamp === b.timestamp && a.value === b.value,\n    ) === -1\n  ) {\n    annotations.push(a);\n  }\n}\n\n// This ensures we only add rows for tags that are unique on key and value on merge\nfunction maybePushTag(tags, a) {\n  const sameKeyAndValue = tags.filter(\n    (b) => a.key === b.key && a.value === b.value,\n  );\n  if (sameKeyAndValue.length === 0) {\n    tags.push(a);\n    return;\n  }\n  if ((a.endpoints || []).length === 0) {\n    return; // no endpoints to merge\n  }\n  // Handle when tags are reported by different endpoints\n  sameKeyAndValue.forEach((t) => {\n    if (!t.endpoints) t.endpoints = []; // eslint-disable-line no-param-reassign\n    a.endpoints.forEach((endpoint) => {\n      if (t.endpoints.indexOf(endpoint) === -1) {\n        t.endpoints.push(endpoint);\n      }\n    });\n  });\n}\n\n// This guards to ensure we don't add duplicate service names on merge\nfunction maybePushServiceName(serviceNames, serviceName) {\n  if (!serviceName) return;\n  if (serviceNames.findIndex((s) => s === serviceName) === -1) {\n    serviceNames.push(serviceName);\n  }\n}\n\nexport function getServiceName(endpoint) {\n  return endpoint ? endpoint.serviceName : undefined;\n}\n\nfunction isNullOrUndefined(ref) {\n  return typeof ref === 'undefined' || ref === null;\n}\n\n// Merges the data into a single span row, which is lacking presentation information\nexport function newSpanRow(spansToMerge, isLeafSpan) {\n  const [first] = spansToMerge;\n  const res = {\n    spanId: first.id,\n    serviceNames: [],\n    annotations: [],\n    tags: [],\n    errorType: 'none',\n  };\n\n  let sharedTimestamp;\n  let sharedDuration;\n  spansToMerge.forEach((next) => {\n    if (next.parentId) res.parentId = next.parentId;\n    if (next.name && (!res.spanName || next.kind === 'SERVER')) {\n      res.spanName = next.name; // prefer the server's span name\n    }\n\n    if (next.shared) {\n      // save off any shared timestamp, it is our second choice\n      if (!sharedTimestamp) sharedTimestamp = next.timestamp;\n      if (!sharedDuration) sharedDuration = next.duration;\n    } else {\n      if (!res.timestamp && next.timestamp) res.timestamp = next.timestamp;\n      if (!res.duration && next.duration) res.duration = next.duration;\n    }\n\n    const nextLocalServiceName = getServiceName(next.localEndpoint);\n    const nextRemoteServiceName = getServiceName(next.remoteEndpoint);\n    if (nextLocalServiceName && next.kind === 'SERVER') {\n      res.serviceName = nextLocalServiceName; // prefer the server's service name\n    } else if (\n      isLeafSpan &&\n      nextRemoteServiceName &&\n      next.kind === 'CLIENT' &&\n      !res.serviceName\n    ) {\n      // use the client's remote service name only on leaf spans\n      res.serviceName = nextRemoteServiceName;\n    } else if (nextLocalServiceName && !res.serviceName) {\n      res.serviceName = nextLocalServiceName;\n    }\n\n    maybePushServiceName(res.serviceNames, nextLocalServiceName);\n    maybePushServiceName(res.serviceNames, nextRemoteServiceName);\n\n    parseAnnotationRows(next).forEach((a) =>\n      maybePushAnnotation(res.annotations, a),\n    );\n    parseTagRows(next).forEach((t) => maybePushTag(res.tags, t));\n\n    res.errorType = getErrorType(next, res.errorType);\n\n    if (next.debug) res.debug = true;\n  });\n\n  // timestamp is used to derive positional data later\n  if (!res.timestamp && sharedTimestamp) res.timestamp = sharedTimestamp;\n  // duration is used for deriving data, and also for the zoom function\n  if (!res.duration && sharedDuration) res.duration = sharedDuration;\n\n  // Ensure no required property failures rendering an incomplete or malformed trace\n  if (isNullOrUndefined(res.duration)) res.duration = 0;\n  if (isNullOrUndefined(res.spanName)) res.spanName = 'unknown';\n  if (isNullOrUndefined(res.serviceName)) res.serviceName = 'unknown';\n  res.annotations.forEach((a) => {\n    // eslint-disable-next-line no-param-reassign\n    if (isNullOrUndefined(a.endpoint)) a.endpoint = 'unknown';\n  });\n\n  res.serviceNames.sort();\n  res.annotations.sort((a, b) => a.timestamp - b.timestamp);\n  return res;\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/span-row.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport { newSpanRow, getErrorType, formatEndpoint } from './span-row';\nimport { clean } from './span-cleaner';\n\n// bad traces from https://github.com/openzipkin/zipkin/issues/2829\nimport malformedTrace from '../test/data/malformed'; // Many data problems from Kong\nimport envoyTrace from '../../testdata/envoy.json'; // Slight problem: missing the local service name\n\n// endpoints from zipkin2.TestObjects\nconst frontend = {\n  serviceName: 'frontend',\n  ipv4: '127.0.0.1',\n  port: 8080,\n};\n\nconst backend = {\n  serviceName: 'backend',\n  ipv4: '192.168.99.101',\n  port: 9000,\n};\n\ndescribe('getErrorType', () => {\n  it('should return none if annotations and tags are empty', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [],\n      tags: {},\n    };\n    expect(getErrorType(span, 'none')).toBe('none');\n  });\n\n  it('should return none if ann=noError and tag=noError', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [{ timestamp: 1, value: 'not' }],\n      tags: { not: 'error' },\n    };\n    expect(getErrorType(span, 'none')).toBe('none');\n  });\n\n  it('should return none if second span has ann=noError and tag=noError', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      parentId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [{ timestamp: 1, value: 'not' }],\n      tags: { not: 'error' },\n    };\n    expect(getErrorType(span, 'none')).toBe('none');\n  });\n\n  it('should return critical if ann empty and tag=error', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [],\n      tags: { error: '' },\n    };\n    expect(getErrorType(span, 'none')).toBe('critical');\n  });\n\n  it('should return critical if ann=noError and tag=error', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [{ timestamp: 1, value: 'not' }],\n      tags: { error: '' },\n    };\n    expect(getErrorType(span, 'none')).toBe('critical');\n  });\n\n  it('should return critical if ann=error and tag=error', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [{ timestamp: 1, value: 'error' }],\n      tags: { error: '' },\n    };\n    expect(getErrorType(span, 'none')).toBe('critical');\n  });\n\n  it('should return critical if span1 has ann=error and span2 has tag=error', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      parentId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [],\n      tags: { error: '' },\n    };\n    expect(getErrorType(span, 'transient')).toBe('critical');\n  });\n\n  it('should return transient if ann=error and tag noError', () => {\n    const span = {\n      traceId: '1e223ff1f80f1c69',\n      id: 'bf396325699c84bf',\n      annotations: [{ timestamp: 1, value: 'error' }],\n      tags: { not: 'error' },\n    };\n    expect(getErrorType(span)).toBe('transient');\n  });\n});\n\ndescribe('SPAN v2 -> spanRow Conversion', () => {\n  // originally zipkin2.v1.SpanConverterTest.client\n  it('converts client span', () => {\n    const v2 = {\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'CLIENT',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      localEndpoint: frontend,\n      remoteEndpoint: backend,\n      annotations: [\n        { value: 'ws', timestamp: 1472470996238000 },\n        { value: 'wr', timestamp: 1472470996403000 },\n      ],\n      tags: {\n        'http.path': '/api',\n        'clnt/finagle.version': '6.45.0',\n      },\n    };\n\n    const spanRow = {\n      parentId: '2',\n      spanId: '3',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: false,\n          value: 'Wire Send',\n          timestamp: 1472470996238000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: false,\n          value: 'Wire Receive',\n          timestamp: 1472470996403000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: true,\n          value: 'Client Finish',\n          timestamp: 1472470996406000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [\n        {\n          key: 'http.path',\n          value: '/api',\n          endpoints: ['127.0.0.1:8080 (frontend)'],\n        },\n        {\n          key: 'clnt/finagle.version',\n          value: '6.45.0',\n          endpoints: ['127.0.0.1:8080 (frontend)'],\n        },\n        {\n          key: 'Server Address',\n          value: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      serviceName: 'frontend', // prefer the local address vs remote\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  it('should not duplicate service names', () => {\n    const converted = newSpanRow(\n      [\n        clean({\n          traceId: '1',\n          id: '3',\n          localEndpoint: frontend,\n          remoteEndpoint: frontend,\n        }),\n      ],\n      false,\n    );\n\n    expect(converted.serviceNames).toEqual(['frontend']);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.SpanConverterTest.client_unfinished\n  it('converts incomplete client span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'CLIENT',\n      timestamp: 1472470996199000,\n      localEndpoint: frontend,\n      annotations: [{ value: 'ws', timestamp: 1472470996238000 }],\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: false,\n          value: 'Wire Send',\n          timestamp: 1472470996238000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [], // prefers empty array to nil\n      serviceName: 'frontend',\n      serviceNames: ['frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.client_kindInferredFromAnnotation\n  it('infers cr annotation', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      localEndpoint: frontend,\n      annotations: [{ value: 'cs', timestamp: 1472470996199000 }],\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: true,\n          value: 'Client Finish',\n          timestamp: 1472470996406000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'frontend',\n      serviceNames: ['frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.lateRemoteEndpoint_cr\n  it('converts client span reporting remote endpoint with late cr', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'CLIENT',\n      localEndpoint: frontend,\n      remoteEndpoint: backend,\n      annotations: [{ value: 'cr', timestamp: 1472470996199000 }],\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Finish',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [{ key: 'Server Address', value: '192.168.99.101:9000 (backend)' }],\n      serviceName: 'frontend',\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.lateRemoteEndpoint_sa\n  it('converts late remoteEndpoint to sa', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      remoteEndpoint: backend,\n    });\n\n    const converted = newSpanRow([v2], false);\n    expect(converted.tags).toEqual([\n      { key: 'Server Address', value: '192.168.99.101:9000 (backend)' },\n    ]);\n    expect(converted.serviceName).toEqual('unknown');\n    expect(converted.serviceNames).toEqual(['backend']);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.noAnnotationsExceptAddresses\n  it('converts when remoteEndpoint exist without kind', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      localEndpoint: frontend,\n      remoteEndpoint: backend,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [],\n      tags: [\n        { key: 'Local Address', value: '127.0.0.1:8080 (frontend)' },\n        { key: 'Server Address', value: '192.168.99.101:9000 (backend)' },\n      ],\n      serviceName: 'frontend',\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.server\n  it('converts root server span', () => {\n    // let's pretend there was no caller, so we don't set shared flag\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      name: 'get',\n      kind: 'SERVER',\n      localEndpoint: backend,\n      remoteEndpoint: frontend,\n      timestamp: 1472470996199000,\n      duration: 207000,\n      tags: {\n        'http.path': '/api',\n        'finagle.version': '6.45.0',\n      },\n    });\n\n    const spanRow = {\n      spanId: '0000000000000002',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Server Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n        {\n          isDerived: true,\n          value: 'Server Finish',\n          timestamp: 1472470996406000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [\n        {\n          key: 'http.path',\n          value: '/api',\n          endpoints: ['192.168.99.101:9000 (backend)'],\n        },\n        {\n          key: 'finagle.version',\n          value: '6.45.0',\n          endpoints: ['192.168.99.101:9000 (backend)'],\n        },\n        { key: 'Client Address', value: '127.0.0.1:8080 (frontend)' },\n      ],\n      serviceName: 'backend',\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.missingEndpoints\n  it('converts span with no endpoints', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '1',\n      id: '2',\n      name: 'foo',\n      timestamp: 1472470996199000,\n      duration: 207000,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000001',\n      spanId: '0000000000000002',\n      spanName: 'foo',\n      serviceName: 'unknown',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [],\n      tags: [],\n      serviceNames: [],\n      errorType: 'none',\n    };\n\n    const expected = newSpanRow([v2], false);\n    expect(spanRow).toEqual(expected);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.coreAnnotation\n  it('converts v2 span retaining a cs annotation', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '1',\n      id: '2',\n      name: 'foo',\n      timestamp: 1472470996199000,\n      annotations: [{ value: 'cs', timestamp: 1472470996199000 }],\n    });\n\n    const spanRow = {\n      parentId: '0000000000000001',\n      spanId: '0000000000000002',\n      spanName: 'foo',\n      timestamp: 1472470996199000,\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Start',\n          timestamp: 1472470996199000,\n          endpoint: 'unknown',\n        },\n      ],\n      tags: [],\n      serviceName: 'unknown',\n      serviceNames: [],\n      errorType: 'none',\n    };\n\n    const expected = newSpanRow([v2], false);\n    expect(spanRow).toEqual(expected);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.server_shared_spanRow_no_timestamp_duration\n  it('when shared server span is missing its client, write its timestamp and duration', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'SERVER',\n      shared: true,\n      localEndpoint: backend,\n      timestamp: 1472470996199000,\n      duration: 207000,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Server Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n        {\n          isDerived: true,\n          value: 'Server Finish',\n          timestamp: 1472470996406000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'backend',\n      serviceNames: ['backend'],\n      errorType: 'none',\n    };\n\n    const expected = newSpanRow([v2], false);\n    expect(spanRow).toEqual(expected);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.server_incomplete_shared\n  it('converts incomplete shared server span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'SERVER',\n      shared: true,\n      localEndpoint: backend,\n      timestamp: 1472470996199000,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'get',\n      timestamp: 1472470996199000, // When we only have a shared timestamp, we should use it\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Server Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'backend',\n      serviceNames: ['backend'],\n      errorType: 'none',\n    };\n\n    const expected = newSpanRow([v2], false);\n    expect(spanRow).toEqual(expected);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.lateRemoteEndpoint_ss\n  it('converts late incomplete server span with remote endpoint', () => {\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      name: 'get',\n      kind: 'SERVER',\n      localEndpoint: backend,\n      remoteEndpoint: frontend,\n      annotations: [{ value: 'ss', timestamp: 1472470996199000 }],\n    });\n\n    const spanRow = {\n      spanId: '0000000000000002',\n      spanName: 'get',\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Server Finish',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [{ key: 'Client Address', value: '127.0.0.1:8080 (frontend)' }],\n      serviceName: 'backend',\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.lateRemoteEndpoint_ca\n  it('converts late remote endpoint server span', () => {\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      kind: 'SERVER',\n      remoteEndpoint: frontend,\n    });\n\n    const converted = newSpanRow([v2], false);\n    expect(converted.tags).toEqual([\n      { key: 'Client Address', value: '127.0.0.1:8080 (frontend)' },\n    ]);\n    expect(converted.serviceName).toEqual('unknown');\n    expect(converted.serviceNames).toEqual(['frontend']);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.localSpan_emptyComponent\n  it('converts local span', () => {\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      name: 'local',\n      localEndpoint: { serviceName: 'frontend' },\n      timestamp: 1472470996199000,\n      duration: 207000,\n    });\n\n    const spanRow = {\n      spanId: '0000000000000002',\n      spanName: 'local',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [],\n      tags: [{ key: 'Local Address', value: 'frontend' }],\n      serviceName: 'frontend',\n      serviceNames: ['frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.producer_remote\n  it('converts incomplete producer span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'send',\n      kind: 'PRODUCER',\n      timestamp: 1472470996199000,\n      localEndpoint: frontend,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'send',\n      timestamp: 1472470996199000,\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Producer Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'frontend',\n      serviceNames: ['frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.producer_duration\n  it('converts producer span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'send',\n      kind: 'PRODUCER',\n      localEndpoint: frontend,\n      timestamp: 1472470996199000,\n      duration: 51000,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'send',\n      timestamp: 1472470996199000,\n      duration: 51000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Producer Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: true,\n          value: 'Producer Finish',\n          timestamp: 1472470996250000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'frontend',\n      serviceNames: ['frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.consumer\n  it('converts incomplete consumer span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'next-message',\n      kind: 'CONSUMER',\n      timestamp: 1472470996199000,\n      localEndpoint: backend,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'next-message',\n      timestamp: 1472470996199000,\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Consumer Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'backend',\n      serviceNames: ['backend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.consumer_remote\n  it('converts incomplete consumer span with remote endpoint', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'next-message',\n      kind: 'CONSUMER',\n      timestamp: 1472470996199000,\n      localEndpoint: backend,\n      remoteEndpoint: { serviceName: 'kafka' },\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'next-message',\n      timestamp: 1472470996199000,\n      duration: 0,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Consumer Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [{ key: 'Broker Address', value: 'kafka' }],\n      serviceName: 'backend',\n      serviceNames: ['backend', 'kafka'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.consumer_duration\n  it('converts consumer span', () => {\n    const v2 = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'send',\n      kind: 'CONSUMER',\n      localEndpoint: backend,\n      timestamp: 1472470996199000,\n      duration: 51000,\n    });\n\n    const spanRow = {\n      parentId: '0000000000000002',\n      spanId: '0000000000000003',\n      spanName: 'send',\n      timestamp: 1472470996199000,\n      duration: 51000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Consumer Start',\n          timestamp: 1472470996199000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n        {\n          isDerived: true,\n          value: 'Consumer Finish',\n          timestamp: 1472470996250000,\n          endpoint: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      tags: [],\n      serviceName: 'backend',\n      serviceNames: ['backend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], false)).toEqual(spanRow);\n  });\n\n  it('should prefer ipv6', () => {\n    const localEndpoint = {\n      serviceName: 'there',\n      ipv4: '10.57.50.84',\n      ipv6: '2001:db8::c001',\n      port: 80,\n    };\n\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      localEndpoint,\n    });\n\n    const spanRow = newSpanRow([v2], false);\n    expect(spanRow.tags.map((s) => s.value)).toEqual([\n      '[2001:db8::c001]:80 (there)',\n    ]);\n  });\n\n  it('should not require endpoint serviceName', () => {\n    const v2 = clean({\n      traceId: '1',\n      id: '2',\n      kind: 'CLIENT',\n      timestamp: 1,\n      localEndpoint: {\n        ipv6: '2001:db8::c001',\n      },\n    });\n\n    const spanRow = newSpanRow([v2], false);\n    expect(spanRow.annotations.map((s) => s.endpoint)).toEqual([\n      '[2001:db8::c001]',\n    ]);\n  });\n\n  it('converts client leaf spans using its remote service name', () => {\n    const v2 = {\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'CLIENT',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      localEndpoint: frontend,\n      remoteEndpoint: backend,\n      annotations: [\n        { value: 'ws', timestamp: 1472470996238000 },\n        { value: 'wr', timestamp: 1472470996403000 },\n      ],\n      tags: {\n        'http.path': '/api',\n        'clnt/finagle.version': '6.45.0',\n      },\n    };\n\n    const spanRow = {\n      parentId: '2',\n      spanId: '3',\n      spanName: 'get',\n      timestamp: 1472470996199000,\n      duration: 207000,\n      annotations: [\n        {\n          isDerived: true,\n          value: 'Client Start',\n          timestamp: 1472470996199000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: false,\n          value: 'Wire Send',\n          timestamp: 1472470996238000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: false,\n          value: 'Wire Receive',\n          timestamp: 1472470996403000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n        {\n          isDerived: true,\n          value: 'Client Finish',\n          timestamp: 1472470996406000,\n          endpoint: '127.0.0.1:8080 (frontend)',\n        },\n      ],\n      tags: [\n        {\n          key: 'http.path',\n          value: '/api',\n          endpoints: ['127.0.0.1:8080 (frontend)'],\n        },\n        {\n          key: 'clnt/finagle.version',\n          value: '6.45.0',\n          endpoints: ['127.0.0.1:8080 (frontend)'],\n        },\n        {\n          key: 'Server Address',\n          value: '192.168.99.101:9000 (backend)',\n        },\n      ],\n      serviceName: 'backend', // prefer client remote address on leaf client-only spans\n      serviceNames: ['backend', 'frontend'],\n      errorType: 'none',\n    };\n\n    expect(newSpanRow([v2], true)).toEqual(spanRow);\n  });\n});\n\ndescribe('newSpanRow', () => {\n  const clientSpan = clean({\n    traceId: '1',\n    parentId: '2',\n    id: '3',\n    name: 'get',\n    kind: 'CLIENT',\n    timestamp: 1472470996199000,\n    duration: 207000,\n    localEndpoint: frontend,\n  });\n  const serverSpan = clean({\n    traceId: '1',\n    parentId: '2',\n    id: '3',\n    name: 'get',\n    kind: 'SERVER',\n    timestamp: 1472470996238000,\n    duration: 165000,\n    localEndpoint: backend,\n    shared: true,\n  });\n  const expectedSpanRow = {\n    parentId: '0000000000000002',\n    spanId: '0000000000000003',\n    spanName: 'get',\n    timestamp: 1472470996199000,\n    duration: 207000,\n    annotations: [\n      {\n        isDerived: true,\n        value: 'Client Start',\n        timestamp: 1472470996199000,\n        endpoint: '127.0.0.1:8080 (frontend)',\n      },\n      {\n        isDerived: true,\n        value: 'Server Start',\n        timestamp: 1472470996238000,\n        endpoint: '192.168.99.101:9000 (backend)',\n      },\n      {\n        isDerived: true,\n        value: 'Server Finish',\n        timestamp: 1472470996403000,\n        endpoint: '192.168.99.101:9000 (backend)',\n      },\n      {\n        isDerived: true,\n        value: 'Client Finish',\n        timestamp: 1472470996406000,\n        endpoint: '127.0.0.1:8080 (frontend)',\n      },\n    ],\n    tags: [],\n    serviceName: 'backend', // prefer server in shared span\n    serviceNames: ['backend', 'frontend'],\n    errorType: 'none',\n  };\n\n  it('should merge server and client span', () => {\n    const spanRow = newSpanRow([serverSpan, clientSpan], false);\n\n    expect(spanRow).toEqual(expectedSpanRow);\n  });\n\n  it('should merge client and server span', () => {\n    const spanRow = newSpanRow([clientSpan, serverSpan], false);\n\n    expect(spanRow).toEqual(expectedSpanRow);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.mergeWhenTagsSentSeparately\n  it('should add late server addr', () => {\n    const spanRow = newSpanRow(\n      [\n        clientSpan,\n        clean({\n          traceId: '1',\n          id: '3',\n          remoteEndpoint: backend,\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.tags).toEqual([\n      { key: 'Server Address', value: '192.168.99.101:9000 (backend)' },\n    ]);\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.mergePrefersServerSpanName\n  it('should overwrite client name with server name', () => {\n    const spanRow = newSpanRow(\n      [\n        clientSpan,\n        clean({\n          traceId: '1',\n          id: '3',\n          name: 'get /users/:userId',\n          timestamp: 1472470996238000,\n          kind: 'SERVER',\n          localEndpoint: backend,\n          shared: true,\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.spanName).toBe('get /users/:userId');\n  });\n\n  // originally zipkin2.v1.SpanConverterTest.timestampAndDurationMergeWithClockSkew\n  it('should merge timestamp and duration even with skew', () => {\n    const leftTimestamp = 100 * 1000;\n    const leftDuration = 35 * 1000;\n\n    const rightTimestamp = 200 * 1000;\n    const rightDuration = 30 * 1000;\n\n    const leftSpan = clean({\n      traceId: '1',\n      parentId: '2',\n      id: '3',\n      name: 'get',\n      kind: 'CLIENT',\n      timestamp: leftTimestamp,\n      duration: leftDuration,\n      localEndpoint: frontend,\n    });\n\n    const rightSpan = clean({\n      traceId: '1',\n      id: '3',\n      name: 'get',\n      kind: 'SERVER',\n      timestamp: rightTimestamp,\n      duration: rightDuration,\n      localEndpoint: backend,\n      shared: true,\n    });\n\n    const leftFirst = newSpanRow([leftSpan, rightSpan], false);\n    const rightFirst = newSpanRow([rightSpan, leftSpan], false);\n\n    [leftFirst, rightFirst].forEach((completeSpan) => {\n      expect(completeSpan.timestamp).toEqual(leftTimestamp);\n      expect(completeSpan.duration).toEqual(leftDuration);\n\n      // ensure if server isn't propagated the parent ID, it is still ok.\n      expect(completeSpan.parentId).toBe('0000000000000002');\n    });\n  });\n\n  it('should not overwrite client name with empty', () => {\n    const spanRow = newSpanRow(\n      [\n        clientSpan,\n        clean({\n          traceId: '1',\n          id: '3',\n          timestamp: 1472470996238000,\n          kind: 'SERVER',\n          localEndpoint: backend,\n          shared: true,\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.spanName).toBe(clientSpan.name);\n  });\n\n  it('should dedupe annotations with same timestamp and value', () => {\n    const spanRow = newSpanRow(\n      [\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          kind: 'CLIENT',\n          localEndpoint: frontend,\n          annotations: [{ timestamp: 1, value: 'hit' }],\n        }),\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          kind: 'CLIENT',\n          localEndpoint: frontend,\n          annotations: [{ timestamp: 1, value: 'hit' }],\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.annotations).toEqual([\n      {\n        timestamp: 1,\n        value: 'hit',\n        endpoint: '127.0.0.1:8080 (frontend)',\n        isDerived: false,\n      },\n    ]);\n  });\n\n  it('should merge endpoints on shared tag', () => {\n    const spanRow = newSpanRow(\n      [\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          kind: 'CLIENT',\n          localEndpoint: frontend,\n          tags: { 'http.path': '/foo' },\n        }),\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          shared: true,\n          kind: 'SERVER',\n          localEndpoint: backend,\n          tags: { 'http.path': '/foo' },\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.tags).toEqual([\n      {\n        key: 'http.path',\n        value: '/foo',\n        endpoints: [\n          '127.0.0.1:8080 (frontend)',\n          '192.168.99.101:9000 (backend)',\n        ],\n      },\n    ]);\n  });\n\n  it('should show difference in tag values per endpoint', () => {\n    const spanRow = newSpanRow(\n      [\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          kind: 'CLIENT',\n          localEndpoint: frontend,\n          tags: { 'http.path': '/foo' },\n        }),\n        clean({\n          traceId: '1',\n          parentId: '2',\n          id: '3',\n          shared: true,\n          kind: 'SERVER',\n          localEndpoint: backend,\n          tags: { 'http.path': '/foo/redirected' },\n        }),\n      ],\n      false,\n    );\n\n    expect(spanRow.tags).toEqual([\n      {\n        key: 'http.path',\n        value: '/foo',\n        endpoints: ['127.0.0.1:8080 (frontend)'],\n      },\n      {\n        key: 'http.path',\n        value: '/foo/redirected',\n        endpoints: ['192.168.99.101:9000 (backend)'],\n      },\n    ]);\n  });\n\n  // This prevents white screens due to failed required property tests downstream\n  it('should backfill data in malformed trace', () => {\n    malformedTrace.concat(envoyTrace).forEach((span) => {\n      const spanRow = newSpanRow([clean(span)], false);\n      expect(spanRow.duration).toBeDefined();\n      expect(spanRow.serviceName).toBeDefined();\n      expect(spanRow.spanName).toBeDefined();\n      spanRow.annotations.forEach((a) => {\n        expect(a.endpoint).toBeDefined();\n      });\n    });\n  });\n});\n\ndescribe('formatEndpoint', () => {\n  it('should format ip and port', () => {\n    expect(formatEndpoint({ ipv4: '150.151.152.153', port: 5000 })).toBe(\n      '150.151.152.153:5000',\n    );\n  });\n\n  it('should not use port when missing or zero', () => {\n    expect(formatEndpoint({ ipv4: '150.151.152.153' })).toBe('150.151.152.153');\n    expect(formatEndpoint({ ipv4: '150.151.152.153', port: 0 })).toBe(\n      '150.151.152.153',\n    );\n  });\n\n  it('should put service name in parenthesis', () => {\n    expect(\n      formatEndpoint({\n        ipv4: '150.151.152.153',\n        port: 9042,\n        serviceName: 'cassandra',\n      }),\n    ).toBe('150.151.152.153:9042 (cassandra)');\n    expect(\n      formatEndpoint({\n        ipv4: '150.151.152.153',\n        serviceName: 'cassandra',\n      }),\n    ).toBe('150.151.152.153 (cassandra)');\n  });\n\n  it('should not show empty service name', () => {\n    expect(\n      formatEndpoint({\n        ipv4: '150.151.152.153',\n        port: 9042,\n        serviceName: '',\n      }),\n    ).toBe('150.151.152.153:9042');\n    expect(\n      formatEndpoint({\n        ipv4: '150.151.152.153',\n        serviceName: '',\n      }),\n    ).toBe('150.151.152.153');\n  });\n\n  it('should show service name missing IP', () => {\n    expect(\n      formatEndpoint({\n        serviceName: 'rabbit',\n      }),\n    ).toBe('rabbit');\n  });\n\n  it('should not crash on no data', () => {\n    expect(formatEndpoint({})).toBe('');\n  });\n\n  it('should put ipv6 in brackets', () => {\n    expect(\n      formatEndpoint({\n        ipv6: '2001:db8::c001',\n        port: 9042,\n        serviceName: 'cassandra',\n      }),\n    ).toBe('[2001:db8::c001]:9042 (cassandra)');\n\n    expect(\n      formatEndpoint({\n        ipv6: '2001:db8::c001',\n        port: 9042,\n      }),\n    ).toBe('[2001:db8::c001]:9042');\n\n    expect(\n      formatEndpoint({\n        ipv6: '2001:db8::c001',\n      }),\n    ).toBe('[2001:db8::c001]');\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/trace-constants.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nconst CLIENT_SEND = 'cs';\nconst CLIENT_SEND_FRAGMENT = 'csf';\nconst CLIENT_RECEIVE = 'cr';\nconst CLIENT_RECEIVE_FRAGMENT = 'crf';\nconst MESSAGE_SEND = 'ms';\nconst MESSAGE_RECEIVE = 'mr';\nconst SERVER_SEND = 'ss';\nconst SERVER_SEND_FRAGMENT = 'ssf';\nconst SERVER_RECEIVE = 'sr';\nconst SERVER_RECEIVE_FRAGMENT = 'srf';\nconst SERVER_ADDR = 'sa';\nconst CLIENT_ADDR = 'ca';\nconst MESSAGE_ADDR = 'ma';\nconst WIRE_SEND = 'ws';\nconst WIRE_RECEIVE = 'wr';\nconst LOCAL_COMPONENT = 'lc';\n\nexport const ConstantNames = {};\nConstantNames[CLIENT_SEND] = 'Client Send';\nConstantNames[CLIENT_SEND_FRAGMENT] = 'Client Send Fragment';\nConstantNames[CLIENT_RECEIVE] = 'Client Receive';\nConstantNames[CLIENT_RECEIVE_FRAGMENT] = 'Client Receive Fragment';\nConstantNames[MESSAGE_SEND] = 'Producer Send';\nConstantNames[MESSAGE_RECEIVE] = 'Consumer Receive';\nConstantNames[SERVER_SEND] = 'Server Send';\nConstantNames[SERVER_SEND_FRAGMENT] = 'Server Send Fragment';\nConstantNames[SERVER_RECEIVE] = 'Server Receive';\nConstantNames[SERVER_RECEIVE_FRAGMENT] = 'Server Receive Fragment';\nConstantNames[CLIENT_ADDR] = 'Client Address';\nConstantNames[MESSAGE_ADDR] = 'Broker Address';\nConstantNames[SERVER_ADDR] = 'Server Address';\nConstantNames[WIRE_SEND] = 'Wire Send';\nConstantNames[WIRE_RECEIVE] = 'Wire Receive';\nConstantNames[LOCAL_COMPONENT] = 'Local Component';\n// Don't add ERROR to ConstantNames -- css coloring depends on constant name 'error'\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/trace.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport orderBy from 'lodash/orderBy';\nimport moment from 'moment';\nimport { compare } from './span-cleaner';\nimport { getErrorType, newSpanRow, getServiceName } from './span-row';\n\n// To ensure data doesn't scroll off the screen, we need all timestamps, not just\n// client/server ones.\nexport function addTimestamps(span, timestamps) {\n  if (!span.timestamp) return;\n  timestamps.push(span.timestamp);\n  if (!span.duration) return;\n  timestamps.push(span.timestamp + span.duration);\n}\n\nexport function getMaxDuration(timestamps) {\n  if (timestamps.length > 1) {\n    timestamps.sort();\n    return timestamps[timestamps.length - 1] - timestamps[0];\n  }\n  return 0;\n}\n\nfunction pushEntry(dict, key, value) {\n  if (dict[key]) {\n    dict[key].push(value);\n  } else {\n    dict[key] = [value]; // eslint-disable-line no-param-reassign\n  }\n}\n\nfunction addServiceNameTimestampDuration(span, groupedTimestamps) {\n  const value = {\n    timestamp: span.timestamp || 0, // only used by totalDuration\n    duration: span.duration || 0,\n  };\n\n  if (span.localEndpoint && span.localEndpoint.serviceName) {\n    pushEntry(groupedTimestamps, span.localEndpoint.serviceName, value);\n  }\n  // TODO: only do this if it is a leaf span and a client or producer.\n  // If we are at the bottom of the tree, it can be helpful to count also against a remote\n  // uninstrumented service\n  if (span.remoteEndpoint && span.remoteEndpoint.serviceName) {\n    pushEntry(groupedTimestamps, span.remoteEndpoint.serviceName, value);\n  }\n}\n\nfunction nodeByTimestamp(a, b) {\n  return compare(a.span.timestamp, b.span.timestamp);\n}\n\n// Returns null on empty or when missing a timestamp\nexport function traceSummary(root) {\n  const timestamps = [];\n  const groupedTimestamps = {};\n\n  let traceId;\n  let spanCount = 0;\n  let errorType = 'none';\n\n  root.traverse((span) => {\n    spanCount += 1;\n    traceId = span.traceId;\n    errorType = getErrorType(span, errorType);\n    addTimestamps(span, timestamps);\n    addServiceNameTimestampDuration(span, groupedTimestamps);\n  });\n\n  if (timestamps.length === 0)\n    throw new Error(`Trace ${traceId} is missing a timestamp`);\n\n  // If the first element does not exist, Error will be thrown.\n  // So we don't have to check rootSpan exisitence.\n  const [rootSpan] = root.queueRootMostSpans();\n  const rootServiceName =\n    getServiceName(rootSpan._span.localEndpoint) ||\n    getServiceName(rootSpan._span.remoteEndpoint) ||\n    'unknown';\n  const rootSpanName = rootSpan._span.name || 'unknown';\n\n  return {\n    traceId,\n    timestamp: timestamps[0],\n    duration: getMaxDuration(timestamps),\n    groupedTimestamps,\n    errorType,\n    spanCount,\n    root: {\n      serviceName: rootServiceName,\n      spanName: rootSpanName,\n    },\n  };\n}\n\nfunction formatDate(timestamp, utc) {\n  let m = moment(timestamp / 1000);\n  if (utc) {\n    m = m.utc();\n  }\n  return m.format('MM-DD-YYYYTHH:mm:ss.SSSZZ');\n}\n\nexport function mkDurationStr(duration) {\n  if (duration === 0) {\n    return '0ms';\n  }\n  if (typeof duration === 'undefined') {\n    return '';\n  }\n  if (duration < 1000) {\n    return `${duration.toFixed(0)}μs`;\n  }\n  if (duration < 1000000) {\n    if (duration % 1000 === 0) {\n      // Sometimes spans are in milliseconds resolution\n      return `${(duration / 1000).toFixed(0)}ms`;\n    }\n    return `${(duration / 1000).toFixed(3)}ms`;\n  }\n  return `${(duration / 1000000).toFixed(3)}s`;\n}\n\n// Returns a list of {:serviceName, :spanCount} ordered descending by trace duration\nexport function getServiceSummaries(groupedTimestamps) {\n  const services = Object.entries(groupedTimestamps).map(\n    ([serviceName, sts]) => ({\n      serviceName,\n      spanCount: sts.length,\n      maxSpanDuration: Math.max(...sts.map((t) => t.duration)),\n    }),\n  );\n  return orderBy(\n    services,\n    ['maxSpanDuration', 'serviceName'],\n    ['desc', 'asc'],\n  ).map((summary) => ({\n    serviceName: summary.serviceName,\n    spanCount: summary.spanCount,\n  }));\n}\n\nexport function traceSummaries(serviceName, summaries, utc = false) {\n  const maxDuration = Math.max(...summaries.map((s) => s.duration));\n\n  return summaries\n    .map((t) => {\n      const { timestamp } = t;\n\n      const res = {\n        traceId: t.traceId, // used to navigate to trace screen\n        timestamp, // used only for client-side sort\n        startTs: formatDate(timestamp, utc),\n        spanCount: t.spanCount,\n        root: t.root,\n      };\n\n      const duration = t.duration || 0;\n      if (duration) {\n        // used to show the relative duration this trace was compared to others\n        res.width = parseInt(\n          (parseFloat(duration) / parseFloat(maxDuration)) * 100,\n          10,\n        );\n        res.duration = duration; // Used in summary view and client-side sort\n        res.durationStr = mkDurationStr(duration);\n      }\n\n      // groupedTimestamps is keyed by service name, if there are no service names in the trace,\n      // don't try to add data dependent on service names.\n      if (Object.keys(t.groupedTimestamps).length !== 0) {\n        res.serviceSummaries = getServiceSummaries(t.groupedTimestamps);\n      } else {\n        res.serviceSummaries = [];\n      }\n\n      if (t.errorType !== 'none') res.infoClass = `trace-error-${t.errorType}`;\n      return res;\n    })\n    .sort((t1, t2) => {\n      const durationComparison = t2.duration - t1.duration;\n      if (durationComparison === 0) {\n        return t1.traceId.localeCompare(t2.traceId);\n      }\n      return durationComparison;\n    });\n}\n\nfunction incrementEntry(dict, key) {\n  if (dict[key]) {\n    dict[key] += 1; // eslint-disable-line no-param-reassign\n  } else {\n    dict[key] = 1; // eslint-disable-line no-param-reassign\n  }\n}\n\n// We need to do an initial traversal in order to get the timestamp and duration of the trace,\n// as that is used for positioning spans later.\nfunction getTraceTimestampAndDuration(root) {\n  const timestamps = [];\n  root.traverse((span) => addTimestamps(span, timestamps));\n  return {\n    timestamp: timestamps[0] || 0,\n    duration: getMaxDuration(timestamps),\n  };\n}\n\nfunction addLayoutDetails(\n  spanRow,\n  traceTimestamp,\n  traceDuration,\n  depth,\n  childIds,\n) {\n  /* eslint-disable no-param-reassign */\n  spanRow.childIds = childIds;\n  spanRow.depth = depth + 1;\n  spanRow.depthClass = (depth - 1) % 6;\n\n  // Add the correct width and duration string for the span\n  if (spanRow.duration) {\n    // implies traceDuration, as trace duration is derived from spans\n    const width = traceDuration ? (spanRow.duration / traceDuration) * 100 : 0;\n    spanRow.width = width < 0.1 ? 0.1 : width;\n    spanRow.durationStr = mkDurationStr(spanRow.duration); // bubble over the span in trace view\n  } else {\n    spanRow.width = 0.1;\n    spanRow.durationStr = '';\n  }\n\n  if (traceDuration) {\n    // position the span at the correct offset in the trace diagram.\n    spanRow.left = ((spanRow.timestamp - traceTimestamp) / traceDuration) * 100;\n\n    // position each annotation at the offset in the trace diagram.\n    spanRow.annotations.forEach((a) => {\n      /* eslint-disable no-param-reassign */\n      // left offset here is from the span\n      a.left = spanRow.duration\n        ? ((a.timestamp - spanRow.timestamp) / spanRow.duration) * 100\n        : 0;\n      // relative time is for the trace itself\n      a.relativeTime = mkDurationStr(a.timestamp - traceTimestamp);\n      a.width = 8; // size of the dot\n    });\n  } else {\n    spanRow.left = 0;\n  }\n}\n\n// TODO: Revisit naming\nexport function detailedTraceSummary(root) {\n  const serviceNameToCount = {};\n  let queue = root.queueRootMostSpans();\n  const modelview = {\n    traceId: queue[0].span.traceId,\n    depth: 0,\n    spans: [],\n  };\n\n  const { timestamp, duration } = getTraceTimestampAndDuration(root);\n  if (!timestamp)\n    throw new Error(`Trace ${modelview.traceId} is missing a timestamp`);\n\n  while (queue.length > 0) {\n    let current = queue.shift();\n\n    // This is more than a normal tree traversal, as we are merging any server spans that share the\n    // same ID. When that's the case, we pull up any of their children as if they are our own.\n    const spansToMerge = [current.span];\n    const children = [];\n    current.children.forEach((child) => {\n      if (current.span.id === child.span.id) {\n        spansToMerge.push(child.span);\n        child.children.forEach((grandChild) => children.push(grandChild));\n      } else {\n        children.push(child);\n      }\n    });\n\n    // Pulling up children may affect our sort order. We re-sort to ensure rows are added in\n    // timestamp order.\n    children.sort(nodeByTimestamp);\n    queue = children.concat(queue);\n    const childIds = children.map((child) => child.span.id);\n\n    // The mustache template expects one row per span ID. To get the correct depth class, we need to\n    // count distinct span IDs above us.\n    let depth = 1;\n    while (current.parent && current.parent.span) {\n      if (current.parent.span.id !== current.span.id) depth += 1;\n      current = current.parent;\n    }\n    // If we are the deepest span, mark the trace accordingly\n    if (depth > modelview.depth) modelview.depth = depth;\n\n    const isLeafSpan = children.length === 0;\n    const spanRow = newSpanRow(spansToMerge, isLeafSpan);\n\n    addLayoutDetails(spanRow, timestamp, duration, depth, childIds);\n    // NOTE: This will increment both the local and remote service name\n    //\n    // TODO: We should only do this if it is a leaf span and a client or producer. If we are at the\n    // bottom of the tree, it can be helpful to count also against a remote uninstrumented service.\n    spanRow.serviceNames.forEach((serviceName) =>\n      incrementEntry(serviceNameToCount, serviceName),\n    );\n\n    modelview.spans.push(spanRow);\n  }\n\n  modelview.rootSpan = {};\n  // If the first element does not exist, Error will be thrown.\n  // So we don't have to check rootSpan existence.\n  const [rootSpan] = root.queueRootMostSpans();\n  modelview.rootSpan.serviceName =\n    getServiceName(rootSpan._span.localEndpoint) ||\n    getServiceName(rootSpan._span.remoteEndpoint) ||\n    'unknown';\n  modelview.rootSpan.spanName = rootSpan._span.name || 'unknown';\n\n  modelview.serviceNameAndSpanCounts = Object.keys(serviceNameToCount)\n    .sort()\n    .map((serviceName) => ({\n      serviceName,\n      spanCount: serviceNameToCount[serviceName],\n    }));\n\n  // the zoom feature needs backups and timeMarkers regardless of if there is a trace duration\n  modelview.spansBackup = modelview.spans;\n  modelview.timeMarkers = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0].map((p, index) => ({\n    index,\n    time: mkDurationStr(duration * p),\n  }));\n  modelview.timeMarkersBackup = modelview.timeMarkers;\n\n  modelview.duration = duration;\n  modelview.durationStr = mkDurationStr(duration);\n\n  return modelview;\n}\n"
  },
  {
    "path": "zipkin-lens/src/zipkin/trace.test.js",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { describe, it, expect } from 'vitest';\nimport {\n  traceSummary,\n  traceSummaries,\n  mkDurationStr,\n  detailedTraceSummary,\n} from './trace';\nimport { SpanNode } from './span-node';\nimport { clean } from './span-cleaner';\nimport { treeCorrectedForClockSkew } from './clock-skew';\nimport yelpTrace from '../../testdata/yelp.json';\n\nconst frontend = {\n  serviceName: 'frontend',\n  ipv4: '172.17.0.13',\n};\n\nconst backend = {\n  serviceName: 'backend',\n  ipv4: '172.17.0.9',\n};\n\nconst httpTrace = [\n  {\n    traceId: 'bb1f0e21882325b8',\n    parentId: 'bb1f0e21882325b8',\n    id: 'c8c50ebd2abc179e',\n    kind: 'CLIENT',\n    name: 'get',\n    timestamp: 1541138169297572,\n    duration: 111121,\n    localEndpoint: frontend,\n    annotations: [\n      { value: 'ws', timestamp: 1541138169337695 },\n      { value: 'wr', timestamp: 1541138169368570 },\n    ],\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n    },\n  },\n  {\n    traceId: 'bb1f0e21882325b8',\n    id: 'bb1f0e21882325b8',\n    kind: 'SERVER',\n    name: 'get /',\n    timestamp: 1541138169255688,\n    duration: 168731,\n    localEndpoint: frontend,\n    remoteEndpoint: {\n      ipv4: '110.170.201.178',\n      port: 63678,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/',\n      'mvc.controller.class': 'Frontend',\n      'mvc.controller.method': 'callBackend',\n    },\n  },\n  {\n    traceId: 'bb1f0e21882325b8',\n    parentId: 'bb1f0e21882325b8',\n    id: 'c8c50ebd2abc179e',\n    kind: 'SERVER',\n    name: 'get /api',\n    timestamp: 1541138169377997, // this is actually skewed right, but we can't correct it\n    duration: 26326,\n    localEndpoint: backend,\n    remoteEndpoint: {\n      ipv4: '172.17.0.13',\n      port: 63679,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n      'mvc.controller.class': 'Backend',\n      'mvc.controller.method': 'printDate',\n    },\n    shared: true,\n  },\n];\n\nconst missingLocalEndpointTrace = [\n  {\n    traceId: '0b3ba16a811130ed',\n    parentId: '0b3ba16a811130ed',\n    id: '37c9d5cfc985592b',\n    kind: 'SERVER',\n    name: 'get',\n    timestamp: 1587434790009238,\n    duration: 10803,\n    remoteEndpoint: {\n      ipv4: '127.0.0.1',\n      port: 64529,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n      'mvc.controller.class': 'Backend',\n    },\n    shared: true,\n  },\n  {\n    traceId: '0b3ba16a811130ed',\n    parentId: '0b3ba16a811130ed',\n    id: '37c9d5cfc985592b',\n    kind: 'CLIENT',\n    name: 'get',\n    timestamp: 1587434789956001,\n    duration: 74362,\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/api',\n    },\n  },\n  {\n    traceId: '0b3ba16a811130ed',\n    id: '0b3ba16a811130ed',\n    kind: 'SERVER',\n    name: 'get',\n    timestamp: 1587434789934510,\n    duration: 103410,\n    remoteEndpoint: {\n      ipv6: '::1',\n      port: 64528,\n    },\n    tags: {\n      'http.method': 'GET',\n      'http.path': '/',\n      'mvc.controller.class': 'Frontend',\n    },\n  },\n];\n\n// renders data into a tree for traceMustache\nconst cleanedHttpTrace = treeCorrectedForClockSkew(httpTrace);\nconst cleanedMissingLocalEndpointTrace = treeCorrectedForClockSkew(\n  missingLocalEndpointTrace,\n);\n\ndescribe('traceSummary', () => {\n  it('should classify durations local to the endpoint', () => {\n    expect(traceSummary(cleanedHttpTrace).groupedTimestamps).toEqual({\n      frontend: [\n        { timestamp: 1541138169255688, duration: 168731 },\n        { timestamp: 1541138169297572, duration: 111121 },\n      ],\n      backend: [{ timestamp: 1541138169377997, duration: 26326 }],\n    });\n  });\n\n  // Ex netflix sometimes add annotations with no duration\n  it('should backfill incomplete duration as zero instead of undefined', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        id: '2480ccca8df0fca5',\n        kind: 'CLIENT',\n        timestamp: 1541138169297572,\n        duration: 111121,\n        localEndpoint: frontend,\n      }),\n    );\n    testTrace.addChild(\n      new SpanNode(\n        clean({\n          traceId: '2480ccca8df0fca5',\n          parentId: '2480ccca8df0fca5',\n          id: 'bf396325699c84bf',\n          timestamp: 1541138169377997,\n          localEndpoint: backend,\n        }),\n      ),\n    );\n\n    expect(traceSummary(testTrace).groupedTimestamps).toEqual({\n      frontend: [{ timestamp: 1541138169297572, duration: 111121 }],\n      backend: [{ timestamp: 1541138169377997, duration: 0 }],\n    });\n  });\n\n  it('should throw error on trace missing timestamp', () => {\n    let error;\n    try {\n      traceSummary(\n        new SpanNode(\n          clean({\n            traceId: '1e223ff1f80f1c69',\n            id: 'bf396325699c84bf',\n          }),\n        ),\n      );\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error.message).toEqual(\n      'Trace 1e223ff1f80f1c69 is missing a timestamp',\n    );\n  });\n\n  it('calculates timestamp and duration', () => {\n    const summary = traceSummary(cleanedHttpTrace);\n    expect(summary.timestamp).toBe(cleanedHttpTrace.span.timestamp);\n    expect(summary.duration).toBe(cleanedHttpTrace.span.duration);\n  });\n\n  it('should get span count', () => {\n    const summary = traceSummary(cleanedHttpTrace);\n    expect(summary.spanCount).toBe(httpTrace.length);\n  });\n});\n\ndescribe('traceSummariesToMustache', () => {\n  const summary = traceSummary(cleanedHttpTrace);\n\n  it('should return empty list for empty list', () => {\n    expect(traceSummaries(null, [])).toEqual([]);\n  });\n\n  it('should not change unit of timestamp or duration', () => {\n    const model = traceSummaries(null, [summary]);\n    expect(model[0].timestamp).toBe(summary.timestamp);\n    expect(model[0].duration).toBe(summary.duration);\n  });\n\n  it('should render empty serviceSummaries when spans lack localEndpoint', () => {\n    const model = traceSummaries(null, [\n      traceSummary(cleanedMissingLocalEndpointTrace),\n    ]);\n    expect(model[0].serviceSummaries).toEqual([]);\n  });\n\n  it('should get service summaries, ordered descending by max span duration', () => {\n    const model = traceSummaries(null, [summary]);\n    expect(model[0].serviceSummaries).toEqual([\n      {\n        serviceName: 'frontend',\n        spanCount: 2,\n      },\n      { serviceName: 'backend', spanCount: 1 },\n    ]);\n  });\n\n  it('should pass on the trace id', () => {\n    const model = traceSummaries('backend', [summary]);\n    expect(model[0].traceId).toBe(summary.traceId);\n  });\n\n  it('should format start time', () => {\n    const model = traceSummaries(null, [summary], true);\n    expect(model[0].startTs).toBe('11-02-2018T05:56:09.255+0000');\n  });\n\n  it('should format duration', () => {\n    const model = traceSummaries(null, [summary]);\n    expect(model[0].durationStr).toBe('168.731ms');\n  });\n\n  it('should calculate the width in percent', () => {\n    const start = 1;\n    const summary1 = {\n      traceId: 'cafebaby',\n      timestamp: start,\n      duration: 2000,\n      groupedTimestamps: {\n        backend: [{ timestamp: start + 1, duration: 2000 }],\n      },\n    };\n    const summary2 = {\n      traceId: 'cafedead',\n      timestamp: start,\n      duration: 20000,\n      groupedTimestamps: {\n        backend: [{ timestamp: start, duration: 20000 }],\n      },\n    };\n\n    // Model is ordered by duration, and the width should be relative (percentage)\n    const model = traceSummaries(null, [summary1, summary2]);\n    expect(model[0].width).toBe(100);\n    expect(model[1].width).toBe(10);\n  });\n\n  it('should pass on timestamp', () => {\n    const model = traceSummaries(null, [summary]);\n    expect(model[0].timestamp).toBe(summary.timestamp);\n  });\n\n  it('should get correct spanCount', () => {\n    const testSummary = traceSummary(cleanedHttpTrace);\n    const [model] = traceSummaries(null, [testSummary]);\n    expect(model.spanCount).toBe(httpTrace.length);\n  });\n\n  it('should order traces by duration and tie-break using trace id', () => {\n    const traceId1 = '9ed44141f679130b';\n    const traceId2 = '6ff1c14161f7bde1';\n    const traceId3 = '1234561234561234';\n    const summary1 = traceSummary(\n      new SpanNode(\n        clean({\n          traceId: traceId1,\n          name: 'get',\n          id: '6ff1c14161f7bde1',\n          timestamp: 1457186441657000,\n          duration: 4000,\n        }),\n      ),\n    );\n    const summary2 = traceSummary(\n      new SpanNode(\n        clean({\n          traceId: traceId2,\n          name: 'get',\n          id: '9ed44141f679130b',\n          timestamp: 1457186568026000,\n          duration: 4000,\n        }),\n      ),\n    );\n    const summary3 = traceSummary(\n      new SpanNode(\n        clean({\n          traceId: traceId3,\n          name: 'get',\n          id: '6677567324735',\n          timestamp: 1457186568027000,\n          duration: 3000,\n        }),\n      ),\n    );\n\n    const model = traceSummaries(null, [summary1, summary2, summary3]);\n    expect(model[0].traceId).toBe(traceId2);\n    expect(model[1].traceId).toBe(traceId1);\n    expect(model[2].traceId).toBe(traceId3);\n  });\n});\n\ndescribe('mkDurationStr', () => {\n  it('should return 0ms on zero duration', () => {\n    expect(mkDurationStr(0)).toBe('0ms');\n  });\n\n  it('should return empty string on undefined duration', () => {\n    expect(mkDurationStr()).toBe('');\n  });\n\n  it('should format microseconds', () => {\n    expect(mkDurationStr(3)).toBe('3μs');\n  });\n\n  it('should format ms', () => {\n    expect(mkDurationStr(1500)).toBe('1.500ms');\n  });\n\n  it('should format exact ms', () => {\n    expect(mkDurationStr(15000)).toBe('15ms');\n  });\n\n  it('should format seconds', () => {\n    expect(mkDurationStr(2534999)).toBe('2.535s');\n  });\n});\n\nconst cleanedYelpTrace = treeCorrectedForClockSkew(yelpTrace);\n\ndescribe('detailedTraceSummary', () => {\n  it('should derive summary info', () => {\n    const { traceId, durationStr, depth, serviceNameAndSpanCounts, rootSpan } =\n      detailedTraceSummary(cleanedHttpTrace);\n\n    expect(traceId).toBe('bb1f0e21882325b8');\n    expect(durationStr).toBe('168.731ms');\n    expect(depth).toBe(2); // number of span rows (distinct span IDs)\n    expect(serviceNameAndSpanCounts).toEqual([\n      { serviceName: 'backend', spanCount: 1 },\n      { serviceName: 'frontend', spanCount: 2 },\n    ]);\n    expect(rootSpan).toEqual({\n      serviceName: 'frontend',\n      spanName: 'get /',\n    });\n  });\n\n  it('should position incomplete spans at the correct offset', () => {\n    const { spans } = detailedTraceSummary(cleanedYelpTrace);\n\n    // the absolute values are not important, just checks that only the root span is at offset 0\n    expect(spans.map((s) => s.left)[0]).toEqual(0);\n  });\n\n  it('should derive summary info even when headless', () => {\n    const headless = new SpanNode(); // headless as there's no root span\n\n    // make a copy of the cleaned http trace as adding a child is a mutation\n    treeCorrectedForClockSkew(httpTrace).children.forEach((child) =>\n      headless.addChild(child),\n    );\n\n    const { traceId, durationStr, depth, serviceNameAndSpanCounts, rootSpan } =\n      detailedTraceSummary(headless);\n\n    expect(traceId).toBe('bb1f0e21882325b8');\n    expect(durationStr).toBe('111.121ms'); // client duration\n    expect(depth).toBe(1); // number of span rows (distinct span IDs)\n    expect(serviceNameAndSpanCounts).toEqual([\n      { serviceName: 'backend', spanCount: 1 },\n      { serviceName: 'frontend', spanCount: 1 },\n    ]);\n    expect(rootSpan).toEqual({\n      serviceName: 'frontend',\n      spanName: 'get',\n    });\n  });\n\n  it('should show human-readable annotation name', () => {\n    const {\n      spans: [testSpan],\n    } = detailedTraceSummary(cleanedHttpTrace);\n    expect(testSpan.annotations[0].value).toBe('Server Start');\n    expect(testSpan.annotations[1].value).toBe('Server Finish');\n    expect(testSpan.tags[4].key).toBe('Client Address');\n  });\n\n  it('should tolerate spans without annotations', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        name: 'get',\n        id: '2480ccca8df0fca5',\n        timestamp: 1457186385375000,\n        duration: 333000,\n        localEndpoint: {\n          serviceName: 'zipkin-query',\n          ipv4: '127.0.0.1',\n          port: 9411,\n        },\n        tags: { lc: 'component' },\n      }),\n    );\n    const {\n      spans: [testSpan],\n    } = detailedTraceSummary(testTrace);\n    expect(testSpan.tags[0].key).toBe('Local Component');\n  });\n\n  it('should not include empty Local Component annotations', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        name: 'get',\n        id: '2480ccca8df0fca5',\n        timestamp: 1457186385375000,\n        duration: 333000,\n        localEndpoint: {\n          serviceName: 'zipkin-query',\n          ipv4: '127.0.0.1',\n          port: 9411,\n        },\n      }),\n    );\n    const {\n      spans: [testSpan],\n    } = detailedTraceSummary(testTrace);\n    // skips empty Local Component, but still shows it as an address\n    expect(testSpan.tags[0].key).toBe('Local Address');\n  });\n\n  it('should tolerate spans without tags', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        name: 'get',\n        id: '2480ccca8df0fca5',\n        kind: 'SERVER',\n        timestamp: 1457186385375000,\n        duration: 333000,\n        localEndpoint: {\n          serviceName: 'zipkin-query',\n          ipv4: '127.0.0.1',\n          port: 9411,\n        },\n      }),\n    );\n    const {\n      spans: [testSpan],\n    } = detailedTraceSummary(testTrace);\n    expect(testSpan.annotations[0].value).toBe('Server Start');\n    expect(testSpan.annotations[1].value).toBe('Server Finish');\n  });\n\n  // TODO: we should really only allocate remote endpoints when on an uninstrumented link\n  it('should count spans for any endpoint', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        id: '2480ccca8df0fca5',\n        kind: 'CLIENT',\n        timestamp: 1,\n        localEndpoint: frontend,\n        remoteEndpoint: frontend,\n      }),\n    );\n    testTrace.addChild(\n      new SpanNode(\n        clean({\n          traceId: '2480ccca8df0fca5',\n          parentId: '2480ccca8df0fca5',\n          id: 'bf396325699c84bf',\n          name: 'foo',\n          timestamp: 2,\n          localEndpoint: backend,\n        }),\n      ),\n    );\n\n    const { serviceNameAndSpanCounts } = detailedTraceSummary(testTrace);\n    expect(serviceNameAndSpanCounts).toEqual([\n      { serviceName: 'backend', spanCount: 1 },\n      { serviceName: 'frontend', spanCount: 1 },\n    ]);\n  });\n\n  it('should count spans with no timestamp or duration', () => {\n    const testTrace = new SpanNode(\n      clean({\n        traceId: '2480ccca8df0fca5',\n        id: '2480ccca8df0fca5',\n        kind: 'CLIENT',\n        timestamp: 1, // root always needs a timestamp\n        localEndpoint: frontend,\n        remoteEndpoint: frontend,\n      }),\n    );\n    testTrace.addChild(\n      new SpanNode(\n        clean({\n          traceId: '2480ccca8df0fca5',\n          parentId: '2480ccca8df0fca5',\n          id: 'bf396325699c84bf',\n          name: 'foo',\n          localEndpoint: backend,\n        }),\n      ),\n    );\n\n    const { serviceNameAndSpanCounts } = detailedTraceSummary(testTrace);\n    expect(serviceNameAndSpanCounts).toEqual([\n      { serviceName: 'backend', spanCount: 1 },\n      { serviceName: 'frontend', spanCount: 1 },\n    ]);\n  });\n\n  /*\n   * The input data to the trace view is already sorted by timestamp. Span rows need to be added in\n   * depth-first order to ensure they can be collapsed by parent.\n   *\n   *          a\n   *        / | \\\n   *       b  c  d\n   *      /|\\\n   *     e f 1\n   *          \\\n   *           2\n   */\n  it('should order spans by timestamp, root first, not by cause', () => {\n    // to prevent invalid trace errors, we need a timestamp on the root span.\n    const a = new SpanNode(clean({ traceId: '1', id: 'a', timestamp: 1 }));\n    const b = new SpanNode(clean({ traceId: '1', id: 'b', parentId: 'a' }));\n    const c = new SpanNode(clean({ traceId: '1', id: 'c', parentId: 'a' }));\n    const d = new SpanNode(clean({ traceId: '1', id: 'd', parentId: 'a' }));\n    // root(a) has children b, c, d\n    a.addChild(b);\n    a.addChild(c);\n    a.addChild(d);\n    const e = new SpanNode(clean({ traceId: '1', id: 'e', parentId: 'b' }));\n    const f = new SpanNode(clean({ traceId: '1', id: 'f', parentId: 'b' }));\n    const g = new SpanNode(clean({ traceId: '1', id: '1', parentId: 'b' }));\n    // child(b) has children e, f, g\n    b.addChild(e);\n    b.addChild(f);\n    b.addChild(g);\n    const h = new SpanNode(clean({ traceId: '1', id: '2', parentId: '1' }));\n    // f has no children\n    // child(g) has child h\n    g.addChild(h);\n\n    const { spans } = detailedTraceSummary(a);\n    expect(spans.map((s) => s.spanId)).toEqual([\n      '000000000000000a',\n      '000000000000000b',\n      '000000000000000e',\n      '000000000000000f',\n      '0000000000000001',\n      '0000000000000002',\n      '000000000000000c',\n      '000000000000000d',\n    ]);\n  });\n\n  it('should order rows by topologically, then timestamp when endpoints are inconsistent', () => {\n    const traceWithEndpointProblems = [\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: '5',\n        name: 'get /header',\n        timestamp: 359175,\n        duration: 108123,\n        localEndpoint: {\n          serviceName: 'sad_pages',\n          ipv4: '10.1.1.123',\n          port: 31107,\n        },\n      },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: '4',\n        name: 'get /footer',\n        timestamp: 341172,\n        duration: 16640,\n        localEndpoint: {\n          serviceName: 'sad_pages',\n          ipv4: '10.1.1.123',\n          port: 31107,\n        },\n      },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: '3',\n        kind: 'CLIENT',\n        name: 'get',\n        timestamp: 30000,\n        duration: 123000,\n        localEndpoint: {\n          serviceName: 'sad_pages',\n          ipv4: '169.254.0.3',\n          port: 43193,\n        },\n      },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: '2',\n        kind: 'SERVER',\n        name: 'get /token',\n        timestamp: 12137,\n        duration: 12785,\n        localEndpoint: {\n          serviceName: 'phantom',\n          ipv4: '10.1.1.123',\n          port: 32767,\n        },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        parentId: '1',\n        id: '2',\n        kind: 'CLIENT',\n        name: 'get',\n        timestamp: 11000,\n        duration: 13000,\n        localEndpoint: {\n          serviceName: 'sad_pages',\n          ipv4: '169.254.0.2',\n          port: 49647,\n        },\n      },\n      {\n        traceId: 'a',\n        id: '1',\n        kind: 'SERVER',\n        name: 'get /sad',\n        timestamp: 4857,\n        duration: 525280,\n        localEndpoint: {\n          serviceName: 'sad_pages',\n          ipv4: '10.1.1.123',\n          port: 31107,\n        },\n        shared: true,\n      },\n      {\n        traceId: 'a',\n        id: '1',\n        kind: 'CLIENT',\n        name: 'get',\n        timestamp: 1,\n        duration: 537000,\n        localEndpoint: {\n          serviceName: 'whelp-main',\n          ipv4: '169.254.0.1',\n          port: 43237,\n        },\n      },\n    ];\n\n    const { spans } = detailedTraceSummary(\n      treeCorrectedForClockSkew(traceWithEndpointProblems),\n    );\n    expect(spans.map((s) => s.timestamp)).toEqual([\n      1,\n      11000,\n      30000,\n      341172,\n      359175, // increasing order\n    ]);\n  });\n});\n"
  },
  {
    "path": "zipkin-lens/testdata/README.md",
    "content": "# Test json\nFor testing the UI, you can simply POST or paste json in this directory.\n\nFor example, assuming you started zipkin allowing it to see up to 5 year old data `QUERY_LOOKBACK=157784630000 java -jar zipkin.jar`\n```bash\n$ curl -X POST -s localhost:9411/api/v2/spans -H'Content-Type: application/json' -d @yelp.json\n$ open \"http://localhost:9411/zipkin/?lookback=range&endTs=$(date +%s)000&startTs=0&limit=1\"\n```\n\n\n## Data File Explanations\n\n### smartthings-oauth-authorization.json\n\n\nThis trace captures an OAuth flow that integrates with a 3rd party. So it leaves our system and comes back.\n\nWe add trace propagation across web redirects to tie several systems together, especially when they involve round trips to other web sites we don’t own. Further we add user IDs to these traces to investigate specific customer issues or complaints. Lastly we also embed the trace context in things like oauth authorization codes so we can tie a full user actions together even when part of that action is performed on back end systems we don’t own.\n\n### smartthings-mobile-web-install.json\n\nThis trace captures a user that is being directed to a embedded web experience on a mobile device through a multi step install process.\n\nThere are clusters of requests the long pauses in between are the user interacting with the page.\n\nThe end of the trace is the internal events noting the install starting to flow through the system.\n"
  },
  {
    "path": "zipkin-lens/testdata/ascend.json",
    "content": "[\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ef86c83c0a05a6d6\",\n        \"id\": \"ecc00062ceef4bf0\",\n        \"kind\": \"CLIENT\",\n        \"name\": \"get\",\n        \"timestamp\": 1531282088548312,\n        \"duration\": 20333,\n        \"localEndpoint\": {\n            \"serviceName\": \"mobile-gateway\",\n            \"ipv4\": \"172.17.0.13\"\n        },\n        \"tags\": {\n            \"http.method\": \"GET\",\n            \"http.path\": \"/content/personal\"\n        }\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ecc00062ceef4bf0\",\n        \"id\": \"c21c6c51ac71b3ba\",\n        \"kind\": \"CLIENT\",\n        \"name\": \"get\",\n        \"timestamp\": 1531282088556600,\n        \"duration\": 8965,\n        \"localEndpoint\": {\n            \"serviceName\": \"content-service\",\n            \"ipv4\": \"172.17.0.10\"\n        },\n        \"tags\": {\n            \"http.method\": \"GET\",\n            \"http.path\": \"/api/v1/banners/10000001502\"\n        }\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ef86c83c0a05a6d6\",\n        \"id\": \"ecc00062ceef4bf0\",\n        \"kind\": \"SERVER\",\n        \"name\": \"get /content/personal\",\n        \"timestamp\": 1531282088552037,\n        \"duration\": 15888,\n        \"localEndpoint\": {\n            \"serviceName\": \"content-service\",\n            \"ipv4\": \"172.17.0.10\"\n        },\n        \"remoteEndpoint\": {\n            \"ipv4\": \"10.230.16.208\",\n            \"port\": 3400\n        },\n        \"tags\": {\n            \"http.method\": \"GET\",\n            \"http.path\": \"/content/personal\",\n            \"mvc.controller.class\": \"GenericController\",\n            \"mvc.controller.method\": \"getPersonalGeneric\"\n        },\n        \"shared\": true\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ef86c83c0a05a6d6\",\n        \"id\": \"52b1ab4956917c39\",\n        \"kind\": \"CLIENT\",\n        \"name\": \"get\",\n        \"timestamp\": 1531282088533834,\n        \"duration\": 14077,\n        \"localEndpoint\": {\n            \"serviceName\": \"mobile-gateway\",\n            \"ipv4\": \"172.17.0.13\"\n        },\n        \"tags\": {\n            \"http.method\": \"GET\",\n            \"http.path\": \"/auth-service/v1/authentications/\"\n        }\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ef86c83c0a05a6d6\",\n        \"id\": \"52b1ab4956917c39\",\n        \"kind\": \"SERVER\",\n        \"name\": \"http:/parent/auth-service/v1/authentications/\",\n        \"timestamp\": 1531282088553000,\n        \"duration\": 4157,\n        \"localEndpoint\": {\n            \"serviceName\": \"auth-service\",\n            \"ipv4\": \"172.17.0.9\",\n            \"port\": 8080\n        },\n        \"tags\": {\n            \"http.host\": \"auth-service-alp\",\n            \"http.method\": \"GET\",\n            \"http.path\": \"/auth-service/v1/authentications/\",\n            \"http.url\": \"http://auth-service-alp/auth-service/v1/authentications/\",\n            \"mvc.controller.class\": \"AuthenticationController\",\n            \"mvc.controller.method\": \"authenticationsWithAuthorizationHeader\",\n            \"spring.instance_id\": \"bb52cfc01466:auth-service\"\n        },\n        \"shared\": true\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"id\": \"ef86c83c0a05a6d6\",\n        \"kind\": \"SERVER\",\n        \"name\": \"get\",\n        \"timestamp\": 1531282088533035,\n        \"duration\": 38793,\n        \"localEndpoint\": {\n            \"serviceName\": \"mobile-gateway\",\n            \"ipv4\": \"172.17.0.13\"\n        },\n        \"remoteEndpoint\": {\n            \"ipv4\": \"110.170.201.178\",\n            \"port\": 39308\n        },\n        \"tags\": {\n            \"http.method\": \"GET\",\n            \"http.path\": \"/mobile-gateway/content/personal\"\n        }\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"52b1ab4956917c39\",\n        \"id\": \"80c0d1d62f437a1b\",\n        \"name\": \"extend-token-life\",\n        \"timestamp\": 1531282088553000,\n        \"duration\": 1291,\n        \"localEndpoint\": {\n            \"serviceName\": \"auth-service\",\n            \"ipv4\": \"172.17.0.9\",\n            \"port\": 8080\n        },\n        \"tags\": {\n            \"class\": \"AuthenticationService\",\n            \"lc\": \"async\",\n            \"method\": \"extendToken\"\n        }\n    },\n    {\n        \"traceId\": \"ef86c83c0a05a6d6\",\n        \"parentId\": \"ecc00062ceef4bf0\",\n        \"id\": \"e6422f7ff7d78099\",\n        \"name\": \"hystrix\",\n        \"timestamp\": 1531282088555338,\n        \"duration\": 863,\n        \"localEndpoint\": {\n            \"serviceName\": \"content-service\",\n            \"ipv4\": \"172.17.0.10\"\n        }\n    }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/envoy.json",
    "content": "[\n  {\n    \"traceId\": \"978883983d506fa5\",\n    \"id\": \"978883983d506fa5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"localhost:10000\",\n    \"timestamp\": 1570523661059232,\n    \"duration\": 127115,\n    \"localEndpoint\": {\n      \"ipv4\": \"169.254.65.45\"\n    },\n    \"tags\": {\n      \"component\": \"proxy\",\n      \"downstream_cluster\": \"-\",\n      \"guid:x-request-id\": \"d709b78f-583e-988e-b581-25b72c63852d\",\n      \"http.method\": \"GET\",\n      \"http.protocol\": \"HTTP/1.1\",\n      \"http.status_code\": \"200\",\n      \"http.url\": \"http://www.google.com/\",\n      \"request_size\": \"0\",\n      \"response_flags\": \"-\",\n      \"response_size\": \"67342\",\n      \"upstream_cluster\": \"service_google\",\n      \"user_agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36\"\n    },\n    \"shared\": true\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/messaging-kafka.json",
    "content": "[\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"id\": \"0562809467078eab\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397200023,\n    \"duration\": 26,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"0562809467078eab\",\n    \"id\": \"09fdf808ef0d60c0\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397200002,\n    \"duration\": 252090,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"0562809467078eab\",\n    \"id\": \"e73d7bc0194a76e0\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397452002,\n    \"duration\": 223097,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"0562809467078eab\",\n    \"id\": \"d1b4806e7e9eed25\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397675002,\n    \"duration\": 171798,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"09fdf808ef0d60c0\",\n    \"id\": \"5141af930f03274e\",\n    \"name\": \"handle\",\n    \"timestamp\": 1541405397413496,\n    \"duration\": 866,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"tags\": {\n      \"class\": \"Projector\",\n      \"method\": \"handle\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"09fdf808ef0d60c0\",\n    \"id\": \"2e22a0564764943a\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397450652,\n    \"duration\": 1471,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"09fdf808ef0d60c0\",\n    \"id\": \"26bd982d53f50d1f\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397451227,\n    \"duration\": 102480,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"central.messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"e73d7bc0194a76e0\",\n    \"id\": \"556955e15da195f5\",\n    \"name\": \"handle\",\n    \"timestamp\": 1541405397655018,\n    \"duration\": 1255,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"tags\": {\n      \"class\": \"Projector\",\n      \"method\": \"handle\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"e73d7bc0194a76e0\",\n    \"id\": \"5133caca649f802b\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397674703,\n    \"duration\": 1035,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"e73d7bc0194a76e0\",\n    \"id\": \"b7808dc69c7726f2\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397674873,\n    \"duration\": 957,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"central.messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"d1b4806e7e9eed25\",\n    \"id\": \"97a1159e0eb74759\",\n    \"name\": \"handle\",\n    \"timestamp\": 1541405397827569,\n    \"duration\": 774,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"tags\": {\n      \"class\": \"Projector\",\n      \"method\": \"handle\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"d1b4806e7e9eed25\",\n    \"id\": \"085a9df516127e50\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397846422,\n    \"duration\": 1259,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"d1b4806e7e9eed25\",\n    \"id\": \"5bc03264c704f245\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397846576,\n    \"duration\": 803,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.key\": \"e1b2f8b5-dd1e-11e8-93d1-0050568b53f5\",\n      \"kafka.topic\": \"central.messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"5141af930f03274e\",\n    \"id\": \"79145dc8245c2899\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397414201,\n    \"duration\": 3268,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"command-messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"2e22a0564764943a\",\n    \"id\": \"b4a2a87ee7f003bc\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397452008,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"2e22a0564764943a\",\n    \"id\": \"e0473dc3d8732e8b\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397847007,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"556955e15da195f5\",\n    \"id\": \"e2feae695b1d1a6a\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397655722,\n    \"duration\": 937,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"command-messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"5133caca649f802b\",\n    \"id\": \"a5524eec8147b1da\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397676009,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"5133caca649f802b\",\n    \"id\": \"08e980b0e786309b\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397848003,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"97a1159e0eb74759\",\n    \"id\": \"b42b60c5c19ce714\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"send\",\n    \"timestamp\": 1541405397827998,\n    \"duration\": 746,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"command-messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"085a9df516127e50\",\n    \"id\": \"bad2ddec9d33837b\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397848002,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"e0473dc3d8732e8b\",\n    \"id\": \"d77b1306b69e39a5\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397848002,\n    \"duration\": 149,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"085a9df516127e50\",\n    \"id\": \"519d27c657334615\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"poll\",\n    \"timestamp\": 1541405397848005,\n    \"duration\": 1,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"kafka\"\n    },\n    \"tags\": {\n      \"kafka.topic\": \"messages\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"b4a2a87ee7f003bc\",\n    \"id\": \"2f77d5b0b8e0de35\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397453001,\n    \"duration\": 310,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"tags\": {\n      \"error\": \"some error\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"a5524eec8147b1da\",\n    \"id\": \"4a64a63e4b58e665\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397676001,\n    \"duration\": 265,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"tags\": {\n      \"error\": \"some error\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"08e980b0e786309b\",\n    \"id\": \"630c1215f2e430a7\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397849002,\n    \"duration\": 65,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"bad2ddec9d33837b\",\n    \"id\": \"d5d4778b19cf7e93\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397849001,\n    \"duration\": 45,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\"\n    }\n  },\n  {\n    \"traceId\": \"0562809467078eab\",\n    \"parentId\": \"519d27c657334615\",\n    \"id\": \"568b33e6af8a225a\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1541405397848001,\n    \"duration\": 251,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\"\n    },\n    \"tags\": {\n      \"error\": \"\"\n    }\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/messaging.json",
    "content": "[\n  {\n    \"traceId\": \"5aab74dbb904746bb33447baae403ed6\",\n    \"parentId\": \"05e3ac9a4f6e3b90\",\n    \"id\": \"e457b5a2e4d86bd1\",\n    \"kind\": \"CONSUMER\",\n    \"name\": \"next-message\",\n    \"timestamp\": 1521186011929043,\n    \"duration\": 14,\n    \"localEndpoint\": {\n      \"serviceName\": \"backend\",\n      \"ipv4\": \"192.168.0.10\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"rabbitmq\"\n    },\n    \"tags\": {\n      \"rabbit.exchange\": \"\",\n      \"rabbit.queue\": \"backend\",\n      \"rabbit.routing_key\": \"backend\"\n    }\n  },\n  {\n    \"traceId\": \"5aab74dbb904746bb33447baae403ed6\",\n    \"parentId\": \"e457b5a2e4d86bd1\",\n    \"id\": \"4ad2db84ac76def7\",\n    \"name\": \"on-message\",\n    \"timestamp\": 1521186011929105,\n    \"duration\": 371,\n    \"localEndpoint\": {\n      \"serviceName\": \"backend\",\n      \"ipv4\": \"192.168.0.10\"\n    }\n  },\n  {\n    \"traceId\": \"5aab74dbb904746bb33447baae403ed6\",\n    \"parentId\": \"b33447baae403ed6\",\n    \"id\": \"05e3ac9a4f6e3b90\",\n    \"kind\": \"PRODUCER\",\n    \"name\": \"publish\",\n    \"timestamp\": 1521186011927475,\n    \"duration\": 13,\n    \"localEndpoint\": {\n      \"serviceName\": \"frontend\",\n      \"ipv4\": \"192.168.0.10\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"rabbitmq\"\n    }\n  },\n  {\n    \"traceId\": \"5aab74dbb904746bb33447baae403ed6\",\n    \"id\": \"b33447baae403ed6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /\",\n    \"timestamp\": 1521186011926119,\n    \"duration\": 2839,\n    \"localEndpoint\": {\n      \"serviceName\": \"frontend\",\n      \"ipv4\": \"192.168.0.10\"\n    },\n    \"remoteEndpoint\": {\n      \"ipv6\": \"::1\",\n      \"port\": 54602\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/\",\n      \"mvc.controller.class\": \"Frontend\",\n      \"mvc.controller.method\": \"callBackend\"\n    }\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/messaging2.json",
    "content": "[\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0153a596db406c83\",\n      \"id\": \"939e2fdc360a1201\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post\",\n      \"timestamp\": 1540799213008149,\n      \"duration\": 779592,\n      \"localEndpoint\": {\n        \"serviceName\": \"transfer-service\",\n        \"ipv4\": \"172.17.0.7\"\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/core-service/transfer\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0153a596db406c83\",\n      \"id\": \"dfdfa6bdfc538d3e\",\n      \"name\": \"push-topic-s-n-s\",\n      \"timestamp\": 1540799213922806,\n      \"duration\": 79027,\n      \"localEndpoint\": {\n        \"serviceName\": \"transfer-service\",\n        \"ipv4\": \"172.17.0.7\"\n      },\n      \"tags\": {\n        \"class\": \"P2PSnsPushTopicService\",\n        \"method\": \"pushTopicSNS\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"id\": \"0d1a94ebc9256244\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post\",\n      \"timestamp\": 1540799212976024,\n      \"duration\": 29051,\n      \"localEndpoint\": {\n        \"serviceName\": \"mobile-gateway\",\n        \"ipv4\": \"172.17.0.12\"\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"110.170.201.178\",\n        \"port\": 7978\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/mobile-gateway/transfer-service/transactions/5698ba57-f2f6-44e9-90f8-987e10353ed8/\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0d1a94ebc9256244\",\n      \"id\": \"dca1d27152851c1c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"http:/parent/auth-service/v1/authentications/\",\n      \"timestamp\": 1540799212978000,\n      \"duration\": 4057,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth-service\",\n        \"ipv4\": \"172.17.0.8\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.host\": \"auth-service\",\n        \"http.method\": \"GET\",\n        \"http.path\": \"/auth-service/v1/authentications/\",\n        \"http.url\": \"http://auth-service/v1/authentications/\",\n        \"mvc.controller.class\": \"AuthenticationController\",\n        \"mvc.controller.method\": \"authenticationsWithAuthorizationHeader\",\n        \"spring.instance_id\": \"69805c144e6c:auth-service\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"dca1d27152851c1c\",\n      \"id\": \"26242947380ec944\",\n      \"name\": \"extend-token\",\n      \"timestamp\": 1540799212978000,\n      \"duration\": 1241,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth-service\",\n        \"ipv4\": \"172.17.0.8\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"class\": \"AuthenticationService\",\n        \"lc\": \"async\",\n        \"method\": \"extendToken\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0153a596db406c83\",\n      \"id\": \"c779d4ff1adfc8f9\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get\",\n      \"timestamp\": 1540799213788437,\n      \"duration\": 132879,\n      \"localEndpoint\": {\n        \"serviceName\": \"transfer-service\",\n        \"ipv4\": \"172.17.0.7\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/internal-api/v1/customers/profiles-basic/agent/tryhxd090jhjhj90ccvsedr38288\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0153a596db406111\",\n      \"id\": \"3df9ceef91bbc631\",\n      \"kind\": \"PRODUCER\",\n      \"name\": \"publish\",\n      \"timestamp\": 1540799213922139,\n      \"duration\": 12,\n      \"localEndpoint\": {\n        \"serviceName\": \"transfer-service\",\n        \"ipv4\": \"172.17.0.7\"\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"rabbitmq\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0d1a94ebc9256244\",\n      \"id\": \"99a4a9084091114d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post\",\n      \"timestamp\": 1540799212983049,\n      \"duration\": 21813,\n      \"localEndpoint\": {\n        \"serviceName\": \"mobile-gateway\",\n        \"ipv4\": \"172.17.0.12\"\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/transfer-service/transactions/5698ba57-f2f6-44e9-90f8-987e10353ed8/\"\n      }\n    },\n    {\n      \"traceId\": \"0d1a94ebc9256244\",\n      \"parentId\": \"0153a596db406c83\",\n      \"id\": \"693fb64c3cf75cf3\",\n      \"name\": \"send-confirm-transfer\",\n      \"timestamp\": 1540799213921929,\n      \"duration\": 2555791,\n      \"localEndpoint\": {\n        \"serviceName\": \"transfer-service\",\n        \"ipv4\": \"172.17.0.7\"\n      },\n      \"tags\": {\n        \"class\": \"EmailService\",\n        \"method\": \"sendConfirmTransfer\"\n      }\n    },\n    {\n        \"traceId\": \"0d1a94ebc9256244\",\n        \"parentId\": \"0153a596db406111\",\n        \"id\": \"3df9ceef91bbc777\",\n        \"kind\": \"CONSUMER\",\n        \"name\": \"consume\",\n        \"timestamp\": 1540799214963929,\n        \"duration\": 40,\n        \"localEndpoint\": {\n          \"serviceName\": \"notification-service\",\n          \"ipv4\": \"172.17.0.7\"\n        },\n        \"remoteEndpoint\": {\n          \"serviceName\": \"rabbitmq\"\n        },\n        \"tags\": {\n            \"rabbit.exchange\": \"notification-exchange\",\n            \"rabbit.queue\": \"notification\"\n          }\n      },\n      {\n        \"traceId\": \"0d1a94ebc9256244\",\n        \"parentId\": \"3df9ceef91bbc777\",\n        \"id\": \"99a4a9084092224d\",\n        \"kind\": \"CLIENT\",\n        \"name\": \"post\",\n        \"timestamp\": 1540799214993929,\n        \"duration\": 15860,\n        \"localEndpoint\": {\n          \"serviceName\": \"notification-service\",\n          \"ipv4\": \"172.17.0.14\"\n        },\n        \"tags\": {\n          \"http.method\": \"POST\",\n          \"http.path\": \"/notification-service/send\"\n        }\n      }\n  ]\n"
  },
  {
    "path": "zipkin-lens/testdata/simple-db-p6.json",
    "content": "[\n  {\n    \"traceId\": \"19f84f102048e047\",\n    \"parentId\": \"19f84f102048e047\",\n    \"id\": \"5d532ef6fed52445\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"insert\",\n    \"timestamp\": 1541485537488973,\n    \"duration\": 2033,\n    \"localEndpoint\": {\n      \"serviceName\": \"bootifulmeters\",\n      \"ipv4\": \"172.23.250.16\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"app_db\"\n    },\n    \"tags\": {\n      \"sql.query\": \"insert into revinfo (rev, revtstmp) values (null, 1541485537483)\"\n    }\n  },\n  {\n    \"traceId\": \"19f84f102048e047\",\n    \"parentId\": \"19f84f102048e047\",\n    \"id\": \"e4efd924fec0624c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"insert\",\n    \"timestamp\": 1541485537470563,\n    \"duration\": 2414,\n    \"localEndpoint\": {\n      \"serviceName\": \"bootifulmeters\",\n      \"ipv4\": \"172.23.250.16\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"app_db\"\n    },\n    \"tags\": {\n      \"sql.query\": \"insert into api_book (book_price, book_title, book_id) values (12.25, 'Cloud Native Java', 10001)\"\n    }\n  },\n  {\n    \"traceId\": \"19f84f102048e047\",\n    \"parentId\": \"19f84f102048e047\",\n    \"id\": \"8b1e63c0f49a81f7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"call\",\n    \"timestamp\": 1541485537391810,\n    \"duration\": 6051,\n    \"localEndpoint\": {\n      \"serviceName\": \"bootifulmeters\",\n      \"ipv4\": \"172.23.250.16\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"app_db\"\n    },\n    \"tags\": {\n      \"sql.query\": \"call next value for hibernate_sequence\"\n    }\n  },\n  {\n    \"traceId\": \"19f84f102048e047\",\n    \"id\": \"19f84f102048e047\",\n    \"kind\": \"SERVER\",\n    \"name\": \"http:/book\",\n    \"timestamp\": 1541485537291342,\n    \"duration\": 252016,\n    \"localEndpoint\": {\n      \"serviceName\": \"bootifulmeters\",\n      \"ipv4\": \"172.23.250.16\"\n    },\n    \"remoteEndpoint\": {\n      \"ipv6\": \"::1\",\n      \"port\": 53367\n    },\n    \"tags\": {\n      \"http.host\": \"localhost\",\n      \"http.method\": \"POST\",\n      \"http.path\": \"/book\",\n      \"http.url\": \"http://localhost:9777/book\",\n      \"mvc.controller.class\": \"AppController\",\n      \"mvc.controller.method\": \"postBook\"\n    }\n  },\n  {\n    \"traceId\": \"19f84f102048e047\",\n    \"parentId\": \"19f84f102048e047\",\n    \"id\": \"544e854ede87b217\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"insert\",\n    \"timestamp\": 1541485537498305,\n    \"duration\": 1118,\n    \"localEndpoint\": {\n      \"serviceName\": \"bootifulmeters\",\n      \"ipv4\": \"172.23.250.16\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"app_db\"\n    },\n    \"tags\": {\n      \"sql.query\": \"insert into api_book_aud (revtype, book_price, book_title, book_id, rev) values (0, 12.25, 'Cloud Native Java', 10001, 97)\"\n    }\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/skew.json",
    "content": "[\n  {\n    \"traceId\": \"1e223ff1f80f1c69\",\n    \"parentId\": \"74280ae0c10d8062\",\n    \"id\": \"43210ae0c10d1234\",\n    \"name\": \"async\",\n    \"timestamp\": 1470150004008762,\n    \"duration\": 65000,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\",\n      \"ipv4\": \"192.0.0.0\"\n    }\n  },\n  {\n    \"traceId\": \"1e223ff1f80f1c69\",\n    \"parentId\": \"bf396325699c84bf\",\n    \"id\": \"74280ae0c10d8062\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1470150004008761,\n    \"duration\": 93577,\n    \"localEndpoint\": {\n      \"serviceName\": \"serviceb\",\n      \"ipv4\": \"192.0.0.0\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"1e223ff1f80f1c69\",\n    \"id\": \"bf396325699c84bf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1470150004071068,\n    \"duration\": 99411,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\",\n      \"ipv4\": \"127.0.0.0\"\n    }\n  },\n  {\n    \"traceId\": \"1e223ff1f80f1c69\",\n    \"parentId\": \"bf396325699c84bf\",\n    \"id\": \"74280ae0c10d8062\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1470150004074202,\n    \"duration\": 94539,\n    \"localEndpoint\": {\n      \"serviceName\": \"servicea\",\n      \"ipv4\": \"127.0.0.0\"\n    }\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/smartthings-mobile-web-install.json",
    "content": "[\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"a0dc23f22c4fee3f\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"017bfc73a014d42c\",\n    \"id\": \"60e1ace16723844a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /events\",\n    \"timestamp\": 1543549826259342,\n    \"duration\": 2120,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.83.20\",\n      \"port\": 8197\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549826261000,\n        \"value\": \"Body Part Received\"\n      },\n      {\n        \"timestamp\": 1543549826261000,\n        \"value\": \"Headers Received\"\n      },\n      {\n        \"timestamp\": 1543549826261000,\n        \"value\": \"Status Received\"\n      }\n    ],\n    \"tags\": {\n      \"http.url\": \"http://archiver.st.internal/events\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cbab506b53f336fc\",\n    \"id\": \"017bfc73a014d42c\",\n    \"name\": \"process.archiver\",\n    \"timestamp\": 1543549826259281,\n    \"duration\": 2231,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.83.20\",\n      \"port\": 8197\n    },\n    \"tags\": {\n      \"lc\": \"EventProcessor\",\n      \"pusher.eventType\": \"INSTALLED_APP_LIFECYCLE_EVENT\",\n      \"pusher.timeInKafka\": \"36\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"receive iot-events360\",\n    \"timestamp\": 1543549826258568,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.83.20\",\n      \"port\": 8197\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bouncer\"\n    },\n    \"tags\": {\n      \"kafka.partition\": \"276\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"receive iot-events360\",\n    \"timestamp\": 1543549826257913,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.81.206\",\n      \"port\": 8197\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bouncer\"\n    },\n    \"tags\": {\n      \"kafka.partition\": \"276\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c1c06fd706fca9cb\",\n    \"id\": \"10e33f54b04eac0b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /events\",\n    \"timestamp\": 1543549826241017,\n    \"duration\": 41509,\n    \"localEndpoint\": {\n      \"serviceName\": \"oreck\",\n      \"ipv4\": \"10.6.31.170\",\n      \"port\": 8230\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.88.250\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/events\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"receive iot-events360\",\n    \"timestamp\": 1543549826239391,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.88.250\",\n      \"port\": 8197\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bouncer\"\n    },\n    \"tags\": {\n      \"kafka.partition\": \"276\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c1c06fd706fca9cb\",\n    \"id\": \"10e33f54b04eac0b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /events\",\n    \"timestamp\": 1543549826238826,\n    \"duration\": 42796,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.88.250\",\n      \"port\": 8197\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549826282000,\n        \"value\": \"Body Part Received\"\n      },\n      {\n        \"timestamp\": 1543549826282000,\n        \"value\": \"Headers Received\"\n      },\n      {\n        \"timestamp\": 1543549826282000,\n        \"value\": \"Status Received\"\n      }\n    ],\n    \"tags\": {\n      \"http.url\": \"http://oreck.st.internal/events\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cbab506b53f336fc\",\n    \"id\": \"c1c06fd706fca9cb\",\n    \"name\": \"process.oreck\",\n    \"timestamp\": 1543549826238758,\n    \"duration\": 42910,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.88.250\",\n      \"port\": 8197\n    },\n    \"tags\": {\n      \"lc\": \"EventProcessor\",\n      \"pusher.eventType\": \"INSTALLED_APP_LIFECYCLE_EVENT\",\n      \"pusher.timeInKafka\": \"16\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"receive iot-events360\",\n    \"timestamp\": 1543549826238837,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.16.173\",\n      \"port\": 8197\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bouncer\"\n    },\n    \"tags\": {\n      \"kafka.partition\": \"276\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b6fdd2a6b74e32e5\",\n    \"id\": \"7c4eb808223a2034\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /deliver/\",\n    \"timestamp\": 1543549826232010,\n    \"duration\": 4350232,\n    \"localEndpoint\": {\n      \"serviceName\": \"dove\",\n      \"ipv4\": \"10.6.22.99\",\n      \"port\": 8215\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.150.179\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/deliver\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cbab506b53f336fc\",\n    \"id\": \"b6fdd2a6b74e32e5\",\n    \"name\": \"process.dove\",\n    \"timestamp\": 1543549826231537,\n    \"duration\": 4351650,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.150.179\",\n      \"port\": 8197\n    },\n    \"tags\": {\n      \"lc\": \"EventProcessor\",\n      \"pusher.eventType\": \"INSTALLED_APP_LIFECYCLE_EVENT\",\n      \"pusher.timeInKafka\": \"9\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b6fdd2a6b74e32e5\",\n    \"id\": \"7c4eb808223a2034\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /deliver\",\n    \"timestamp\": 1543549826231581,\n    \"duration\": 4351562,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.150.179\",\n      \"port\": 8197\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549830583000,\n        \"value\": \"Body Part Received\"\n      },\n      {\n        \"timestamp\": 1543549830583000,\n        \"value\": \"Headers Received\"\n      },\n      {\n        \"timestamp\": 1543549830583000,\n        \"value\": \"Status Received\"\n      }\n    ],\n    \"tags\": {\n      \"http.url\": \"http://dove.st.internal/deliver\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"receive iot-events360\",\n    \"timestamp\": 1543549826231758,\n    \"localEndpoint\": {\n      \"serviceName\": \"pusher\",\n      \"ipv4\": \"10.6.150.179\",\n      \"port\": 8197\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bouncer\"\n    },\n    \"tags\": {\n      \"kafka.partition\": \"276\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"cbab506b53f336fc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"send iot-events360\",\n    \"timestamp\": 1543549826223236,\n    \"localEndpoint\": {\n      \"serviceName\": \"bouncer\",\n      \"ipv4\": \"10.6.90.57\",\n      \"port\": 8194\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"pusher\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0109a05a5e8e03bb\",\n    \"id\": \"d02d5bcc785e0a45\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549826215726,\n    \"duration\": 638,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.78\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0109a05a5e8e03bb\",\n    \"id\": \"495e026b47b6dbd7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549826214573,\n    \"duration\": 1090,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.78\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0109a05a5e8e03bb\",\n    \"id\": \"c8288ef68dc9108f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549826213496,\n    \"duration\": 1032,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.78\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0109a05a5e8e03bb\",\n    \"id\": \"9f23934dc53cd056\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549826212424,\n    \"duration\": 1022,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.78\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"0109a05a5e8e03bb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549826212026,\n    \"duration\": 4684,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.78\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 31120\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"64c75434bbe02f59\",\n    \"id\": \"0109a05a5e8e03bb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549826159579,\n    \"duration\": 63285,\n    \"localEndpoint\": {\n      \"serviceName\": \"bouncer\",\n      \"ipv4\": \"10.6.90.57\",\n      \"port\": 8194\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"64c75434bbe02f59\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549826159422,\n    \"duration\": 64464,\n    \"localEndpoint\": {\n      \"serviceName\": \"bouncer\",\n      \"ipv4\": \"10.6.90.57\",\n      \"port\": 8194\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.path\": \"events\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"64c75434bbe02f59\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549826154966,\n    \"duration\": 69611,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/events\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"407de1ef9ab01704\",\n    \"id\": \"98ffd568af9b79a0\",\n    \"name\": \"delete installedapps/_installedappid_/subscriptions\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.6.25.44\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"91235c0453588201\",\n    \"name\": \"post installedapps/_installedappid_/configs/_installconfigid_/authorize\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a19654a3118fb191\",\n    \"id\": \"b2766e10cd03d005\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825865830,\n    \"duration\": 1189,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.18.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.128.112\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.128.112\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d8317797fae1d23a\",\n    \"id\": \"a19654a3118fb191\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549825865024,\n    \"duration\": 2337,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.18.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 9904\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d8317797fae1d23a\",\n    \"id\": \"a19654a3118fb191\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825812314,\n    \"duration\": 61279,\n    \"localEndpoint\": {\n      \"serviceName\": \"paperboy\",\n      \"ipv4\": \"10.6.95.177\",\n      \"port\": 8195\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"98ffd568af9b79a0\",\n    \"id\": \"d8317797fae1d23a\",\n    \"kind\": \"SERVER\",\n    \"name\": \"delete /installedapps/:installedappid/subscriptions/\",\n    \"timestamp\": 1543549825812013,\n    \"duration\": 75749,\n    \"localEndpoint\": {\n      \"serviceName\": \"paperboy\",\n      \"ipv4\": \"10.6.95.177\",\n      \"port\": 8195\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.method\": \"DELETE\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/subscriptions\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"98ffd568af9b79a0\",\n    \"id\": \"d8317797fae1d23a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"delete\",\n    \"timestamp\": 1543549825774000,\n    \"duration\": 115521,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.6.25.44\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/subscriptions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"98ffd568af9b79a0\",\n    \"id\": \"87c35c338e0cbc1b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825773992,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.6.25.44\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549836181133,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/subscriptions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"407de1ef9ab01704\",\n    \"id\": \"98ffd568af9b79a0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"delete installedapps/_installedappid_/subscriptions\",\n    \"timestamp\": 1543549825766014,\n    \"duration\": 123648,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.6.25.44\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/subscriptions\",\n      \"oauth.additionalInfo.principal\": \"installedapp:86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX:b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[r:devices:4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX, r:locations:*, r:devices:f9b535f8-8005-495d-a988-XXXXXXXXXXXX, r:devices:cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX, r:devices:c101b176-4159-42c5-a189-XXXXXXXXXXXX, r:devices:4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX, r:devices:f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX, x:devices:36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX, r:devices:8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX, r:devices:642ef8b5-db73-4544-ad10-XXXXXXXXXXXX, r:devices:66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX, x:devices:*, x:devices:cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX, l:devices, r:devices:21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX, x:devices:8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX, x:devices:66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX, r:devices:22bf04c8-86e4-4596-8499-XXXXXXXXXXXX, x:devices:21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX, r:devices:36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX, x:devices:4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX, r:devices:76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX, r:devices:a2073f14-66b6-45a4-8020-XXXXXXXXXXXX, r:devices:73a92ff4-b847-4575-88d6-XXXXXXXXXXXX, x:devices:f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX, x:devices:68414701-516f-49cf-831e-XXXXXXXXXXXX, x:devices:4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX, r:devices:70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX, x:devices:f9b535f8-8005-495d-a988-XXXXXXXXXXXX, x:devices:9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX, r:devices:4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX, r:devices:4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX, x:devices:c101b176-4159-42c5-a189-XXXXXXXXXXXX, r:devices:*, r:devices:9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX, r:devices:68414701-516f-49cf-831e-XXXXXXXXXXXX]\",\n      \"oauth.clientId\": \"2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"407de1ef9ab01704\",\n    \"id\": \"98ffd568af9b79a0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"delete\",\n    \"timestamp\": 1543549825741529,\n    \"duration\": 148692,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.20.164\",\n      \"port\": 8860\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/subscriptions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"407de1ef9ab01704\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825737908,\n    \"duration\": 415853,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.20.164\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"407de1ef9ab01704\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825736148,\n    \"duration\": 418697,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"00698440b42876a2\",\n    \"id\": \"4ca852bcce68cc53\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825729100,\n    \"duration\": 1422,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"00698440b42876a2\",\n    \"id\": \"c16cd5b2551c1e08\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825723525,\n    \"duration\": 1136,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"00698440b42876a2\",\n    \"id\": \"c121e1a75541b590\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825717461,\n    \"duration\": 1426,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"00698440b42876a2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/configs/{installconfigurationid}\",\n    \"timestamp\": 1543549825716018,\n    \"duration\": 17348,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/526cdf9e-2fd5-48e6-9af7-fc8c2c4b02b5\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"getInstallConfigurationDetail\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"00698440b42876a2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825714356,\n    \"duration\": 19591,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/526cdf9e-2fd5-48e6-9af7-fc8c2c4b02b5\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"098a0deb6dae6822\",\n    \"id\": \"a92172216f5b748d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825710440,\n    \"duration\": 986,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"098a0deb6dae6822\",\n    \"id\": \"87a5d5f8902a705a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825706262,\n    \"duration\": 963,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT COUNT(*)\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"098a0deb6dae6822\",\n    \"id\": \"8cec6291f92636a4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825701476,\n    \"duration\": 1087,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"098a0deb6dae6822\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549825700009,\n    \"duration\": 13261,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"listInstallConfigurations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"098a0deb6dae6822\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825698399,\n    \"duration\": 15840,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"71687cb74971c332\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get circuitbreakers/_entitytype_/_entityid_\",\n    \"timestamp\": 1543549825697031,\n    \"duration\": 1501,\n    \"localEndpoint\": {\n      \"serviceName\": \"alice\",\n      \"ipv4\": \"10.6.31.193\",\n      \"port\": 8231\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"error\": \"404\",\n      \"http.path\": \"circuitbreakers/installedapp/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"http.status_code\": \"404\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"71687cb74971c332\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825693593,\n    \"duration\": 3858,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"error\": \"404\",\n      \"http.method\": \"GET\",\n      \"http.path\": \"/circuitbreakers/installedapp/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"http.status_code\": \"404\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"534e658e15e89d59\",\n    \"id\": \"66cdc6cc7f219082\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825689323,\n    \"duration\": 722,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"534e658e15e89d59\",\n    \"id\": \"d5e1915e5e659f71\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825686314,\n    \"duration\": 409,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"534e658e15e89d59\",\n    \"id\": \"3cecc0360e46fabe\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825683786,\n    \"duration\": 595,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"534e658e15e89d59\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/authorized\",\n    \"timestamp\": 1543549825682008,\n    \"duration\": 9657,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/authorized\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findAuthorizedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce7bd8ffe2892e48\",\n    \"id\": \"534e658e15e89d59\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825680677,\n    \"duration\": 11415,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/authorized\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a0dc23f22c4fee3f\",\n    \"id\": \"ce7bd8ffe2892e48\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549825680004,\n    \"duration\": 544778,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a0dc23f22c4fee3f\",\n    \"id\": \"ce7bd8ffe2892e48\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825674259,\n    \"duration\": 557307,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a0dc23f22c4fee3f\",\n    \"id\": \"cdcc8c3c82642de2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825674255,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549827710299,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"a0dc23f22c4fee3f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825673008,\n    \"duration\": 558631,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"a0dc23f22c4fee3f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549825648375,\n    \"duration\": 583853,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"7e83291848ef55f7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /admin/cache/evict\",\n    \"timestamp\": 1543549825639008,\n    \"duration\": 244,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.90.175\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/admin/cache/evict\",\n      \"jaxrs.resource.class\": \"CacheAdminResource\",\n      \"jaxrs.resource.method\": \"evictCache\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"7e83291848ef55f7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825575069,\n    \"duration\": 63777,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/admin/cache/evict\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"fa418b823dee9c6d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825566393,\n    \"duration\": 1186,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        UPDATE installed_app\\n           SET install_status = ?,\\n               last_updated = ?\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"6c8e28846de88e68\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825564473,\n    \"duration\": 1039,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        UPDATE install_configuration\\n           SET status = ?,\\n               last_updated = ?\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"02b0e95bf10c4911\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825562051,\n    \"duration\": 1359,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        UPDATE install_configuration\\n           SET status = \\\"REVOKED\\\",\\n               last_updated = ?\\n         WHERE installed_app_id = ?\\n           AND status = \\\"AUTHORIZED\\\"\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"c27d3e8966462bf9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825557369,\n    \"duration\": 939,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"dfe19e0bd035fec6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825553585,\n    \"duration\": 482,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"80b8a000cf09dd08\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549825550403,\n    \"duration\": 760,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e6d7bd1e0571d4cf\",\n    \"id\": \"675869991498f9e6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825543128,\n    \"duration\": 707,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.41\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.11.3\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.11.3\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e6d7bd1e0571d4cf\",\n    \"id\": \"dc4b00b0e0bac426\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549825542436,\n    \"duration\": 649,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.41\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e6d7bd1e0571d4cf\",\n    \"id\": \"338406982d0f1d8b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549825541153,\n    \"duration\": 1247,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.41\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.133.141\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.133.141\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e6d7bd1e0571d4cf\",\n    \"id\": \"d45b2a4119d44de3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549825540326,\n    \"duration\": 796,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.41\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"e6d7bd1e0571d4cf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549825540024,\n    \"duration\": 4078,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.41\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 53584\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97a81550f3c633d\",\n    \"id\": \"e6d7bd1e0571d4cf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825530081,\n    \"duration\": 19592,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"91235c0453588201\",\n    \"id\": \"a97a81550f3c633d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /installedapps/{installedappid}/configs/{installconfigurationid}/authorize\",\n    \"timestamp\": 1543549825530006,\n    \"duration\": 109105,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/authorize\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"authorizeInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"91235c0453588201\",\n    \"id\": \"a97a81550f3c633d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825524342,\n    \"duration\": 121584,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/authorize\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"91235c0453588201\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post installedapps/_installedappid_/configs/_installconfigid_/authorize\",\n    \"timestamp\": 1543549825523021,\n    \"duration\": 123029,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/authorize\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"91235c0453588201\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /installedapps/_uuid_/configs/_uuid_/authorize\",\n    \"timestamp\": 1543549825486459,\n    \"duration\": 161129,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/authorize\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"e3897e8561dacc10\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-store-refresh\",\n    \"timestamp\": 1543549825483138,\n    \"duration\": 2157,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.14.17\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tINSERT INTO auth.oauth_refresh_token (oauth_refresh_token, refresh_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.14.17\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"4de79b9e9c4bfe23\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-store\",\n    \"timestamp\": 1543549825475311,\n    \"duration\": 7744,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tBEGIN BATCH\\n\\t\\t\\tINSERT INTO auth.oauth_access_token (oauth_token, access_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_refresh (refresh_token, access_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_authentication (authentication_id, access_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_client_with_token (client_id, client_mod, user_id, oauth_token, access_token, authentication, last_modified) VALUES (?, ?, ?, ?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_user (user_id_hash, access_token, authentication, device_name) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_user_and_client (user_id_hash, client_id, access_token, authentication, device_name) VALUES (?, ?, ?, ?, ?);\\n\\t\\tAPPLY BATCH;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"bbc7f120bfe2299b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825473920,\n    \"duration\": 1176,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.143.190\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.143.190\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"d52eb687cb1875e6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825473206,\n    \"duration\": 665,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"6886f6a15857ecc9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825470010,\n    \"duration\": 3137,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.128.112\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.128.112\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"18fe82087dce6503\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-authentication_id\",\n    \"timestamp\": 1543549825469547,\n    \"duration\": 447,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.72.174\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token_by_authentication WHERE authentication_id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.72.174\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"164b99781540173f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"bound-statement\",\n    \"timestamp\": 1543549825392503,\n    \"duration\": 76958,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.254\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"QUORUM\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"DELETE FROM auth.oauth_code WHERE code = ?;\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.254\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"9957ba5f81cc5b1c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"authcode-select-by-code\",\n    \"timestamp\": 1543549825390397,\n    \"duration\": 1836,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.oauth_code WHERE code = ?;\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"a6ab3deb83272c10\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825389024,\n    \"duration\": 1245,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.128.112\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.128.112\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"e63c6b977c734502\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825387851,\n    \"duration\": 1048,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"b15f66a75119b6a7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/token\",\n    \"timestamp\": 1543549825386021,\n    \"duration\": 99783,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 37514\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b15f66a75119b6a7\",\n    \"id\": \"d545b2dbb7a08345\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825386722,\n    \"duration\": 1082,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.82.209\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.143.190\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.143.190\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cfcd115a2af18bd9\",\n    \"id\": \"b15f66a75119b6a7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/token\",\n    \"timestamp\": 1543549825370981,\n    \"duration\": 115111,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"cfcd115a2af18bd9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get authorize/finalize\",\n    \"timestamp\": 1543549825275055,\n    \"duration\": 957480,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.app_id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"authorize/finalize\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cf1f076681dce55e\",\n    \"id\": \"5474e16e905ab081\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"batch-statement\",\n    \"timestamp\": 1543549825071286,\n    \"duration\": 1888,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.89.46\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.128.112\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.128.112\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cf1f076681dce55e\",\n    \"id\": \"d4bdd91cd79f5876\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825070211,\n    \"duration\": 881,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.89.46\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ca71c90b5e7bb727\",\n    \"id\": \"7163c2df77468e97\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"authcode-store\",\n    \"timestamp\": 1543549825068268,\n    \"duration\": 77937,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.250\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.128.112\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"QUORUM\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"INSERT INTO auth.oauth_code (code, authentication) VALUES (?, ?) USING TTL ?;\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.128.112\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"cf1f076681dce55e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /approvals/create\",\n    \"timestamp\": 1543549825067023,\n    \"duration\": 6346,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.89.46\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 32990\n    },\n    \"tags\": {\n      \"http.path\": \"/approvals/create\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"ca71c90b5e7bb727\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /authorization/code\",\n    \"timestamp\": 1543549825067025,\n    \"duration\": 79435,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.250\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 47360\n    },\n    \"tags\": {\n      \"http.path\": \"/authorization/code\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"cf1f076681dce55e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825050402,\n    \"duration\": 24530,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/approvals/create\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"ca71c90b5e7bb727\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825050402,\n    \"duration\": 97710,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/authorization/code\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cba1d9801be84df6\",\n    \"id\": \"40e3c763e80cdec6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549825048089,\n    \"duration\": 759,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.30.242\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"cba1d9801be84df6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /clients/_uuid_\",\n    \"timestamp\": 1543549825047026,\n    \"duration\": 2121,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.30.242\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 43912\n    },\n    \"tags\": {\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"eeac0dbfa12ff828\",\n    \"id\": \"c72e2e52c3cdcb92\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549825031007,\n    \"duration\": 117216,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"authorize.client\": \"2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\",\n      \"authorize.installedAppId\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"authorize.principal\": \"installedapp:86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX:b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.method\": \"POST\",\n      \"http.path\": \"oauth/authorize\",\n      \"http.status_code\": \"302\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c72e2e52c3cdcb92\",\n    \"id\": \"cba1d9801be84df6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549825031808,\n    \"duration\": 18440,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"28808df9f93a7c85\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b2f64394c624cb7e\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"dca5497bce1f30e7\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8147b701209895f9\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea7d3293387c206a\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"148ff130b8f9d203\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c29b9bfc45b65f55\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea61284cf8e9ebbf\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0f50451b157df742\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51de0f7b03482c75\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"1690b3bf3f99db6f\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"cb35ad2e7ec47ca5\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"597eefc6290d988e\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d9a3ba4aaf210a15\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"12705d3eb65cbfd1\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"34ed439de5737954\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b544123330759f64\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"50c2f27f7eaaa91f\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"70e1dee0fc49183e\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"3cc19b19171b5e41\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4a32feff666afde4\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4ca2c002df1086bf\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"94249f6b3dc42872\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"87b1faf2e0ad9125\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2248db03e9caa194\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"084558c5cc271f49\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"db48156017a86433\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d94919c95b9a9832\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"21efba9eda0fb785\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8b412a7ddca17648\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"e56ef93ff7cd1752\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"600f1e14447154ac\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"fb6022ce1779a46b\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"de126a6e96932af0\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0674a7dbf4856939\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b29504808db96bb9\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c8b08f924e9c39d2\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"97c1955c975e8d47\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"42b9d34a69adb28f\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"7e89b7ed2f186a6f\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4d732220594f941e\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"32ba9cfc14a34eef\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"571949b3ab39dc3b\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f2f670dce4e9709e\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"eeac0dbfa12ff828\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820324007,\n    \"duration\": 159,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.21.207\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"img/samsung_connect.38d72717.png\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"40db5d57dfa1ca9e\",\n    \"name\": \"get installedapps/_installedappid_/configs/_installconfigid_/permissions\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"5e88461960c898bb\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2647a0f9451a0546\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8f5b0907cf48ed82\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"daea78f67e7c95a0\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51b9adb49adf3a90\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f04d0ead678c33f1\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ff86b855acdf0945\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"bc860c8a1e18532c\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dca5497bce1f30e7\",\n    \"id\": \"b4502ec48c290114\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820114008,\n    \"duration\": 53280,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"148ff130b8f9d203\",\n    \"id\": \"9626ddb2c7e01434\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820114007,\n    \"duration\": 46306,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dca5497bce1f30e7\",\n    \"id\": \"5a9b409744ce0d80\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820113240,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821665159,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0674a7dbf4856939\",\n    \"id\": \"8305f9368b62db8d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820113011,\n    \"duration\": 46839,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dca5497bce1f30e7\",\n    \"id\": \"b4502ec48c290114\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820113246,\n    \"duration\": 54954,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"148ff130b8f9d203\",\n    \"id\": \"9626ddb2c7e01434\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820112401,\n    \"duration\": 48009,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0674a7dbf4856939\",\n    \"id\": \"b5dfcbbc571bb368\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820112094,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549833727591,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"148ff130b8f9d203\",\n    \"id\": \"6e1d447d22055f84\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820112395,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822966896,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0674a7dbf4856939\",\n    \"id\": \"8305f9368b62db8d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820112102,\n    \"duration\": 48518,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0674a7dbf4856939\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820111008,\n    \"duration\": 49702,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"dca5497bce1f30e7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820111009,\n    \"duration\": 57308,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"87b1faf2e0ad9125\",\n    \"id\": \"268ff520a63e6695\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820110022,\n    \"duration\": 57533,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"148ff130b8f9d203\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820110006,\n    \"duration\": 50478,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea7d3293387c206a\",\n    \"id\": \"0ce3504b0f76e9d8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820109009,\n    \"duration\": 45866,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"87b1faf2e0ad9125\",\n    \"id\": \"268ff520a63e6695\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820109357,\n    \"duration\": 59355,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"87b1faf2e0ad9125\",\n    \"id\": \"dff2832be9b005f4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820109351,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824233227,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"084558c5cc271f49\",\n    \"id\": \"ad91e377e8154534\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820108008,\n    \"duration\": 46482,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8b08f924e9c39d2\",\n    \"id\": \"c18fee0c071fc196\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820108008,\n    \"duration\": 44132,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c29b9bfc45b65f55\",\n    \"id\": \"511d9f9e6d026923\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820108011,\n    \"duration\": 44797,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea7d3293387c206a\",\n    \"id\": \"0ce3504b0f76e9d8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820108361,\n    \"duration\": 47722,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea7d3293387c206a\",\n    \"id\": \"61a4159eee2ec4e3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820108355,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821665700,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c29b9bfc45b65f55\",\n    \"id\": \"511d9f9e6d026923\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107120,\n    \"duration\": 46125,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8b08f924e9c39d2\",\n    \"id\": \"9d89ec5b7621af40\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107140,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821905092,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8b08f924e9c39d2\",\n    \"id\": \"c18fee0c071fc196\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107146,\n    \"duration\": 46427,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c29b9bfc45b65f55\",\n    \"id\": \"2f6354d40561751f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107111,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822967796,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"084558c5cc271f49\",\n    \"id\": \"88243f5effe1938e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107142,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821771028,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"084558c5cc271f49\",\n    \"id\": \"ad91e377e8154534\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820107153,\n    \"duration\": 48443,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"084558c5cc271f49\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820106008,\n    \"duration\": 49745,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea7d3293387c206a\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820106008,\n    \"duration\": 50294,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"87b1faf2e0ad9125\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820106006,\n    \"duration\": 62817,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c8b08f924e9c39d2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820106012,\n    \"duration\": 47689,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c29b9bfc45b65f55\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820105011,\n    \"duration\": 48325,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"597eefc6290d988e\",\n    \"id\": \"24bab9dd1cc117e3\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820092008,\n    \"duration\": 46295,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"21efba9eda0fb785\",\n    \"id\": \"bfa936f2225af505\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820092008,\n    \"duration\": 44069,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"597eefc6290d988e\",\n    \"id\": \"7af91bca100ff3ed\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820091093,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822478068,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"597eefc6290d988e\",\n    \"id\": \"24bab9dd1cc117e3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820091102,\n    \"duration\": 48102,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"21efba9eda0fb785\",\n    \"id\": \"18c23a48dfe34d95\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820090708,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821770513,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"21efba9eda0fb785\",\n    \"id\": \"bfa936f2225af505\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820090720,\n    \"duration\": 46092,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"600f1e14447154ac\",\n    \"id\": \"a3cfdd46fd13ba57\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820089007,\n    \"duration\": 43115,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"597eefc6290d988e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820089006,\n    \"duration\": 50282,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"600f1e14447154ac\",\n    \"id\": \"a3cfdd46fd13ba57\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820088179,\n    \"duration\": 47275,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"600f1e14447154ac\",\n    \"id\": \"daf1c64899742211\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820088169,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821799823,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8147b701209895f9\",\n    \"id\": \"fac20283bebe2f59\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820087012,\n    \"duration\": 43648,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4ca2c002df1086bf\",\n    \"id\": \"a6774b05c5047720\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820086006,\n    \"duration\": 48153,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.85.224\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d732220594f941e\",\n    \"id\": \"0bda698f0223b74b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820086007,\n    \"duration\": 48987,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"600f1e14447154ac\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820086008,\n    \"duration\": 49562,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8147b701209895f9\",\n    \"id\": \"39ac0e0ca6656efe\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820085262,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821664782,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8147b701209895f9\",\n    \"id\": \"fac20283bebe2f59\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820085268,\n    \"duration\": 45674,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d732220594f941e\",\n    \"id\": \"0bda698f0223b74b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820084142,\n    \"duration\": 51586,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d732220594f941e\",\n    \"id\": \"bb895c5bf8fc9bb6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820084136,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549823961891,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4ca2c002df1086bf\",\n    \"id\": \"a6774b05c5047720\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820084155,\n    \"duration\": 50529,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4ca2c002df1086bf\",\n    \"id\": \"d397f79f9596ab17\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820084149,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824232728,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e89b7ed2f186a6f\",\n    \"id\": \"417e62260f34297f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820084013,\n    \"duration\": 46475,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"21efba9eda0fb785\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820083014,\n    \"duration\": 53968,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8147b701209895f9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820083009,\n    \"duration\": 48066,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e89b7ed2f186a6f\",\n    \"id\": \"fb47b0d3165dc4b6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820083208,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549823962417,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de126a6e96932af0\",\n    \"id\": \"58b428612fa37bcb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820083010,\n    \"duration\": 44728,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e89b7ed2f186a6f\",\n    \"id\": \"417e62260f34297f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820083219,\n    \"duration\": 48026,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4ca2c002df1086bf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820082006,\n    \"duration\": 52767,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de126a6e96932af0\",\n    \"id\": \"58b428612fa37bcb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820082125,\n    \"duration\": 46502,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4d732220594f941e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820082004,\n    \"duration\": 53855,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de126a6e96932af0\",\n    \"id\": \"7dd6d1f182fdfefb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820082118,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549833726466,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"de126a6e96932af0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820081006,\n    \"duration\": 47722,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"7e89b7ed2f186a6f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820081008,\n    \"duration\": 50344,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"dca5497bce1f30e7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820080041,\n    \"duration\": 90441,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"148ff130b8f9d203\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820080085,\n    \"duration\": 82020,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0674a7dbf4856939\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820079950,\n    \"duration\": 81786,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d9a3ba4aaf210a15\",\n    \"id\": \"28fc2b59c9d8b43c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820079011,\n    \"duration\": 43629,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.85.224\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d9a3ba4aaf210a15\",\n    \"id\": \"28fc2b59c9d8b43c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820078184,\n    \"duration\": 46048,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d9a3ba4aaf210a15\",\n    \"id\": \"2bac70cda43d939d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820078176,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822477808,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d9a3ba4aaf210a15\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820076006,\n    \"duration\": 48296,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1690b3bf3f99db6f\",\n    \"id\": \"e286f5ad5f11c112\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820063011,\n    \"duration\": 49008,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"70e1dee0fc49183e\",\n    \"id\": \"39955cb67848f8d1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820063013,\n    \"duration\": 44347,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1690b3bf3f99db6f\",\n    \"id\": \"2d1786424cdf901d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820062136,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822478420,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1690b3bf3f99db6f\",\n    \"id\": \"e286f5ad5f11c112\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820062145,\n    \"duration\": 51364,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"70e1dee0fc49183e\",\n    \"id\": \"39955cb67848f8d1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820062113,\n    \"duration\": 46423,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"70e1dee0fc49183e\",\n    \"id\": \"fe150a8fce7a02d7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820062104,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824233196,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b29504808db96bb9\",\n    \"id\": \"440b1417ed9ff4e2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549820062011,\n    \"duration\": 42905,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b29504808db96bb9\",\n    \"id\": \"440b1417ed9ff4e2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820061160,\n    \"duration\": 44724,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b29504808db96bb9\",\n    \"id\": \"88d47e3ff7d4a1dd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820061150,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549833726589,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"1690b3bf3f99db6f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820060010,\n    \"duration\": 53593,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"70e1dee0fc49183e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820060008,\n    \"duration\": 48611,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b29504808db96bb9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549820060020,\n    \"duration\": 45969,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"87b1faf2e0ad9125\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820059221,\n    \"duration\": 110952,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"600f1e14447154ac\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820056962,\n    \"duration\": 79890,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"597eefc6290d988e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820056873,\n    \"duration\": 83845,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea7d3293387c206a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820053883,\n    \"duration\": 104096,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d9a3ba4aaf210a15\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820053841,\n    \"duration\": 71487,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c8b08f924e9c39d2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820053571,\n    \"duration\": 102556,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"084558c5cc271f49\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820053746,\n    \"duration\": 104006,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"c29b9bfc45b65f55\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549820053694,\n    \"duration\": 101544,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8147b701209895f9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819997129,\n    \"duration\": 135961,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"70e1dee0fc49183e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819997279,\n    \"duration\": 112628,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"1690b3bf3f99db6f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996896,\n    \"duration\": 117695,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4d732220594f941e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996624,\n    \"duration\": 140814,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"21efba9eda0fb785\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996994,\n    \"duration\": 141876,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4ca2c002df1086bf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996131,\n    \"duration\": 140264,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"7e89b7ed2f186a6f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996373,\n    \"duration\": 136477,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"de126a6e96932af0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996758,\n    \"duration\": 133569,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b29504808db96bb9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819996520,\n    \"duration\": 110965,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"fac290fc9648b73c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /clients/_uuid_\",\n    \"timestamp\": 1543549819993025,\n    \"duration\": 2006,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 62500\n    },\n    \"tags\": {\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fac290fc9648b73c\",\n    \"id\": \"5a83927b8dba1246\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549819993657,\n    \"duration\": 1101,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"fac290fc9648b73c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819975134,\n    \"duration\": 20406,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"acf01eee13dd1fba\",\n    \"name\": \"post installedapps/_installedappid_/configs/_installconfigid_/done\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0f50451b157df742\",\n    \"id\": \"5cafbd954609f6d2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925007,\n    \"duration\": 43526,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"28808df9f93a7c85\",\n    \"id\": \"db0ae68e5d712b7f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925006,\n    \"duration\": 47227,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea61284cf8e9ebbf\",\n    \"id\": \"180b16a70d7dcd46\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925007,\n    \"duration\": 46230,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8b412a7ddca17648\",\n    \"id\": \"3bc469fc4a7c0620\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925010,\n    \"duration\": 43133,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2248db03e9caa194\",\n    \"id\": \"da7a24db66025d67\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925008,\n    \"duration\": 46623,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f5b0907cf48ed82\",\n    \"id\": \"a89a37941d6c9b4a\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819925008,\n    \"duration\": 47269,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51b9adb49adf3a90\",\n    \"id\": \"b4e30d7a4cebe440\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819924007,\n    \"duration\": 46775,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f5b0907cf48ed82\",\n    \"id\": \"afc514bbc0386095\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819924070,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822052558,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0f50451b157df742\",\n    \"id\": \"5cafbd954609f6d2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819924057,\n    \"duration\": 45977,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0f50451b157df742\",\n    \"id\": \"362868d4ef251cf5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819924051,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822966528,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f5b0907cf48ed82\",\n    \"id\": \"a89a37941d6c9b4a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819924077,\n    \"duration\": 49273,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8b412a7ddca17648\",\n    \"id\": \"3bc469fc4a7c0620\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923099,\n    \"duration\": 45812,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2248db03e9caa194\",\n    \"id\": \"5814cb3207981838\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923178,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821770628,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"28808df9f93a7c85\",\n    \"id\": \"3a37e07008a406ee\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923224,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821664254,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8b412a7ddca17648\",\n    \"id\": \"3274f22ce24be97f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923075,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821797789,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea61284cf8e9ebbf\",\n    \"id\": \"180b16a70d7dcd46\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923098,\n    \"duration\": 48399,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2248db03e9caa194\",\n    \"id\": \"da7a24db66025d67\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923187,\n    \"duration\": 48650,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2647a0f9451a0546\",\n    \"id\": \"8850f9683f6bc9d5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819923006,\n    \"duration\": 42990,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ea61284cf8e9ebbf\",\n    \"id\": \"397507321d0f4d7d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923092,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822966330,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8f5b0907cf48ed82\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819923005,\n    \"duration\": 50574,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"28808df9f93a7c85\",\n    \"id\": \"db0ae68e5d712b7f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819923231,\n    \"duration\": 49395,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51b9adb49adf3a90\",\n    \"id\": \"b4e30d7a4cebe440\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819922276,\n    \"duration\": 49518,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0f50451b157df742\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819922006,\n    \"duration\": 48110,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2248db03e9caa194\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819922009,\n    \"duration\": 49949,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51b9adb49adf3a90\",\n    \"id\": \"606141c1e343959f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819922269,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822052474,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2647a0f9451a0546\",\n    \"id\": \"8850f9683f6bc9d5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819922269,\n    \"duration\": 47425,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2647a0f9451a0546\",\n    \"id\": \"715c808418e944e2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819922260,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822051807,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8b412a7ddca17648\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819921006,\n    \"duration\": 47986,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2647a0f9451a0546\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819921006,\n    \"duration\": 48819,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"28808df9f93a7c85\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819921008,\n    \"duration\": 51791,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51b9adb49adf3a90\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819921007,\n    \"duration\": 50884,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea61284cf8e9ebbf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819921007,\n    \"duration\": 50628,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff86b855acdf0945\",\n    \"id\": \"848795496021f724\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819917008,\n    \"duration\": 44200,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"bc860c8a1e18532c\",\n    \"id\": \"7dc70f72152e9604\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819917012,\n    \"duration\": 48913,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"571949b3ab39dc3b\",\n    \"id\": \"18fee93a895641cf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819917012,\n    \"duration\": 46582,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e56ef93ff7cd1752\",\n    \"id\": \"216738fd2ffd115f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819916007,\n    \"duration\": 46068,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.85.224\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"bc860c8a1e18532c\",\n    \"id\": \"671212d139c8eb76\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819916265,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549838895387,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"34ed439de5737954\",\n    \"id\": \"4f01e5837a500e5b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819916029,\n    \"duration\": 46064,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4a32feff666afde4\",\n    \"id\": \"f82dcf8a748f7031\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819916010,\n    \"duration\": 47320,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.85.224\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb35ad2e7ec47ca5\",\n    \"id\": \"a79b53bb1c280328\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819916016,\n    \"duration\": 42686,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3cc19b19171b5e41\",\n    \"id\": \"99fc68dadb14417d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819916008,\n    \"duration\": 47582,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"bc860c8a1e18532c\",\n    \"id\": \"7dc70f72152e9604\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819916277,\n    \"duration\": 54572,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3cc19b19171b5e41\",\n    \"id\": \"314b6eb489d4b7a7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915256,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824232506,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e56ef93ff7cd1752\",\n    \"id\": \"216738fd2ffd115f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915063,\n    \"duration\": 47595,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"571949b3ab39dc3b\",\n    \"id\": \"18fee93a895641cf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915408,\n    \"duration\": 49109,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb35ad2e7ec47ca5\",\n    \"id\": \"a79b53bb1c280328\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915120,\n    \"duration\": 44715,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff86b855acdf0945\",\n    \"id\": \"848795496021f724\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915637,\n    \"duration\": 46272,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"34ed439de5737954\",\n    \"id\": \"0bc4b5c0c961b0bc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915276,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821325530,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e56ef93ff7cd1752\",\n    \"id\": \"d9b6168d27508a71\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915057,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821797913,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff86b855acdf0945\",\n    \"id\": \"6464b85817ca6d3b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915632,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549838894746,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3cc19b19171b5e41\",\n    \"id\": \"99fc68dadb14417d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915266,\n    \"duration\": 49213,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"571949b3ab39dc3b\",\n    \"id\": \"27f5858f3975d8cb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915401,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549823962422,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb35ad2e7ec47ca5\",\n    \"id\": \"66c1b238837c50fb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915111,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822477381,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"34ed439de5737954\",\n    \"id\": \"4f01e5837a500e5b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819915284,\n    \"duration\": 48609,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4a32feff666afde4\",\n    \"id\": \"622ec0b0c0f5c02a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819914242,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824233043,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"bc860c8a1e18532c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819914006,\n    \"duration\": 56924,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4a32feff666afde4\",\n    \"id\": \"f82dcf8a748f7031\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819914249,\n    \"duration\": 49803,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"571949b3ab39dc3b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913017,\n    \"duration\": 51576,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"cb35ad2e7ec47ca5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913013,\n    \"duration\": 46916,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"34ed439de5737954\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913007,\n    \"duration\": 50986,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"3cc19b19171b5e41\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913006,\n    \"duration\": 51547,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"e56ef93ff7cd1752\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913016,\n    \"duration\": 49748,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ff86b855acdf0945\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819913014,\n    \"duration\": 49012,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4a32feff666afde4\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819912006,\n    \"duration\": 52136,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b2f64394c624cb7e\",\n    \"id\": \"db22e424a7ad33e3\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819900009,\n    \"duration\": 44772,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b2f64394c624cb7e\",\n    \"id\": \"18a30c52381ed13f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819896231,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821665507,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2f670dce4e9709e\",\n    \"id\": \"16f0c8c02f003111\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819896010,\n    \"duration\": 46842,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.85.224\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b2f64394c624cb7e\",\n    \"id\": \"db22e424a7ad33e3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819896241,\n    \"duration\": 49602,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b544123330759f64\",\n    \"id\": \"387d943642413f83\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819895008,\n    \"duration\": 44403,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2f670dce4e9709e\",\n    \"id\": \"16f0c8c02f003111\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819894435,\n    \"duration\": 48400,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2f670dce4e9709e\",\n    \"id\": \"e499320c8986a360\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819894428,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549823961978,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51de0f7b03482c75\",\n    \"id\": \"97d76de1849ca7f8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819894009,\n    \"duration\": 48181,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b2f64394c624cb7e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819894014,\n    \"duration\": 51947,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.94.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"daea78f67e7c95a0\",\n    \"id\": \"bd5aa79d2ebc2aff\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819893007,\n    \"duration\": 52148,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94249f6b3dc42872\",\n    \"id\": \"942b7b3fede525df\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819893017,\n    \"duration\": 47186,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"97c1955c975e8d47\",\n    \"id\": \"678b61aeac6894a9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819893017,\n    \"duration\": 51168,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51de0f7b03482c75\",\n    \"id\": \"fe264ebc5e3d8df4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819893085,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822967844,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b544123330759f64\",\n    \"id\": \"387d943642413f83\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819893358,\n    \"duration\": 46065,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b544123330759f64\",\n    \"id\": \"1244591e377f5137\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819893351,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821324548,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51de0f7b03482c75\",\n    \"id\": \"97d76de1849ca7f8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819893092,\n    \"duration\": 49952,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"db48156017a86433\",\n    \"id\": \"d718563b5c8e7796\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819893010,\n    \"duration\": 46995,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f2f670dce4e9709e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819892006,\n    \"duration\": 50915,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"daea78f67e7c95a0\",\n    \"id\": \"bd5aa79d2ebc2aff\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892288,\n    \"duration\": 54070,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"97c1955c975e8d47\",\n    \"id\": \"22d80f9ccdb6d4b2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892349,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821904407,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94249f6b3dc42872\",\n    \"id\": \"e580a0a177f0777c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892141,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824232821,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"daea78f67e7c95a0\",\n    \"id\": \"da323d8de2d15055\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892281,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822052208,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94249f6b3dc42872\",\n    \"id\": \"942b7b3fede525df\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892147,\n    \"duration\": 50726,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"97c1955c975e8d47\",\n    \"id\": \"678b61aeac6894a9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819892359,\n    \"duration\": 52474,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"97c1955c975e8d47\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819891010,\n    \"duration\": 53916,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b544123330759f64\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819891012,\n    \"duration\": 48548,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"db48156017a86433\",\n    \"id\": \"04e02d33d6e509ad\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819891981,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821771203,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51de0f7b03482c75\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819891012,\n    \"duration\": 52140,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"db48156017a86433\",\n    \"id\": \"d718563b5c8e7796\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819891992,\n    \"duration\": 49390,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"daea78f67e7c95a0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819891010,\n    \"duration\": 55460,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"db48156017a86433\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819891009,\n    \"duration\": 50490,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"94249f6b3dc42872\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819890006,\n    \"duration\": 52953,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d94919c95b9a9832\",\n    \"id\": \"f47b2332c1a80df3\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819836013,\n    \"duration\": 46040,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.18\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d94919c95b9a9832\",\n    \"id\": \"7712a05de0e49d1e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819835220,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821770915,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d94919c95b9a9832\",\n    \"id\": \"f47b2332c1a80df3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819835231,\n    \"duration\": 49593,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"12705d3eb65cbfd1\",\n    \"id\": \"f75ae3f8fb93358f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819835007,\n    \"duration\": 45175,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d94919c95b9a9832\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819834014,\n    \"duration\": 50917,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f04d0ead678c33f1\",\n    \"id\": \"6532c2be19b7236d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819834012,\n    \"duration\": 54048,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.86.146\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"32ba9cfc14a34eef\",\n    \"id\": \"bdd473dc795f50c2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819833011,\n    \"duration\": 60729,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f04d0ead678c33f1\",\n    \"id\": \"506214d8f70f9850\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819833083,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549838895102,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f04d0ead678c33f1\",\n    \"id\": \"6532c2be19b7236d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819833093,\n    \"duration\": 55773,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"50c2f27f7eaaa91f\",\n    \"id\": \"ca2fd161cea99373\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819833010,\n    \"duration\": 46196,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.158.105\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"42b9d34a69adb28f\",\n    \"id\": \"1970a7268baadb22\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819833006,\n    \"duration\": 49768,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.31.138\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"12705d3eb65cbfd1\",\n    \"id\": \"f75ae3f8fb93358f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819833349,\n    \"duration\": 46788,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"12705d3eb65cbfd1\",\n    \"id\": \"7f2ccecf485508df\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819833338,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821324294,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fb6022ce1779a46b\",\n    \"id\": \"a66f8b40db92c451\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819832015,\n    \"duration\": 46615,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.149.36\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"42b9d34a69adb28f\",\n    \"id\": \"3b24385497b4d8fe\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819832145,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549821905451,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"50c2f27f7eaaa91f\",\n    \"id\": \"ca2fd161cea99373\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819832273,\n    \"duration\": 48298,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"50c2f27f7eaaa91f\",\n    \"id\": \"e5b92ad3fc8ed680\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819832262,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549824232525,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"42b9d34a69adb28f\",\n    \"id\": \"1970a7268baadb22\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819832155,\n    \"duration\": 51927,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5e88461960c898bb\",\n    \"id\": \"626302c827a39ad8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /devices/:deviceid/\",\n    \"timestamp\": 1543549819831011,\n    \"duration\": 45434,\n    \"localEndpoint\": {\n      \"serviceName\": \"gizmo\",\n      \"ipv4\": \"10.104.17.100\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"32ba9cfc14a34eef\",\n    \"id\": \"bdd473dc795f50c2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819831526,\n    \"duration\": 62513,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"32ba9cfc14a34eef\",\n    \"id\": \"75b299668cd62113\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819831517,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549823961565,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fb6022ce1779a46b\",\n    \"id\": \"a66f8b40db92c451\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819831117,\n    \"duration\": 49108,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f04d0ead678c33f1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819831008,\n    \"duration\": 57988,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"42b9d34a69adb28f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819831010,\n    \"duration\": 53282,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fb6022ce1779a46b\",\n    \"id\": \"2c9969f5254efb18\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819831106,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549833727321,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5e88461960c898bb\",\n    \"id\": \"626302c827a39ad8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819830177,\n    \"duration\": 47542,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"50c2f27f7eaaa91f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819830012,\n    \"duration\": 50651,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5e88461960c898bb\",\n    \"id\": \"35e462fb91b89b03\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819830170,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549822051466,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"fb6022ce1779a46b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819830012,\n    \"duration\": 50335,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.150.64\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"32ba9cfc14a34eef\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819829011,\n    \"duration\": 65131,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"12705d3eb65cbfd1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819829017,\n    \"duration\": 51254,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.19.115\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"5e88461960c898bb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get devices/_deviceid_/\",\n    \"timestamp\": 1543549819829012,\n    \"duration\": 48858,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"SEMANTIC_VERSION: 1\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f2f670dce4e9709e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741816,\n    \"duration\": 203031,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ea61284cf8e9ebbf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741595,\n    \"duration\": 232000,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/73a92ff4-b847-4575-88d6-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"cb35ad2e7ec47ca5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741343,\n    \"duration\": 221058,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"0f50451b157df742\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741835,\n    \"duration\": 230096,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/76760a86-4fd0-4fc5-a8c0-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"5e88461960c898bb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741343,\n    \"duration\": 138258,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/8da0d44a-fc10-4716-abc4-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"fb6022ce1779a46b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741493,\n    \"duration\": 140319,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/a2073f14-66b6-45a4-8020-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2647a0f9451a0546\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741552,\n    \"duration\": 230125,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"daea78f67e7c95a0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741868,\n    \"duration\": 205835,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"28808df9f93a7c85\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741634,\n    \"duration\": 233257,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b544123330759f64\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741766,\n    \"duration\": 200021,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"b2f64394c624cb7e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741720,\n    \"duration\": 208029,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"ff86b855acdf0945\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741262,\n    \"duration\": 222717,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8f5b0907cf48ed82\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741709,\n    \"duration\": 233220,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/c101b176-4159-42c5-a189-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"32ba9cfc14a34eef\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741262,\n    \"duration\": 154519,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/9ffbc1b2-a5bc-4cba-980f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"42b9d34a69adb28f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741426,\n    \"duration\": 144317,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"97c1955c975e8d47\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741675,\n    \"duration\": 207472,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/36d1c26b-97cb-4b0c-8f0f-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"571949b3ab39dc3b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741220,\n    \"duration\": 225024,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/642ef8b5-db73-4544-ad10-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"db48156017a86433\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741919,\n    \"duration\": 201100,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/22bf04c8-86e4-4596-8499-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51de0f7b03482c75\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741624,\n    \"duration\": 203469,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4b1c349f-f7a6-497c-892b-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"2248db03e9caa194\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741751,\n    \"duration\": 232578,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4f70a90f-98ac-4be0-9bee-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"50c2f27f7eaaa91f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741220,\n    \"duration\": 141163,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a4c1058-c0d3-4538-b534-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"4a32feff666afde4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741493,\n    \"duration\": 223928,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/70f4a18d-d989-4764-8cfa-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"3cc19b19171b5e41\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741303,\n    \"duration\": 225754,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"e56ef93ff7cd1752\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741385,\n    \"duration\": 222834,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"51b9adb49adf3a90\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741794,\n    \"duration\": 231463,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/68414701-516f-49cf-831e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"8b412a7ddca17648\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741668,\n    \"duration\": 229230,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f2d5739d-6a99-42ff-8d53-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"34ed439de5737954\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741128,\n    \"duration\": 224075,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"d94919c95b9a9832\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741303,\n    \"duration\": 145307,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/4a244ebe-9067-42c0-8eab-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"bc860c8a1e18532c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741426,\n    \"duration\": 231056,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/66db21ee-2f03-4f77-9eac-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"94249f6b3dc42872\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741552,\n    \"duration\": 203266,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/cdafe620-d218-46fe-a9d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"12705d3eb65cbfd1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741128,\n    \"duration\": 141823,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/21283159-db7e-4b0a-a5d5-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d932067d92c1d3f\",\n    \"id\": \"f04d0ead678c33f1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819741385,\n    \"duration\": 149226,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/devices/f9b535f8-8005-495d-a988-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6c75f18527e952b7\",\n    \"id\": \"9d932067d92c1d3f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819740008,\n    \"duration\": 430768,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"authorize.client\": \"2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\",\n      \"authorize.installedAppId\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"authorize.principal\": \"installedapp:86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX:b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.method\": \"GET\",\n      \"http.path\": \"api/oauth/authorize\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ede8aface0fb083b\",\n    \"id\": \"6c75f18527e952b7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819720007,\n    \"duration\": 248,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.149.226\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"css/authorize-AuthorizeAppV1.29509c1a.css\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ede8aface0fb083b\",\n    \"id\": \"0335dbe1c2e7b7f1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819718004,\n    \"duration\": 111,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"js/authorize-AuthorizeAppV1.bab28062.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"ede8aface0fb083b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819583007,\n    \"duration\": 172,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.21.207\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"js/chunk-vendors.c863181d.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"e1523fc62ee229f7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819581005,\n    \"duration\": 151,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"js/chunk-common.455c9996.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"65c2da782d9fa06e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819564040,\n    \"duration\": 751,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.149.226\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"css/authorize-strongman.7afe8aa2.css\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"b6e4eb853f882156\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819537006,\n    \"duration\": 817,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.90.74\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"js/authorize-strongman.b2444fa1.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"1a719ac9995e7551\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819517007,\n    \"duration\": 743,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.21.207\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"css/chunk-common.2ff68acc.css\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"596db3250b60a956\",\n    \"id\": \"5ec91932479475b1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549819465838,\n    \"duration\": 513,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.94\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"596db3250b60a956\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /clients/_uuid_\",\n    \"timestamp\": 1543549819465032,\n    \"duration\": 1649,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.94\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 2710\n    },\n    \"tags\": {\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"596db3250b60a956\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819438643,\n    \"duration\": 29473,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a60c0e23bf726a94\",\n    \"id\": \"b82887c1e59f4531\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549819424896,\n    \"duration\": 1890,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a60c0e23bf726a94\",\n    \"id\": \"bc4529de34f6a6cf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549819419761,\n    \"duration\": 1110,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a60c0e23bf726a94\",\n    \"id\": \"738537f933b34711\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549819414367,\n    \"duration\": 1364,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"03c8c73330ee27a4\",\n    \"id\": \"d0d2818d556fa90d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549819404101,\n    \"duration\": 569,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.156.70\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.141.175\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.141.175\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"03c8c73330ee27a4\",\n    \"id\": \"a712b31279bb3228\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549819403238,\n    \"duration\": 786,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.156.70\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.69\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.69\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"03c8c73330ee27a4\",\n    \"id\": \"70ae97b5675e99aa\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549819402577,\n    \"duration\": 608,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.156.70\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.69\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.69\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"02c9ccc069857837\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a60c0e23bf726a94\",\n    \"id\": \"03c8c73330ee27a4\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549819401024,\n    \"duration\": 4011,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.156.70\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 63680\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"03c8c73330ee27a4\",\n    \"id\": \"2b9dda89282aa698\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549819401469,\n    \"duration\": 1057,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.156.70\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.75.87\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.75.87\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a60c0e23bf726a94\",\n    \"id\": \"03c8c73330ee27a4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549819391127,\n    \"duration\": 21060,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"40db5d57dfa1ca9e\",\n    \"id\": \"a60c0e23bf726a94\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/configs/{installconfigurationid}/permissions\",\n    \"timestamp\": 1543549819391010,\n    \"duration\": 39258,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/permissions\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"getInstallConfigurationPermissions\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"40db5d57dfa1ca9e\",\n    \"id\": \"dff48013403539a1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819384103,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549819756465,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/permissions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"40db5d57dfa1ca9e\",\n    \"id\": \"a60c0e23bf726a94\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819384111,\n    \"duration\": 52456,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/permissions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"40db5d57dfa1ca9e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get installedapps/_installedappid_/configs/_installconfigid_/permissions\",\n    \"timestamp\": 1543549819374013,\n    \"duration\": 62658,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/permissions\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"224f6a67-df5a-44fa-b77c-3010a43b69dc\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4c39baf930673445\",\n    \"id\": \"40db5d57dfa1ca9e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819341021,\n    \"duration\": 97509,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/permissions\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1521ea88fbf921bd\",\n    \"id\": \"4c39baf930673445\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549819340008,\n    \"duration\": 128393,\n    \"localEndpoint\": {\n      \"serviceName\": \"stLogin\",\n      \"ipv4\": \"10.104.31.11\",\n      \"port\": 8141\n    },\n    \"tags\": {\n      \"authorize.client\": \"2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\",\n      \"authorize.installedAppId\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"authorize.principal\": \"installedapp:86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX:b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.method\": \"GET\",\n      \"http.path\": \"oauth/authorize\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"1521ea88fbf921bd\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get authorize/confirm\",\n    \"timestamp\": 1543549819082026,\n    \"duration\": 108090,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.app_id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"authorize/confirm\",\n      \"http.status_code\": \"302\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"35f818d4a4c2c1ff\",\n    \"id\": \"aecb9742374c5cbf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549818866146,\n    \"duration\": 608,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.215\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"35f818d4a4c2c1ff\",\n    \"id\": \"9ae6b7444f5e4d30\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549818865382,\n    \"duration\": 713,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.215\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.7.217\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.7.217\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"35f818d4a4c2c1ff\",\n    \"id\": \"4dbb7905664542cc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549818864723,\n    \"duration\": 616,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.215\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.7.217\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.7.217\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"38e5660b03de2fac\",\n    \"id\": \"35f818d4a4c2c1ff\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818863024,\n    \"duration\": 4022,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.215\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 61776\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"35f818d4a4c2c1ff\",\n    \"id\": \"f3f74095ecdc26de\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549818863373,\n    \"duration\": 1312,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.215\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"54.172.228.252\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"54.172.228.252\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"38e5660b03de2fac\",\n    \"id\": \"35f818d4a4c2c1ff\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818852127,\n    \"duration\": 21144,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"//oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"38e5660b03de2fac\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /admin/cache/evict\",\n    \"timestamp\": 1543549818852008,\n    \"duration\": 21552,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.25.92\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/admin/cache/evict\",\n      \"jaxrs.resource.class\": \"CacheAdminResource\",\n      \"jaxrs.resource.method\": \"evictCache\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"38e5660b03de2fac\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818835502,\n    \"duration\": 39156,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/admin/cache/evict\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"62118b02890ba7de\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818829957,\n    \"duration\": 553,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        UPDATE install_configuration\\n           SET status = ?,\\n               last_updated = ?\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"c18374c4c8c227aa\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818822114,\n    \"duration\": 3972,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"02135db49716c2d2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818816836,\n    \"duration\": 1171,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9edef3c70a658f9b\",\n    \"id\": \"c06b33257d3ce302\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549818804593,\n    \"duration\": 820,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.85.61\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9edef3c70a658f9b\",\n    \"id\": \"b263aaaf01237135\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549818803824,\n    \"duration\": 749,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.85.61\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fdae6317826183f5\",\n    \"id\": \"9edef3c70a658f9b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818803024,\n    \"duration\": 2917,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.85.61\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 6818\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fdae6317826183f5\",\n    \"id\": \"9edef3c70a658f9b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818790045,\n    \"duration\": 16539,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.93.136\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"fdae6317826183f5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549818790008,\n    \"duration\": 19011,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.93.136\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"fdae6317826183f5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549818783921,\n    \"duration\": 31576,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c2eac7c436329405\",\n    \"id\": \"2df9ff93bce82b34\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818780111,\n    \"duration\": 1359,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"acf01eee13dd1fba\",\n    \"id\": \"c2eac7c436329405\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /installedapps/{installedappid}/configs/{installconfigurationid}/done\",\n    \"timestamp\": 1543549818778009,\n    \"duration\": 97103,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/done\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"doneInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"acf01eee13dd1fba\",\n    \"id\": \"c2eac7c436329405\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818771290,\n    \"duration\": 110145,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/done\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"acf01eee13dd1fba\",\n    \"id\": \"732d1c7c4c1b83f8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549818771277,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549819912619,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/done\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"acf01eee13dd1fba\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post installedapps/_installedappid_/configs/_installconfigid_/done\",\n    \"timestamp\": 1543549818770013,\n    \"duration\": 111540,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/done\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"acf01eee13dd1fba\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /installedapps/_uuid_/configs/_uuid_/done\",\n    \"timestamp\": 1543549818758268,\n    \"duration\": 125270,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs/52327493-a185-49d4-972c-XXXXXXXXXXXX/done\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"9ffa863dd5c3a1f0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818693799,\n    \"duration\": 45907,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"6852e374d70b12a6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818686970,\n    \"duration\": 4290,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"e32706ca6fc32886\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818681979,\n    \"duration\": 786,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"725d9e9b46204578\",\n    \"id\": \"297e4838ced4beee\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549818655580,\n    \"duration\": 1074,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.68\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"725d9e9b46204578\",\n    \"id\": \"5fcf310ccbb26c67\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549818655082,\n    \"duration\": 480,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.68\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"614993abe1910174\",\n    \"id\": \"725d9e9b46204578\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818654028,\n    \"duration\": 3062,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.68\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 44526\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"614993abe1910174\",\n    \"id\": \"725d9e9b46204578\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818635122,\n    \"duration\": 27764,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.149.58\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"614993abe1910174\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549818634892,\n    \"duration\": 42556,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.149.58\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 46518\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"614993abe1910174\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818615318,\n    \"duration\": 62288,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4f220952c7f13e26\",\n    \"id\": \"20430b7f8584439b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549818602179,\n    \"duration\": 507,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.17.109\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4f220952c7f13e26\",\n    \"id\": \"c795136fa08841ef\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549818602694,\n    \"duration\": 778,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.17.109\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0a2a0ec8ed00d383\",\n    \"id\": \"4f220952c7f13e26\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818601036,\n    \"duration\": 2744,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.17.109\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\",\n      \"port\": 52724\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0a2a0ec8ed00d383\",\n    \"id\": \"4f220952c7f13e26\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818586061,\n    \"duration\": 17866,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.158.172\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"0a2a0ec8ed00d383\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549818586012,\n    \"duration\": 19863,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.158.172\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"0a2a0ec8ed00d383\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549818580287,\n    \"duration\": 32288,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"0a01a35808077208\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549818576655,\n    \"duration\": 966,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f844c177b3f7d01b\",\n    \"id\": \"50cc75bbdee04a50\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549818567268,\n    \"duration\": 800,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.65\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f844c177b3f7d01b\",\n    \"id\": \"6d64878131985c01\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549818566163,\n    \"duration\": 1091,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.65\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"f844c177b3f7d01b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549818565031,\n    \"duration\": 3466,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.22.65\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 56304\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"49842b4e2cd5f7b2\",\n    \"id\": \"f844c177b3f7d01b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549818554103,\n    \"duration\": 20333,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"02c9ccc069857837\",\n    \"id\": \"49842b4e2cd5f7b2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549818554005,\n    \"duration\": 194977,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"02c9ccc069857837\",\n    \"id\": \"49842b4e2cd5f7b2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549818546691,\n    \"duration\": 208725,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"02c9ccc069857837\",\n    \"id\": \"c0622540a70157da\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549818546681,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549819828963,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"02c9ccc069857837\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549818545012,\n    \"duration\": 210542,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"682d9e58aab98286\",\n    \"id\": \"02c9ccc069857837\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549818531420,\n    \"duration\": 226192,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"682d9e58aab98286\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/complete\",\n    \"timestamp\": 1543549818530554,\n    \"duration\": 353251,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/complete\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"8a5cb4e5e4a8e435\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"ff7d62fadc66c179\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"499be60fed28ecbf\",\n    \"id\": \"1efde6e029e91ce2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811559769,\n    \"duration\": 1124,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"499be60fed28ecbf\",\n    \"id\": \"1efde6e029e91ce2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811558453,\n    \"duration\": 4090,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8549d6c0df1d304\",\n    \"id\": \"4a926abd813a4efb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811550805,\n    \"duration\": 949,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8549d6c0df1d304\",\n    \"id\": \"5a00ca1de79fcd4d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811548684,\n    \"duration\": 418,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8549d6c0df1d304\",\n    \"id\": \"fd5b4638ba39fd26\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811545944,\n    \"duration\": 711,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"499be60fed28ecbf\",\n    \"id\": \"f8549d6c0df1d304\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549811545006,\n    \"duration\": 7884,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.154.217\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"499be60fed28ecbf\",\n    \"id\": \"f8549d6c0df1d304\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549811544279,\n    \"duration\": 9993,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff7d62fadc66c179\",\n    \"id\": \"499be60fed28ecbf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549811544007,\n    \"duration\": 18917,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff7d62fadc66c179\",\n    \"id\": \"b1cf1fb7f533952a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549811536322,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549812567382,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ff7d62fadc66c179\",\n    \"id\": \"499be60fed28ecbf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811536327,\n    \"duration\": 33219,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"ff7d62fadc66c179\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811535012,\n    \"duration\": 34619,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"ff7d62fadc66c179\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549811490988,\n    \"duration\": 81404,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"1ba506d1e3889e5a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811450905,\n    \"duration\": 22719,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"330a27127e172547\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811445014,\n    \"duration\": 3850,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"40a53342e8758175\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811439568,\n    \"duration\": 1234,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"15800cea2cfa68d6\",\n    \"id\": \"023417e833dfeda5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549811412245,\n    \"duration\": 644,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.126\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"15800cea2cfa68d6\",\n    \"id\": \"06ac5f746b1d3bdd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549811412902,\n    \"duration\": 1286,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.126\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4817da80d8a26edc\",\n    \"id\": \"15800cea2cfa68d6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549811411034,\n    \"duration\": 3566,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.126\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 9892\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"4817da80d8a26edc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549811400458,\n    \"duration\": 36549,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.148.147\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 64556\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4817da80d8a26edc\",\n    \"id\": \"15800cea2cfa68d6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549811400661,\n    \"duration\": 20277,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.148.147\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"4817da80d8a26edc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811396363,\n    \"duration\": 40240,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f1b13cebe217c215\",\n    \"id\": \"3916293c5470064c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549811384046,\n    \"duration\": 877,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.80\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.77.250\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.77.250\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f1b13cebe217c215\",\n    \"id\": \"70600ec38dcad1d9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549811383227,\n    \"duration\": 806,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.80\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4a941a5f10d3c8e4\",\n    \"id\": \"f1b13cebe217c215\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549811382037,\n    \"duration\": 3215,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.80\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 26144\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"4a941a5f10d3c8e4\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549811367017,\n    \"duration\": 21507,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.20.6\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4a941a5f10d3c8e4\",\n    \"id\": \"f1b13cebe217c215\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549811367078,\n    \"duration\": 19378,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.20.6\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"4a941a5f10d3c8e4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549811360793,\n    \"duration\": 34234,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8541ca4fe8f3f619\",\n    \"id\": \"fbfd86fae5524bbf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549811356373,\n    \"duration\": 1415,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8a5cb4e5e4a8e435\",\n    \"id\": \"8541ca4fe8f3f619\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549811354010,\n    \"duration\": 128479,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8a5cb4e5e4a8e435\",\n    \"id\": \"f7928631d5a49935\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549811346380,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549813425962,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8a5cb4e5e4a8e435\",\n    \"id\": \"8541ca4fe8f3f619\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549811346389,\n    \"duration\": 142078,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"8a5cb4e5e4a8e435\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549811345015,\n    \"duration\": 143546,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b59adc98a7683a8\",\n    \"id\": \"8a5cb4e5e4a8e435\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549811333156,\n    \"duration\": 157312,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"5b59adc98a7683a8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549811313119,\n    \"duration\": 259693,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"de1590440afa19a8\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"51db1169297b8068\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f6b08f3a032fe06\",\n    \"id\": \"ad3832c3f050a831\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549804050169,\n    \"duration\": 1038,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.159.20\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f6b08f3a032fe06\",\n    \"id\": \"ad3832c3f050a831\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549804048204,\n    \"duration\": 4746,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9ab44c078c8b8b73\",\n    \"id\": \"ebadc09a14888b53\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549804036876,\n    \"duration\": 3100,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9ab44c078c8b8b73\",\n    \"id\": \"32c3e6039259464d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549804032598,\n    \"duration\": 965,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9ab44c078c8b8b73\",\n    \"id\": \"99c709d9eb8d5ff6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549804028104,\n    \"duration\": 1192,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f6b08f3a032fe06\",\n    \"id\": \"9ab44c078c8b8b73\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549804027008,\n    \"duration\": 15914,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f6b08f3a032fe06\",\n    \"id\": \"9ab44c078c8b8b73\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549804025303,\n    \"duration\": 18736,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51db1169297b8068\",\n    \"id\": \"8f6b08f3a032fe06\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549804025011,\n    \"duration\": 28430,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51db1169297b8068\",\n    \"id\": \"8f6b08f3a032fe06\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549804018528,\n    \"duration\": 41014,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51db1169297b8068\",\n    \"id\": \"ec7a7a0854123e0c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549804018523,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549817036597,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"51db1169297b8068\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549804017016,\n    \"duration\": 42621,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"51db1169297b8068\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549803990732,\n    \"duration\": 72806,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"2106ccfbb926c7c2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549803949289,\n    \"duration\": 21632,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"12cf0e4e54975186\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549803943845,\n    \"duration\": 3744,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"f6b376cd5f649911\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549803939295,\n    \"duration\": 1017,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"05da9b69cb1a3521\",\n    \"id\": \"659c647d35433d75\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549803916882,\n    \"duration\": 1361,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.90.9\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a869af8637f7afc6\",\n    \"id\": \"05da9b69cb1a3521\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549803915025,\n    \"duration\": 3087,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.90.9\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 44322\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"05da9b69cb1a3521\",\n    \"id\": \"48717cb3876edfcc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549803915807,\n    \"duration\": 1063,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.90.9\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a869af8637f7afc6\",\n    \"id\": \"05da9b69cb1a3521\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549803904971,\n    \"duration\": 19022,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.83.193\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"a869af8637f7afc6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549803904595,\n    \"duration\": 32333,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.83.193\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 38614\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"a869af8637f7afc6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549803895490,\n    \"duration\": 41517,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b0d4e3d1105b4a3b\",\n    \"id\": \"eea2971eeb9ac42e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549803881470,\n    \"duration\": 1192,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.77.250\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.77.250\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b0d4e3d1105b4a3b\",\n    \"id\": \"8e379997744aca73\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549803881024,\n    \"duration\": 435,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0bf1af0e2d418a40\",\n    \"id\": \"b0d4e3d1105b4a3b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549803880023,\n    \"duration\": 2936,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 7818\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"0bf1af0e2d418a40\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549803865008,\n    \"duration\": 21748,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.93.14\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0bf1af0e2d418a40\",\n    \"id\": \"b0d4e3d1105b4a3b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549803865051,\n    \"duration\": 18440,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.93.14\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"0bf1af0e2d418a40\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549803859686,\n    \"duration\": 34436,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"1a1e3d1f9a35bc33\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549803856466,\n    \"duration\": 1116,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3986746cd8518089\",\n    \"id\": \"131a2f374316608e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549803846779,\n    \"duration\": 778,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.94.0\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"3986746cd8518089\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549803843023,\n    \"duration\": 4836,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.94.0\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 36500\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3986746cd8518089\",\n    \"id\": \"025bd614d8df4866\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549803843834,\n    \"duration\": 968,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.94.0\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5648e0b2e07c4be5\",\n    \"id\": \"3986746cd8518089\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549803833099,\n    \"duration\": 21414,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de1590440afa19a8\",\n    \"id\": \"5648e0b2e07c4be5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549803833005,\n    \"duration\": 147501,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.25.92\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de1590440afa19a8\",\n    \"id\": \"5648e0b2e07c4be5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549803825034,\n    \"duration\": 161485,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"de1590440afa19a8\",\n    \"id\": \"8c43834f6251eb03\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549803824994,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549807304617,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"de1590440afa19a8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549803824026,\n    \"duration\": 162673,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4d6ef31d9b3b099f\",\n    \"id\": \"de1590440afa19a8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549803801488,\n    \"duration\": 188416,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"4d6ef31d9b3b099f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549803780956,\n    \"duration\": 425723,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"602819fc9af03824\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"f8be221cc18207d1\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"c004c7e5b4169a72\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771986841,\n    \"duration\": 1116,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"c004c7e5b4169a72\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771985811,\n    \"duration\": 4088,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.159.33\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"23213d866218e6ba\",\n    \"id\": \"9e4ce98a83ea393f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771977360,\n    \"duration\": 1040,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"23213d866218e6ba\",\n    \"id\": \"78549a9588808bb1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771973899,\n    \"duration\": 655,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"23213d866218e6ba\",\n    \"id\": \"5fef8136ca5b31df\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771971064,\n    \"duration\": 755,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"23213d866218e6ba\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549771970009,\n    \"duration\": 10297,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.159.33\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"23213d866218e6ba\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549771969728,\n    \"duration\": 11912,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.159.33\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"79b9ab805e916e96\",\n    \"id\": \"46df3a6fb3dc95a7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549771961625,\n    \"duration\": 780,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.16\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.64.95\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.64.95\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"79b9ab805e916e96\",\n    \"id\": \"34e622a77e3f1f0a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549771960309,\n    \"duration\": 1210,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.16\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.133.141\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.133.141\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"79b9ab805e916e96\",\n    \"id\": \"9a4dd87e049da5fa\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549771959308,\n    \"duration\": 938,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.16\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"79b9ab805e916e96\",\n    \"id\": \"32a0b8a0d691f9e8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549771958392,\n    \"duration\": 810,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.16\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"79b9ab805e916e96\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549771958025,\n    \"duration\": 4869,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.26.16\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 53498\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"602819fc9af03824\",\n    \"id\": \"62bf12725cd02965\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549771944009,\n    \"duration\": 46540,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.159.33\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62bf12725cd02965\",\n    \"id\": \"79b9ab805e916e96\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771944108,\n    \"duration\": 25331,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.159.33\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"//oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"602819fc9af03824\",\n    \"id\": \"7c75131098ecc344\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549771936159,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549801266527,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"602819fc9af03824\",\n    \"id\": \"62bf12725cd02965\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771936164,\n    \"duration\": 60835,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"602819fc9af03824\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771935005,\n    \"duration\": 62091,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"602819fc9af03824\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549771912826,\n    \"duration\": 87617,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"8ee44a066936fd6b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771851269,\n    \"duration\": 40330,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"b5ac06d21d2016a4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771844803,\n    \"duration\": 4007,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"af27211630a30b03\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771840055,\n    \"duration\": 675,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f574898f978c1c1\",\n    \"id\": \"fc8d009f3d1f74cd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549771815893,\n    \"duration\": 4237,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.27.196\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8f574898f978c1c1\",\n    \"id\": \"b6a0d9ea6aa228b2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549771815442,\n    \"duration\": 427,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.27.196\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c0af72aa9fada44c\",\n    \"id\": \"8f574898f978c1c1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549771814036,\n    \"duration\": 6550,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.27.196\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 21936\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"c0af72aa9fada44c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549771799252,\n    \"duration\": 39161,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.83.193\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 64758\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c0af72aa9fada44c\",\n    \"id\": \"8f574898f978c1c1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549771799495,\n    \"duration\": 26495,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.83.193\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"c0af72aa9fada44c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771795658,\n    \"duration\": 42192,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"af9884a9f4269373\",\n    \"id\": \"dce24a0eaba6f070\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549771781051,\n    \"duration\": 452,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"af9884a9f4269373\",\n    \"id\": \"e58224214b1a9c7a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549771781535,\n    \"duration\": 1060,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.77.250\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.77.250\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"888bfb34f7f73ab7\",\n    \"id\": \"af9884a9f4269373\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549771780022,\n    \"duration\": 2932,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.236\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 44782\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"888bfb34f7f73ab7\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549771767007,\n    \"duration\": 19818,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.25.172\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"888bfb34f7f73ab7\",\n    \"id\": \"af9884a9f4269373\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771767044,\n    \"duration\": 17391,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.25.172\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"888bfb34f7f73ab7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549771761179,\n    \"duration\": 31718,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"92c4f4d40cec5ec6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549771759382,\n    \"duration\": 746,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fa6e18f11fca2438\",\n    \"id\": \"f14358277a0cf07d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549771749641,\n    \"duration\": 858,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.212\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fa6e18f11fca2438\",\n    \"id\": \"9f9d62b0bce1c00b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549771748086,\n    \"duration\": 1539,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.212\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"fa6e18f11fca2438\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549771747033,\n    \"duration\": 3917,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.212\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 60806\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fba29d2b2c1cb37f\",\n    \"id\": \"fa6e18f11fca2438\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549771736114,\n    \"duration\": 21207,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8be221cc18207d1\",\n    \"id\": \"fba29d2b2c1cb37f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549771736013,\n    \"duration\": 166978,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8be221cc18207d1\",\n    \"id\": \"fba29d2b2c1cb37f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549771729372,\n    \"duration\": 180844,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f8be221cc18207d1\",\n    \"id\": \"2faf7ef66720afa1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549771729361,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549777570297,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"f8be221cc18207d1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549771728013,\n    \"duration\": 182307,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"d0ddc37a7b9e1044\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549771714166,\n    \"duration\": 445488,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"d0ddc37a7b9e1044\",\n    \"id\": \"f8be221cc18207d1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549771714993,\n    \"duration\": 197313,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"11fd76c8b53a4f3d\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"a9f8df66a0a938c5\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"da0c5bae3522adbe\",\n    \"id\": \"d8ba7616a08b7383\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758791109,\n    \"duration\": 1050,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.159.20\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"da0c5bae3522adbe\",\n    \"id\": \"d8ba7616a08b7383\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758789523,\n    \"duration\": 4010,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e15d37bdbc09eb04\",\n    \"id\": \"777c17f622668853\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758777163,\n    \"duration\": 5108,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e15d37bdbc09eb04\",\n    \"id\": \"2829e6f51f2e0bb3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758772623,\n    \"duration\": 954,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e15d37bdbc09eb04\",\n    \"id\": \"53c4870a8961ff7a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758767496,\n    \"duration\": 1187,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"da0c5bae3522adbe\",\n    \"id\": \"e15d37bdbc09eb04\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549758766005,\n    \"duration\": 19175,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.27.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.94.74\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"da0c5bae3522adbe\",\n    \"id\": \"e15d37bdbc09eb04\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549758765204,\n    \"duration\": 21226,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"11fd76c8b53a4f3d\",\n    \"id\": \"da0c5bae3522adbe\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549758765008,\n    \"duration\": 29035,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"11fd76c8b53a4f3d\",\n    \"id\": \"5537fd2c084b15f8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549758758202,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549769207256,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"11fd76c8b53a4f3d\",\n    \"id\": \"da0c5bae3522adbe\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758758207,\n    \"duration\": 42617,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"11fd76c8b53a4f3d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758757008,\n    \"duration\": 43926,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"11fd76c8b53a4f3d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549758733009,\n    \"duration\": 69619,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"4850c564cd53c778\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758673205,\n    \"duration\": 38964,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"b1c9de9ad439f414\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758667761,\n    \"duration\": 3382,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"5193e5ac31490283\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758663851,\n    \"duration\": 646,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce988807cfceea81\",\n    \"id\": \"e011e25ef2442c46\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549758643638,\n    \"duration\": 1030,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.139.111\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.139.111\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ce988807cfceea81\",\n    \"id\": \"6e5ce666abcbbbc9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549758642763,\n    \"duration\": 858,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fcc02ed94084986e\",\n    \"id\": \"ce988807cfceea81\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549758642032,\n    \"duration\": 2954,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 21554\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fcc02ed94084986e\",\n    \"id\": \"ce988807cfceea81\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549758629886,\n    \"duration\": 20930,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.21.119\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"fcc02ed94084986e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549758629631,\n    \"duration\": 32415,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.21.119\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 18058\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"fcc02ed94084986e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758627909,\n    \"duration\": 33871,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3a1cd6ab4d874eac\",\n    \"id\": \"201298d53493ca58\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549758597228,\n    \"duration\": 1168,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.155.76\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3a1cd6ab4d874eac\",\n    \"id\": \"f4aa239092387854\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549758596156,\n    \"duration\": 1042,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.155.76\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7d418daf2a7794ef\",\n    \"id\": \"3a1cd6ab4d874eac\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549758595025,\n    \"duration\": 3808,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.155.76\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\",\n      \"port\": 40082\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"7d418daf2a7794ef\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549758581009,\n    \"duration\": 38490,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.158.183\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7d418daf2a7794ef\",\n    \"id\": \"3a1cd6ab4d874eac\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758581056,\n    \"duration\": 18945,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.158.183\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"7d418daf2a7794ef\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549758574447,\n    \"duration\": 50878,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"625ca02fb0977b96\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549758572518,\n    \"duration\": 820,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7b5d741ec544aa1c\",\n    \"id\": \"218cbb55d3ab2876\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549758563822,\n    \"duration\": 556,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.29.206\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7b5d741ec544aa1c\",\n    \"id\": \"9dca1a02beca8262\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549758562776,\n    \"duration\": 1035,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.29.206\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"7b5d741ec544aa1c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549758562024,\n    \"duration\": 2810,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.29.206\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 25004\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a9f8df66a0a938c5\",\n    \"id\": \"be0382261cfc0f45\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549758552010,\n    \"duration\": 170234,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"be0382261cfc0f45\",\n    \"id\": \"7b5d741ec544aa1c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549758552086,\n    \"duration\": 18429,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.90.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a9f8df66a0a938c5\",\n    \"id\": \"be0382261cfc0f45\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549758545286,\n    \"duration\": 183316,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a9f8df66a0a938c5\",\n    \"id\": \"99f85a14ad746c5e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549758545276,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549761452221,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"a9f8df66a0a938c5\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549758544012,\n    \"duration\": 184691,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"700e6d6f181ebcbf\",\n    \"id\": \"a9f8df66a0a938c5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549758533801,\n    \"duration\": 198571,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"700e6d6f181ebcbf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549758513432,\n    \"duration\": 437633,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c3be75f04e628f93\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"55f73d4b5790f5fb\",\n    \"id\": \"96d65fe149f380c0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722898129,\n    \"duration\": 1051,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.159.20\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"55f73d4b5790f5fb\",\n    \"id\": \"96d65fe149f380c0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722896597,\n    \"duration\": 3146,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.158.255\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ab8775937fc05b73\",\n    \"id\": \"b95a87a3f5898fcc\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722887233,\n    \"duration\": 3001,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ab8775937fc05b73\",\n    \"id\": \"a8d021cc96fa4e27\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722884816,\n    \"duration\": 408,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ab8775937fc05b73\",\n    \"id\": \"e41a928edc4d9992\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722881663,\n    \"duration\": 482,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"55f73d4b5790f5fb\",\n    \"id\": \"ab8775937fc05b73\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549722880009,\n    \"duration\": 11849,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.158.255\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"55f73d4b5790f5fb\",\n    \"id\": \"ab8775937fc05b73\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549722879224,\n    \"duration\": 13317,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.158.255\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c3be75f04e628f93\",\n    \"id\": \"55f73d4b5790f5fb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549722879008,\n    \"duration\": 21207,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.158.255\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c23b86dd9d12cb0e\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c3be75f04e628f93\",\n    \"id\": \"481f0abcbdde9b60\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549722534421,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549725855840,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c3be75f04e628f93\",\n    \"id\": \"55f73d4b5790f5fb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722534427,\n    \"duration\": 372833,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c3be75f04e628f93\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722524010,\n    \"duration\": 383357,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c3be75f04e628f93\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549722506380,\n    \"duration\": 402935,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"85867aada0d78eef\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722439621,\n    \"duration\": 48335,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"d9827491c8653c6b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722433915,\n    \"duration\": 3246,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"5db575d64ddb78cf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722427528,\n    \"duration\": 651,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3d85b575382a817c\",\n    \"id\": \"6a7cd7b8ed200584\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549722404274,\n    \"duration\": 613,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.143\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3d85b575382a817c\",\n    \"id\": \"6f6cf5c71c7d3f75\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549722403255,\n    \"duration\": 1002,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.143\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1360f97f29ab4258\",\n    \"id\": \"3d85b575382a817c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549722402026,\n    \"duration\": 3265,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.143\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 63528\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1360f97f29ab4258\",\n    \"id\": \"3d85b575382a817c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549722391440,\n    \"duration\": 19353,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.154.66\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"1360f97f29ab4258\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549722391182,\n    \"duration\": 34593,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.154.66\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 37814\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"1360f97f29ab4258\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722387637,\n    \"duration\": 37154,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2d1922cd2e8b0af\",\n    \"id\": \"864cb5134dce5522\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549722374152,\n    \"duration\": 689,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.27\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2d1922cd2e8b0af\",\n    \"id\": \"bab9b718da935f22\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549722373185,\n    \"duration\": 952,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.27\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"26db735b54c04212\",\n    \"id\": \"f2d1922cd2e8b0af\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549722372032,\n    \"duration\": 3300,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.27\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\",\n      \"port\": 10470\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"26db735b54c04212\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549722357008,\n    \"duration\": 21385,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.157.220\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"26db735b54c04212\",\n    \"id\": \"f2d1922cd2e8b0af\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722357049,\n    \"duration\": 19339,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.157.220\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"26db735b54c04212\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549722350709,\n    \"duration\": 33750,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"9774e1489ad5042e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549722348509,\n    \"duration\": 763,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c6a10ed52bfa3bab\",\n    \"id\": \"da6107193aeb72eb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549722339051,\n    \"duration\": 1122,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.127\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c6a10ed52bfa3bab\",\n    \"id\": \"eae4a673e928260b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549722338085,\n    \"duration\": 950,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.127\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"c6a10ed52bfa3bab\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549722337031,\n    \"duration\": 3568,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.159.127\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 3752\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c23b86dd9d12cb0e\",\n    \"id\": \"4fb9906de4f2babe\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549722326009,\n    \"duration\": 171523,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fb9906de4f2babe\",\n    \"id\": \"c6a10ed52bfa3bab\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549722326093,\n    \"duration\": 20315,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c23b86dd9d12cb0e\",\n    \"id\": \"49982b3bd7066032\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549722319420,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549723222655,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c23b86dd9d12cb0e\",\n    \"id\": \"4fb9906de4f2babe\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549722319431,\n    \"duration\": 184389,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c23b86dd9d12cb0e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549722318033,\n    \"duration\": 185884,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"283e42f5bed80092\",\n    \"id\": \"c23b86dd9d12cb0e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549722308327,\n    \"duration\": 197438,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"283e42f5bed80092\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549722288406,\n    \"duration\": 1496268,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"ae0c6b3195bd7300\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"e2b1041505b44977\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0d3ebce773cb5430\",\n    \"id\": \"832a56e5c0973288\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639663910,\n    \"duration\": 773,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0d3ebce773cb5430\",\n    \"id\": \"832a56e5c0973288\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639662661,\n    \"duration\": 2899,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.29.115\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6bafefec74cc0c90\",\n    \"id\": \"d9e3a4a5226915eb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639651851,\n    \"duration\": 2618,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6bafefec74cc0c90\",\n    \"id\": \"b2cc59ee1fdc4026\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639648061,\n    \"duration\": 622,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6bafefec74cc0c90\",\n    \"id\": \"a7d0dd805180bac5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639644037,\n    \"duration\": 699,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0d3ebce773cb5430\",\n    \"id\": \"6bafefec74cc0c90\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549639643012,\n    \"duration\": 12873,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.29.115\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e2b1041505b44977\",\n    \"id\": \"0d3ebce773cb5430\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549639641013,\n    \"duration\": 24988,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.29.115\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"0d3ebce773cb5430\",\n    \"id\": \"6bafefec74cc0c90\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549639641240,\n    \"duration\": 15090,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.29.115\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"e2b1041505b44977\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639634013,\n    \"duration\": 39422,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e2b1041505b44977\",\n    \"id\": \"0d3ebce773cb5430\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639634755,\n    \"duration\": 38596,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e2b1041505b44977\",\n    \"id\": \"34b49600d162d99d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549639634750,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.146.177\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549641906955,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"e2b1041505b44977\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549639608970,\n    \"duration\": 66544,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"38269da9500c623e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639564487,\n    \"duration\": 26493,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"728c71f98bd2092c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639559606,\n    \"duration\": 2841,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"d2c7fcc27076ab5d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639555193,\n    \"duration\": 645,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5cc9966a68e7a087\",\n    \"id\": \"833970c3bfd1fd95\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549639535072,\n    \"duration\": 1164,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.151.17\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"beb7fdc6043c86c9\",\n    \"id\": \"5cc9966a68e7a087\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549639533023,\n    \"duration\": 3535,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.151.17\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 19998\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5cc9966a68e7a087\",\n    \"id\": \"1453a7af56c7e241\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549639533895,\n    \"duration\": 1158,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.151.17\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"beb7fdc6043c86c9\",\n    \"id\": \"5cc9966a68e7a087\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549639520338,\n    \"duration\": 22153,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.19.17\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"beb7fdc6043c86c9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549639520028,\n    \"duration\": 33695,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.19.17\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 15314\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"beb7fdc6043c86c9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639518286,\n    \"duration\": 35248,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"96ee29c337717963\",\n    \"id\": \"177844366dcd2267\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549639505469,\n    \"duration\": 832,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.71\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"96ee29c337717963\",\n    \"id\": \"cf2ad4385c04e347\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549639504949,\n    \"duration\": 513,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.71\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8129230b4f779eb\",\n    \"id\": \"96ee29c337717963\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549639504023,\n    \"duration\": 2612,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.71\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 33026\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"c8129230b4f779eb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549639490007,\n    \"duration\": 20036,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.26.140\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8129230b4f779eb\",\n    \"id\": \"96ee29c337717963\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639490047,\n    \"duration\": 17426,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.26.140\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"c8129230b4f779eb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549639483878,\n    \"duration\": 32267,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"d5b4506489636e51\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549639481620,\n    \"duration\": 653,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cdafe8d646a3d63b\",\n    \"id\": \"7879294f50f42e0e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549639471616,\n    \"duration\": 1296,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.157.111\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cdafe8d646a3d63b\",\n    \"id\": \"2ed5307deb70c3d2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549639471162,\n    \"duration\": 444,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.157.111\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"cdafe8d646a3d63b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549639470033,\n    \"duration\": 3325,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.157.111\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 22172\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9a3b97f905f2acd3\",\n    \"id\": \"cdafe8d646a3d63b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549639458126,\n    \"duration\": 21352,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae0c6b3195bd7300\",\n    \"id\": \"9a3b97f905f2acd3\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549639458012,\n    \"duration\": 141267,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae0c6b3195bd7300\",\n    \"id\": \"9a3b97f905f2acd3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549639450529,\n    \"duration\": 155188,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae0c6b3195bd7300\",\n    \"id\": \"6bab1f6379599d27\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549639450518,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549642952141,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"ae0c6b3195bd7300\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549639449016,\n    \"duration\": 156820,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.28.178\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7e93d316e899a60d\",\n    \"id\": \"ae0c6b3195bd7300\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549639434858,\n    \"duration\": 173354,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"7e93d316e899a60d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549639414549,\n    \"duration\": 433668,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"448d969bcee4c30c\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"6fbe292b17e1f060\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb2fefb66e710c28\",\n    \"id\": \"0ee5f57ceec47750\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627321172,\n    \"duration\": 716,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.20.164\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb2fefb66e710c28\",\n    \"id\": \"0ee5f57ceec47750\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627320108,\n    \"duration\": 3407,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dbe97f29d3faab02\",\n    \"id\": \"5271fa53f4693aa6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627307854,\n    \"duration\": 3337,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.30.237\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dbe97f29d3faab02\",\n    \"id\": \"784f780c2239f84f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627302184,\n    \"duration\": 1296,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.30.237\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"dbe97f29d3faab02\",\n    \"id\": \"b6df2be6998a4026\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627296038,\n    \"duration\": 1377,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.30.237\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb2fefb66e710c28\",\n    \"id\": \"dbe97f29d3faab02\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549627294017,\n    \"duration\": 20233,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.30.237\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.154.217\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"cb2fefb66e710c28\",\n    \"id\": \"dbe97f29d3faab02\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549627293253,\n    \"duration\": 22817,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"448d969bcee4c30c\",\n    \"id\": \"cb2fefb66e710c28\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549627293015,\n    \"duration\": 30774,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.154.217\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"448d969bcee4c30c\",\n    \"id\": \"4a528818ed9cfc16\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549627285451,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549634174035,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"448d969bcee4c30c\",\n    \"id\": \"cb2fefb66e710c28\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627285456,\n    \"duration\": 44979,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"448d969bcee4c30c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627272010,\n    \"duration\": 58549,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.151.35\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"448d969bcee4c30c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549627248595,\n    \"duration\": 84280,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"8118ec69de517cd7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627193728,\n    \"duration\": 38083,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"6d78d9ac13d78e88\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627188099,\n    \"duration\": 3472,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"95f22375764db86b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627183470,\n    \"duration\": 478,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"92d8a49253403c43\",\n    \"id\": \"a9fe6ba8b2d18e32\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549627146319,\n    \"duration\": 631,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.121\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"09c4cb418b0c97cd\",\n    \"id\": \"92d8a49253403c43\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549627145023,\n    \"duration\": 6443,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.121\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 48916\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"92d8a49253403c43\",\n    \"id\": \"b2e59552dac2abb2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549627145752,\n    \"duration\": 555,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.16.121\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"09c4cb418b0c97cd\",\n    \"id\": \"92d8a49253403c43\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549627128716,\n    \"duration\": 28517,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"09c4cb418b0c97cd\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549627128298,\n    \"duration\": 53446,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 6596\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"09c4cb418b0c97cd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627112974,\n    \"duration\": 68292,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3530c7132e2dc9bb\",\n    \"id\": \"d096821a38e5b51a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549627098284,\n    \"duration\": 836,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3530c7132e2dc9bb\",\n    \"id\": \"a4cbbb6c806bcf93\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549627096920,\n    \"duration\": 714,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3ecf03b7f3c35502\",\n    \"id\": \"3530c7132e2dc9bb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549627096024,\n    \"duration\": 3418,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.80.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 55366\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3ecf03b7f3c35502\",\n    \"id\": \"3530c7132e2dc9bb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627079077,\n    \"duration\": 21280,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.24.72\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"3ecf03b7f3c35502\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549627079013,\n    \"duration\": 24137,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.24.72\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"3ecf03b7f3c35502\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549627073574,\n    \"duration\": 36199,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"dc05ae6866f9fc6c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549627070337,\n    \"duration\": 964,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5e194d2f76a99df4\",\n    \"id\": \"ac9fe857232f02c0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549627060285,\n    \"duration\": 605,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.28.132\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"5e194d2f76a99df4\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549627059024,\n    \"duration\": 2160,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.28.132\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 46272\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5e194d2f76a99df4\",\n    \"id\": \"44e835ad060609b2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549627059744,\n    \"duration\": 536,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.28.132\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6fbe292b17e1f060\",\n    \"id\": \"007dcf34008f5924\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549627049015,\n    \"duration\": 190499,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"007dcf34008f5924\",\n    \"id\": \"5e194d2f76a99df4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549627049116,\n    \"duration\": 18725,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6fbe292b17e1f060\",\n    \"id\": \"be9e1de6787de1b8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549627042327,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549630825458,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6fbe292b17e1f060\",\n    \"id\": \"007dcf34008f5924\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549627042339,\n    \"duration\": 203376,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"6fbe292b17e1f060\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549627041013,\n    \"duration\": 204812,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ccd536a26f12be3e\",\n    \"id\": \"6fbe292b17e1f060\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549627028985,\n    \"duration\": 219032,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"ccd536a26f12be3e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549627008946,\n    \"duration\": 519422,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"378072a691af9055\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"a311a03fddc66037\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"c3341b60eeed88f6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549581028766,\n    \"duration\": 797,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"c3341b60eeed88f6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549581027417,\n    \"duration\": 3895,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ad8000fdbfc022b8\",\n    \"id\": \"8e4d22adadfa4a36\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549581015969,\n    \"duration\": 5075,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ad8000fdbfc022b8\",\n    \"id\": \"838e02b74537be40\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549581013777,\n    \"duration\": 562,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ad8000fdbfc022b8\",\n    \"id\": \"3e34509024300f3e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549581011041,\n    \"duration\": 805,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"ad8000fdbfc022b8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549581010009,\n    \"duration\": 12531,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.84.174\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"ad8000fdbfc022b8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549581009068,\n    \"duration\": 14132,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8ae6490506eafaf\",\n    \"id\": \"c7d49c3597ea446e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549581000455,\n    \"duration\": 1537,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.93.54\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8ae6490506eafaf\",\n    \"id\": \"6a20a302b2b578d1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549580999524,\n    \"duration\": 816,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.93.54\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8ae6490506eafaf\",\n    \"id\": \"2849b95f8f1a9327\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549580998445,\n    \"duration\": 1037,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.93.54\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.133.141\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.133.141\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"c8ae6490506eafaf\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549580997023,\n    \"duration\": 5249,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.93.54\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 52544\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c8ae6490506eafaf\",\n    \"id\": \"b2f8c7c1b112c225\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"access_token-select-by-oauth_token\",\n    \"timestamp\": 1543549580997334,\n    \"duration\": 1068,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.93.54\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.133.141\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.133.141\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a7b66d28149cd16c\",\n    \"id\": \"c8ae6490506eafaf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580987090,\n    \"duration\": 21751,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"//oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"378072a691af9055\",\n    \"id\": \"a7b66d28149cd16c\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549580987008,\n    \"duration\": 44636,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"378072a691af9055\",\n    \"id\": \"6fa168d3170cadab\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549580980334,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549599581397,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"378072a691af9055\",\n    \"id\": \"a7b66d28149cd16c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580980338,\n    \"duration\": 57264,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"378072a691af9055\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580979008,\n    \"duration\": 58689,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"378072a691af9055\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549580957607,\n    \"duration\": 81902,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"7dc453db52e5ab89\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549580898790,\n    \"duration\": 41399,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"7031027632a6b8bd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549580893227,\n    \"duration\": 3122,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"bc5b1a6282c31659\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549580888465,\n    \"duration\": 617,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"67905a89fc57c332\",\n    \"id\": \"f99d226d8ca6df45\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549580869906,\n    \"duration\": 909,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"67905a89fc57c332\",\n    \"id\": \"72efb4646571e403\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549580868799,\n    \"duration\": 1080,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"63b9e94b8c26bf0f\",\n    \"id\": \"67905a89fc57c332\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549580868023,\n    \"duration\": 3116,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.96\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 54752\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"63b9e94b8c26bf0f\",\n    \"id\": \"67905a89fc57c332\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549580857971,\n    \"duration\": 19607,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"63b9e94b8c26bf0f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549580857742,\n    \"duration\": 29890,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 64302\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"63b9e94b8c26bf0f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580854526,\n    \"duration\": 32394,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"e27e7fbb6ad832bf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549580765304,\n    \"duration\": 88456,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/apps/94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3d618feaba95bfd0\",\n    \"id\": \"182539481a751463\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549580752581,\n    \"duration\": 811,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.110\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3d618feaba95bfd0\",\n    \"id\": \"0863708274b7da13\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549580751160,\n    \"duration\": 1409,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.110\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8374b8a8ab198e6a\",\n    \"id\": \"3d618feaba95bfd0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549580750037,\n    \"duration\": 3693,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.86.110\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 54108\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8374b8a8ab198e6a\",\n    \"id\": \"3d618feaba95bfd0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580724045,\n    \"duration\": 29967,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.88.141\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"8374b8a8ab198e6a\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549580724011,\n    \"duration\": 32113,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.88.141\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"8374b8a8ab198e6a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549580718499,\n    \"duration\": 44074,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"d9c36d4b4673309c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549580716482,\n    \"duration\": 782,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"75c88974fc3a3530\",\n    \"id\": \"96003ddacbb9f44a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549580705972,\n    \"duration\": 1056,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.91.139\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.139.111\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.139.111\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"75c88974fc3a3530\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549580704024,\n    \"duration\": 3341,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.91.139\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 28334\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"75c88974fc3a3530\",\n    \"id\": \"81fcddae3b607591\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549580704836,\n    \"duration\": 1124,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.91.139\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1da3cf40bafab159\",\n    \"id\": \"75c88974fc3a3530\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549580693086,\n    \"duration\": 20947,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a311a03fddc66037\",\n    \"id\": \"1da3cf40bafab159\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549580693010,\n    \"duration\": 255625,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a311a03fddc66037\",\n    \"id\": \"1da3cf40bafab159\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549580686564,\n    \"duration\": 268641,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a311a03fddc66037\",\n    \"id\": \"a256534086b5c510\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549580686554,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549599581412,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"a311a03fddc66037\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549580685012,\n    \"duration\": 270307,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.90.167\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"73faa40841b988ef\",\n    \"id\": \"a311a03fddc66037\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549580674286,\n    \"duration\": 282598,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"73faa40841b988ef\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549580673421,\n    \"duration\": 509980,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"fde9649106754b46\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"5b82d57d3ecf8c99\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51036587b5219e6b\",\n    \"id\": \"eb36532d43295ef3\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561850583,\n    \"duration\": 729,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.159.20\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51036587b5219e6b\",\n    \"id\": \"eb36532d43295ef3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561849755,\n    \"duration\": 2811,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"19f4bfa4ea1d87b8\",\n    \"id\": \"6e72b7a4a78457cf\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561840511,\n    \"duration\": 2358,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"19f4bfa4ea1d87b8\",\n    \"id\": \"2b2912de5c08198a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561838027,\n    \"duration\": 396,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51036587b5219e6b\",\n    \"id\": \"19f4bfa4ea1d87b8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549561835009,\n    \"duration\": 9602,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"19f4bfa4ea1d87b8\",\n    \"id\": \"d47ba6c8dcb09510\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561835960,\n    \"duration\": 556,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"51036587b5219e6b\",\n    \"id\": \"19f4bfa4ea1d87b8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549561834320,\n    \"duration\": 11246,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fde9649106754b46\",\n    \"id\": \"51036587b5219e6b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549561834008,\n    \"duration\": 18866,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fde9649106754b46\",\n    \"id\": \"51036587b5219e6b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561826761,\n    \"duration\": 32931,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fde9649106754b46\",\n    \"id\": \"e1642bbfaffbd956\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549561826753,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549565188839,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"fde9649106754b46\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561825017,\n    \"duration\": 34866,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"fde9649106754b46\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549561799963,\n    \"duration\": 61764,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"4f6b9f5e9d09574a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561752220,\n    \"duration\": 29328,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"d23d0285b84742d0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561747660,\n    \"duration\": 2667,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"065742de24f831da\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561743886,\n    \"duration\": 475,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4ff03ac025303a99\",\n    \"id\": \"8da4fa18263e7039\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549561722485,\n    \"duration\": 1104,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.203\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4ff03ac025303a99\",\n    \"id\": \"84bb7be839d78bb7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549561721608,\n    \"duration\": 729,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.203\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"11540e551268624d\",\n    \"id\": \"4ff03ac025303a99\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549561719025,\n    \"duration\": 4904,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.152.203\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 2426\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"11540e551268624d\",\n    \"id\": \"4ff03ac025303a99\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549561700094,\n    \"duration\": 29676,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"11540e551268624d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549561699830,\n    \"duration\": 43267,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.89.141\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 63308\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"11540e551268624d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561695884,\n    \"duration\": 46307,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"75dbd5124aa06478\",\n    \"id\": \"4b3a0ed18bed073d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549561682344,\n    \"duration\": 1116,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.95\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.139.111\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.139.111\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"75dbd5124aa06478\",\n    \"id\": \"4e5509b3a1567270\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549561681756,\n    \"duration\": 539,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.95\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6046dd8f91dde9ef\",\n    \"id\": \"75dbd5124aa06478\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549561681023,\n    \"duration\": 2735,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.88.95\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 15840\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"6046dd8f91dde9ef\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549561668008,\n    \"duration\": 18840,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.16.214\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6046dd8f91dde9ef\",\n    \"id\": \"75dbd5124aa06478\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561668045,\n    \"duration\": 16462,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.16.214\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"6046dd8f91dde9ef\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549561662191,\n    \"duration\": 30986,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"7b627d891ae25fa7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549561660604,\n    \"duration\": 624,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"80e27ab232246a83\",\n    \"id\": \"91cb3ad1f4df7c04\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549561651670,\n    \"duration\": 638,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"80e27ab232246a83\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549561650026,\n    \"duration\": 2732,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 42794\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"80e27ab232246a83\",\n    \"id\": \"a32f881c0506c2d4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549561650980,\n    \"duration\": 673,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.25.22\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e5decc9a4cc88935\",\n    \"id\": \"80e27ab232246a83\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549561639173,\n    \"duration\": 18791,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b82d57d3ecf8c99\",\n    \"id\": \"e5decc9a4cc88935\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549561639011,\n    \"duration\": 151770,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b82d57d3ecf8c99\",\n    \"id\": \"fc0cceaa8d305ab9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549561631956,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549584258192,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5b82d57d3ecf8c99\",\n    \"id\": \"e5decc9a4cc88935\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549561631975,\n    \"duration\": 165936,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"5b82d57d3ecf8c99\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549561630017,\n    \"duration\": 168061,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b7a3f274310f1e1e\",\n    \"id\": \"5b82d57d3ecf8c99\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549561602784,\n    \"duration\": 196535,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"b7a3f274310f1e1e\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549561601540,\n    \"duration\": 453213,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"e7ebb345032e77f8\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"ae4c5a71880069ec\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fddea6f54df8372d\",\n    \"id\": \"eb27c5c795d97068\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549549004379,\n    \"duration\": 696,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fddea6f54df8372d\",\n    \"id\": \"eb27c5c795d97068\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549549003140,\n    \"duration\": 2984,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.16.124\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fcb20fb81d257bdd\",\n    \"id\": \"40a1dc784902f66a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548993979,\n    \"duration\": 604,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fcb20fb81d257bdd\",\n    \"id\": \"8d9816f3395f559f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548990655,\n    \"duration\": 390,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fcb20fb81d257bdd\",\n    \"id\": \"65ce41dd26ef4fc2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548988299,\n    \"duration\": 598,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fddea6f54df8372d\",\n    \"id\": \"fcb20fb81d257bdd\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549548987014,\n    \"duration\": 9509,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.148.175\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.16.124\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fddea6f54df8372d\",\n    \"id\": \"fcb20fb81d257bdd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549548985251,\n    \"duration\": 11990,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.16.124\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e7ebb345032e77f8\",\n    \"id\": \"fddea6f54df8372d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549548985030,\n    \"duration\": 21404,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.16.124\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e7ebb345032e77f8\",\n    \"id\": \"e02749f370ff2326\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549548978338,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549556764397,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e7ebb345032e77f8\",\n    \"id\": \"fddea6f54df8372d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549548978343,\n    \"duration\": 34616,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"e7ebb345032e77f8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549548977010,\n    \"duration\": 36111,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"e7ebb345032e77f8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549548958204,\n    \"duration\": 56904,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"620cfde59dd81244\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548914992,\n    \"duration\": 26788,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"52b41a9fabbc2d8f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548909807,\n    \"duration\": 3276,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"ee3276217fe89874\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548904974,\n    \"duration\": 683,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2dbb8b326cfde80\",\n    \"id\": \"ed80116be61f5a82\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549548878406,\n    \"duration\": 1062,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.72\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.139.111\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.139.111\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f2dbb8b326cfde80\",\n    \"id\": \"59d1dae2afdd71de\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549548877218,\n    \"duration\": 1163,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.72\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.79.117\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.79.117\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2f6bdae41338cdab\",\n    \"id\": \"f2dbb8b326cfde80\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549548876038,\n    \"duration\": 3894,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.31.72\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\",\n      \"port\": 40026\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2f6bdae41338cdab\",\n    \"id\": \"f2dbb8b326cfde80\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549548864754,\n    \"duration\": 21353,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.16.147\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"2f6bdae41338cdab\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549548864490,\n    \"duration\": 36816,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.16.147\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 1880\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"2f6bdae41338cdab\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549548863478,\n    \"duration\": 38683,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f29e6ae54e348089\",\n    \"id\": \"e5f012006622e3e5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549548850263,\n    \"duration\": 714,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.18.29\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f29e6ae54e348089\",\n    \"id\": \"df4b013a7af7c8ac\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549548849304,\n    \"duration\": 944,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.18.29\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"81fafdc20e80b2c2\",\n    \"id\": \"f29e6ae54e348089\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549548848033,\n    \"duration\": 3280,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.18.29\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 62982\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"81fafdc20e80b2c2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549548835008,\n    \"duration\": 18594,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.91.185\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"81fafdc20e80b2c2\",\n    \"id\": \"f29e6ae54e348089\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549548835041,\n    \"duration\": 16991,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.91.185\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"81fafdc20e80b2c2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549548829500,\n    \"duration\": 30753,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"b5fb9ab1a0ed467a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549548827419,\n    \"duration\": 620,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3947bac369f090cb\",\n    \"id\": \"7a365a7d8e404216\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549548817139,\n    \"duration\": 729,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.98\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.77.250\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.77.250\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3947bac369f090cb\",\n    \"id\": \"01ef3f2d835f952e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549548816133,\n    \"duration\": 977,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.98\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"3947bac369f090cb\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549548815033,\n    \"duration\": 3171,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.98\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 24662\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"8e34795e90e64dc6\",\n    \"id\": \"3947bac369f090cb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549548804159,\n    \"duration\": 20102,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae4c5a71880069ec\",\n    \"id\": \"8e34795e90e64dc6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549548804020,\n    \"duration\": 144919,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.149.149\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae4c5a71880069ec\",\n    \"id\": \"bed30f7a3c2f4035\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549548796542,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549552001350,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ae4c5a71880069ec\",\n    \"id\": \"8e34795e90e64dc6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549548796552,\n    \"duration\": 158879,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"ae4c5a71880069ec\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549548795012,\n    \"duration\": 160523,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.83.117\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"94e64270f1f51ea2\",\n    \"id\": \"ae4c5a71880069ec\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549548783072,\n    \"duration\": 174711,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"94e64270f1f51ea2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549548782387,\n    \"duration\": 372860,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"232d5fce7afe19e2\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"e268a0fcd7c41b98\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1f23804b530fae49\",\n    \"id\": \"dfaf8fd32da3c140\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537370636,\n    \"duration\": 681,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.20.164\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.6.245\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1f23804b530fae49\",\n    \"id\": \"dfaf8fd32da3c140\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537369985,\n    \"duration\": 2676,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.23.211\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"43e69c0c509fcd03\",\n    \"id\": \"81a79d4b839cd7d1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537359707,\n    \"duration\": 842,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"43e69c0c509fcd03\",\n    \"id\": \"2284c3f83a198e9d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537356406,\n    \"duration\": 625,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"43e69c0c509fcd03\",\n    \"id\": \"4d4da64186a30f27\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537353041,\n    \"duration\": 821,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1f23804b530fae49\",\n    \"id\": \"43e69c0c509fcd03\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549537352005,\n    \"duration\": 10122,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.23.211\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1f23804b530fae49\",\n    \"id\": \"43e69c0c509fcd03\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549537351342,\n    \"duration\": 12279,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.23.211\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e268a0fcd7c41b98\",\n    \"id\": \"1f23804b530fae49\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549537351013,\n    \"duration\": 21982,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.23.211\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e268a0fcd7c41b98\",\n    \"id\": \"dbededcf5eb8aee4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549537344496,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549541427459,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"e268a0fcd7c41b98\",\n    \"id\": \"1f23804b530fae49\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537344503,\n    \"duration\": 35043,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"e268a0fcd7c41b98\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537343012,\n    \"duration\": 36639,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"e268a0fcd7c41b98\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549537321557,\n    \"duration\": 58522,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"45988000d5c52259\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537276669,\n    \"duration\": 29403,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"49664734f6bd3648\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537251330,\n    \"duration\": 3020,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"a4c38b4bbd5a4ed4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537246063,\n    \"duration\": 670,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"90331322ad760880\",\n    \"id\": \"7ce772e725118a94\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549537220437,\n    \"duration\": 2337,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"90331322ad760880\",\n    \"id\": \"72ec6c5fc1f83bc4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549537219025,\n    \"duration\": 1386,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9612c52bf1b2b761\",\n    \"id\": \"90331322ad760880\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549537218023,\n    \"duration\": 5198,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 10644\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"9612c52bf1b2b761\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549537207003,\n    \"duration\": 36396,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.149.58\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 32726\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9612c52bf1b2b761\",\n    \"id\": \"90331322ad760880\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549537207275,\n    \"duration\": 21530,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.149.58\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"9612c52bf1b2b761\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537202518,\n    \"duration\": 40809,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1c1fddf8d347e2d8\",\n    \"id\": \"33de5906d011e16c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549537171841,\n    \"duration\": 963,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.83.156\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"31a6e202b17f2e78\",\n    \"id\": \"1c1fddf8d347e2d8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549537170024,\n    \"duration\": 3087,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.83.156\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 33908\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"1c1fddf8d347e2d8\",\n    \"id\": \"543ddf057a11a4db\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549537170833,\n    \"duration\": 999,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.83.156\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"31a6e202b17f2e78\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549537157011,\n    \"duration\": 36750,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.26.44\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"31a6e202b17f2e78\",\n    \"id\": \"1c1fddf8d347e2d8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549537157052,\n    \"duration\": 17053,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.26.44\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"31a6e202b17f2e78\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549537149343,\n    \"duration\": 50135,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"f95a4f440d6cd590\",\n    \"id\": \"f255d0167d26b448\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549537147161,\n    \"duration\": 696,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"232d5fce7afe19e2\",\n    \"id\": \"f95a4f440d6cd590\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549537145005,\n    \"duration\": 168536,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"232d5fce7afe19e2\",\n    \"id\": \"f95a4f440d6cd590\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549537138773,\n    \"duration\": 180863,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"232d5fce7afe19e2\",\n    \"id\": \"e50040446402b65c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549537138763,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549539454523,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"232d5fce7afe19e2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549537137012,\n    \"duration\": 182727,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.29.90\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"7f7a06d5221fbc8f\",\n    \"id\": \"232d5fce7afe19e2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549537126217,\n    \"duration\": 194755,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"7f7a06d5221fbc8f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/page\",\n    \"timestamp\": 1543549537124575,\n    \"duration\": 448571,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.configuration_id\": \"52327493-a185-49d4-972c-XXXXXXXXXXXX\",\n      \"app.id\": \"94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/page\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"5939bf9d81310e75\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"88a252a635ca2680\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"fa62921f213298f6\",\n    \"name\": \"post\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"95c6ad5da593ed4d\",\n    \"name\": \"put installedapps/_installedappid_/init-update-flow\",\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a1120f45cd2c449d\",\n    \"id\": \"8f42039ea396d320\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525540949,\n    \"duration\": 720,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.82.32\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a1120f45cd2c449d\",\n    \"id\": \"8f42039ea396d320\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525539530,\n    \"duration\": 3916,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6459381eb9f5b58f\",\n    \"id\": \"d599ff060b2fc626\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525532299,\n    \"duration\": 819,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6459381eb9f5b58f\",\n    \"id\": \"7f8e10b6b086de8d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525529032,\n    \"duration\": 613,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"6459381eb9f5b58f\",\n    \"id\": \"41d8ea341da5ebdd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525525618,\n    \"duration\": 672,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a1120f45cd2c449d\",\n    \"id\": \"6459381eb9f5b58f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549525523009,\n    \"duration\": 11759,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.80.182\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.157.182\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a1120f45cd2c449d\",\n    \"id\": \"6459381eb9f5b58f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525522207,\n    \"duration\": 13245,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fa62921f213298f6\",\n    \"id\": \"a1120f45cd2c449d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549525522005,\n    \"duration\": 21674,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.157.182\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"fa62921f213298f6\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525515011,\n    \"duration\": 35744,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fa62921f213298f6\",\n    \"id\": \"284b52a4b74c5265\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525515924,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549536851482,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"fa62921f213298f6\",\n    \"id\": \"a1120f45cd2c449d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525515929,\n    \"duration\": 34669,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.156.242\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"fa62921f213298f6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549525492031,\n    \"duration\": 60552,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"95f6b42bcd86c5be\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525443219,\n    \"duration\": 29797,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"2ae636ef4c5cce21\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525438256,\n    \"duration\": 3053,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"e159b29d89c74c6c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525433664,\n    \"duration\": 613,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9c579cc3eb3339e9\",\n    \"id\": \"b51d6063b529653c\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525411222,\n    \"duration\": 860,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.92.113\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9c579cc3eb3339e9\",\n    \"id\": \"0aa70e770499ae33\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549525409844,\n    \"duration\": 1359,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.92.113\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c0b394389ef3e987\",\n    \"id\": \"9c579cc3eb3339e9\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525409026,\n    \"duration\": 3395,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.92.113\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 18682\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"c0b394389ef3e987\",\n    \"id\": \"9c579cc3eb3339e9\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525399068,\n    \"duration\": 19969,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.88.125\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"c0b394389ef3e987\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /api/devices/validateownership\",\n    \"timestamp\": 1543549525398770,\n    \"duration\": 33833,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.6.88.125\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 64938\n    },\n    \"tags\": {\n      \"http.path\": \"/api/devices/validateOwnership\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"c0b394389ef3e987\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525394431,\n    \"duration\": 37331,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/api/devices/validateOwnership\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ab258224e037196b\",\n    \"id\": \"7682c2bd8e2a08ea\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525380372,\n    \"duration\": 808,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.23.28\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ab258224e037196b\",\n    \"id\": \"43e8344bd902268e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549525379735,\n    \"duration\": 629,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.23.28\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"02d0b5ebf7e49015\",\n    \"id\": \"ab258224e037196b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525379027,\n    \"duration\": 2494,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.23.28\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\",\n      \"port\": 63712\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"02d0b5ebf7e49015\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549525365012,\n    \"duration\": 20214,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.95.251\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"02d0b5ebf7e49015\",\n    \"id\": \"ab258224e037196b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525365048,\n    \"duration\": 17651,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.95.251\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"02d0b5ebf7e49015\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525357955,\n    \"duration\": 33278,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"152482b4ae27188a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525356389,\n    \"duration\": 555,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"41c7ab2d16dc68ad\",\n    \"id\": \"d339b3bde47fa904\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525346690,\n    \"duration\": 616,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.30\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.3.24\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.3.24\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"41c7ab2d16dc68ad\",\n    \"id\": \"c95dd98dd5277ba4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549525345737,\n    \"duration\": 938,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.30\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"41c7ab2d16dc68ad\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525345027,\n    \"duration\": 2746,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.20.30\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.19.244\",\n      \"port\": 63452\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"754f06c329b5c8f0\",\n    \"id\": \"41c7ab2d16dc68ad\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525335110,\n    \"duration\": 18690,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"88a252a635ca2680\",\n    \"id\": \"754f06c329b5c8f0\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/configs\",\n    \"timestamp\": 1543549525335007,\n    \"duration\": 146271,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"updateInstallConfiguration\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"88a252a635ca2680\",\n    \"id\": \"85c4bcff360be1ce\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525327763,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549527854222,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"88a252a635ca2680\",\n    \"id\": \"754f06c329b5c8f0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549525327773,\n    \"duration\": 160160,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"88a252a635ca2680\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/configs\",\n    \"timestamp\": 1543549525326007,\n    \"duration\": 162050,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"88a252a635ca2680\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/configs\",\n    \"timestamp\": 1543549525317928,\n    \"duration\": 173503,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/configs\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"643b3fc727984319\",\n    \"id\": \"70d7d4b6bfb8062d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525307334,\n    \"duration\": 620,\n    \"localEndpoint\": {\n      \"serviceName\": \"guardian\",\n      \"ipv4\": \"10.5.20.164\",\n      \"port\": 8860\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute-shm\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"643b3fc727984319\",\n    \"id\": \"70d7d4b6bfb8062d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525306481,\n    \"duration\": 2478,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute-shm\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fbe6038c0c296df\",\n    \"id\": \"bea03649c5d977fb\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525300972,\n    \"duration\": 491,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fbe6038c0c296df\",\n    \"id\": \"0c86dce876b8100a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525298670,\n    \"duration\": 399,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"4fbe6038c0c296df\",\n    \"id\": \"33a85d4a785371d1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525296113,\n    \"duration\": 520,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"643b3fc727984319\",\n    \"id\": \"4fbe6038c0c296df\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /installedapps/{installedappid}/staged\",\n    \"timestamp\": 1543549525295007,\n    \"duration\": 7523,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.158.238\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.6.94.74\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\",\n      \"jaxrs.resource.class\": \"InternalInstalledAppResource\",\n      \"jaxrs.resource.method\": \"findStagedInstalledApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5939bf9d81310e75\",\n    \"id\": \"643b3fc727984319\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549525294005,\n    \"duration\": 15142,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\"\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/app-execute\",\n      \"jaxrs.resource.class\": \"AppExecuteResource\",\n      \"jaxrs.resource.method\": \"executeApp\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"643b3fc727984319\",\n    \"id\": \"4fbe6038c0c296df\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525294187,\n    \"duration\": 9123,\n    \"localEndpoint\": {\n      \"serviceName\": \"execution\",\n      \"ipv4\": \"10.6.94.74\",\n      \"port\": 8600\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/staged\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5939bf9d81310e75\",\n    \"id\": \"34d28886561484ce\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525287392,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549527854490,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"5939bf9d81310e75\",\n    \"id\": \"643b3fc727984319\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525287399,\n    \"duration\": 28599,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"5939bf9d81310e75\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525286016,\n    \"duration\": 30093,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.17.22\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"app-execute\",\n      \"oauth.additionalInfo.scope\": \"[service]\",\n      \"oauth.clientId\": \"57a6c0a8-cac9-4921-a349-267092837d72\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"5939bf9d81310e75\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /app-execute\",\n    \"timestamp\": 1543549525266205,\n    \"duration\": 50971,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/app-execute\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"b5520e1ff6c6774b\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /clients/_uuid_\",\n    \"timestamp\": 1543549525263034,\n    \"duration\": 2188,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.28.238\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.65.17\",\n      \"port\": 40600\n    },\n    \"tags\": {\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b5520e1ff6c6774b\",\n    \"id\": \"92b8357930dadfa4\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525263885,\n    \"duration\": 896,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.28.238\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.78.33\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.78.33\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"b5520e1ff6c6774b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get /clients/_uuid_\",\n    \"timestamp\": 1543549525248209,\n    \"duration\": 17685,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/clients/2291b8a1-fe35-461a-860c-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"8840058be390ea68\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get /apps/_uuid_/composite\",\n    \"timestamp\": 1543549525219056,\n    \"duration\": 28862,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/apps/94ae382e-8903-41fb-ba4e-XXXXXXXXXXXX/composite\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"ab009803cf70f313\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525180044,\n    \"duration\": 20752,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO config (\\n            install_configuration_id,\\n            name,\\n            value_type,\\n            value,\\n            device_id,\\n            component_id,\\n            permissions,\\n            mode_id,\\n            scene_id\\n        )\\n        VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"54635d5e1dc6b20b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525177466,\n    \"duration\": 917,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        DELETE\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"0783395a6fbd19c7\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525167196,\n    \"duration\": 1308,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        INSERT INTO install_configuration(\\n            id,\\n            installed_app_id,\\n            status,\\n            date_created,\\n            last_updated\\n        ) VALUES (\\n            ?,\\n            ?,\\n            ?,\\n            ?,\\n            ?\\n        )\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"8344a4b21eea3167\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525164695,\n    \"duration\": 380,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"ed4902c4aa95b1b6\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525162403,\n    \"duration\": 520,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT install_configuration_id,\\n               name,\\n               value_type,\\n               value,\\n               device_id,\\n               component_id,\\n               permissions,\\n               mode_id,\\n               scene_id\\n          FROM config\\n         WHERE install_configuration_id = ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"c155a67eb6a2f4dd\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525160206,\n    \"duration\": 419,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT id,\\n               installed_app_id,\\n               status,\\n               date_created,\\n               last_updated\\n          FROM install_configuration\\n         WHERE installed_app_id = ?\\n           AND status in (?)\\n         ORDER BY last_updated DESC\\n         LIMIT ?, ?\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2ff57ae413c1e257\",\n    \"id\": \"6a5f8a605357fc9a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525147176,\n    \"duration\": 817,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.135\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2ff57ae413c1e257\",\n    \"id\": \"9a5453b870e5f2e3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549525146175,\n    \"duration\": 988,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.135\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ba0828419743cc96\",\n    \"id\": \"2ff57ae413c1e257\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525145024,\n    \"duration\": 3312,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.81.135\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.64.193\",\n      \"port\": 53832\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"ba0828419743cc96\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /locations/\",\n    \"timestamp\": 1543549525133015,\n    \"duration\": 18817,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.20.180\",\n      \"port\": 8181\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\"\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"ba0828419743cc96\",\n    \"id\": \"2ff57ae413c1e257\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525133094,\n    \"duration\": 16013,\n    \"localEndpoint\": {\n      \"serviceName\": \"account\",\n      \"ipv4\": \"10.104.20.180\",\n      \"port\": 8181\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"ba0828419743cc96\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525127271,\n    \"duration\": 31566,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"GET\",\n      \"http.path\": \"/locations\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"692f097cfe2302a1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"\\n\",\n    \"timestamp\": 1543549525125269,\n    \"duration\": 605,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"bookie\"\n    },\n    \"tags\": {\n      \"sql.query\": \"\\n        SELECT ia.id,\\n               ia.display_name,\\n               ia.installed_app_type,\\n               ia.install_status,\\n               ia.app_id,\\n               ia.reference_id,\\n               ia.location_id,\\n               ia.owner_type,\\n               ia.owner_id,\\n               ia.install_app_notice,\\n               ia.is_deleted,\\n               ia.date_created,\\n               ia.last_updated,\\n               app.plugin_id,\\n               app.dashboard_cards_enabled,\\n               app.preinstall_dashboard_cards_enabled,\\n               app.icon_image_url,\\n               app.display_name AS app_display_name,\\n               app.app_name,\\n               group_concat(app_tag.name order by app_tag.name ASC SEPARATOR ',') AS app_tag_name  \\n          FROM installed_app AS ia\\n          LEFT JOIN app ON ia.app_id = app.app_id\\n          LEFT JOIN app_tag ON ia.app_id = app_tag.app_id AND app_tag.name like 'st:class:%'   \\n         WHERE ia.id = ?\\n         GROUP BY ia.id\\n    \"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"30f66d9cfdb0765f\",\n    \"id\": \"76bcb9c0ad7ab13f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549525115322,\n    \"duration\": 1205,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.146.1\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"30f66d9cfdb0765f\",\n    \"id\": \"68cb0047894fe7ac\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549525114739,\n    \"duration\": 563,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.146.1\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.129.42\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.129.42\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"30f66d9cfdb0765f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549525114030,\n    \"duration\": 2823,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.146.1\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"52.14.10.9\",\n      \"port\": 27698\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"2dfd66410d87952a\",\n    \"id\": \"30f66d9cfdb0765f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1543549525104077,\n    \"duration\": 18771,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"tags\": {\n      \"http.method\": \"POST\",\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"95c6ad5da593ed4d\",\n    \"id\": \"2dfd66410d87952a\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put /installedapps/{installedappid}/init-update-flow\",\n    \"timestamp\": 1543549525104005,\n    \"duration\": 106058,\n    \"localEndpoint\": {\n      \"serviceName\": \"bookie\",\n      \"ipv4\": \"10.6.87.106\",\n      \"port\": 8202\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"34.200.62.214\"\n    },\n    \"tags\": {\n      \"http.method\": \"PUT\",\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/init-update-flow\",\n      \"jaxrs.resource.class\": \"InstalledAppResource\",\n      \"jaxrs.resource.method\": \"initializeUpdateFlow\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"95c6ad5da593ed4d\",\n    \"id\": \"5565f8e3c03ef068\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1543549525096635,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1543549527766692,\n        \"value\": \"brave.flush\"\n      }\n    ],\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/init-update-flow\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"95c6ad5da593ed4d\",\n    \"id\": \"2dfd66410d87952a\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put\",\n    \"timestamp\": 1543549525096645,\n    \"duration\": 119715,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/init-update-flow\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"95c6ad5da593ed4d\",\n    \"kind\": \"SERVER\",\n    \"name\": \"put installedapps/_installedappid_/init-update-flow\",\n    \"timestamp\": 1543549525095014,\n    \"duration\": 121444,\n    \"localEndpoint\": {\n      \"serviceName\": \"platformapi\",\n      \"ipv4\": \"10.104.87.185\",\n      \"port\": 8140\n    },\n    \"tags\": {\n      \"http.path\": \"installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/init-update-flow\",\n      \"oauth.additionalInfo.principal\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\",\n      \"oauth.additionalInfo.scope\": \"[mobile]\",\n      \"oauth.clientId\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"st.version\": \"DEFAULT_VERSION: null\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"853bd027f58c31dc\",\n    \"id\": \"95c6ad5da593ed4d\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"put /installedapps/_uuid_/init-update-flow\",\n    \"timestamp\": 1543549525083951,\n    \"duration\": 134554,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"/installedapps/86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX/init-update-flow\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"62ab8765707452d8\",\n    \"id\": \"853bd027f58c31dc\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post pages/install/initialize\",\n    \"timestamp\": 1543549525082801,\n    \"duration\": 470139,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"app.id\": \"\",\n      \"app.installed_app_id\": \"86ee7c3f-b7ed-4feb-b362-XXXXXXXXXXXX\",\n      \"app.location_id\": \"b8216712-d931-47f6-a81a-XXXXXXXXXXXX\",\n      \"http.path\": \"pages/install/initialize\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b894fa1b259fc56f\",\n    \"id\": \"085d8f6945f30277\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get js/smartthings-strongman.b41485894888f0dcba97.js\",\n    \"timestamp\": 1543549524806578,\n    \"duration\": 53,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.29.201\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"js/smartthings-strongman.b41485894888f0dcba97.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b894fa1b259fc56f\",\n    \"id\": \"62ab8765707452d8\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get js/locale/en.8e63722bec09254c68ca.js\",\n    \"timestamp\": 1543549524806961,\n    \"duration\": 55,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"js/locale/en.8e63722bec09254c68ca.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b894fa1b259fc56f\",\n    \"id\": \"e315a93a44cd7bb4\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get js/messenger.c4a907a946955bf434d8.js\",\n    \"timestamp\": 1543549524804316,\n    \"duration\": 89,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.85.246\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"js/messenger.c4a907a946955bf434d8.js\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"b894fa1b259fc56f\",\n    \"id\": \"c8a8f0df48a70b37\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get css/theme-smartthings.b4607ac84c1974bbd446.css\",\n    \"timestamp\": 1543549524738426,\n    \"duration\": 50,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.153.193\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"css/theme-smartthings.b4607ac84c1974bbd446.css\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"9d73c7b6cfb4ed18\",\n    \"id\": \"b894fa1b259fc56f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get \",\n    \"timestamp\": 1543549524707859,\n    \"duration\": 853,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"tags\": {\n      \"http.path\": \"\",\n      \"oauth.client_id\": \"8f4dbe6d-9066-4d0e-99d0-XXXXXXXXXXXX\",\n      \"user.username\": \"user_uuid:ed7d0ad1-4211-5e9c-2278-XXXXXXXXXXXX\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"9d73c7b6cfb4ed18\",\n    \"kind\": \"SERVER\",\n    \"timestamp\": 1543549524707831,\n    \"localEndpoint\": {\n      \"serviceName\": \"strongman\",\n      \"ipv4\": \"10.104.90.97\",\n      \"port\": 8150\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"9d73c7b6cfb4ed18\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"redirect\",\n    \"timestamp\": 1543549524602759,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.0.146.243\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.redirect\": \"https://strongman-regional.api.smartthings.com/?locationId=b8216712-d931-47f6-a81a-XXXXXXXXXXXX&clientOS=iOS&language=en&appType=ENDPOINTAPP&appType=ENDPOINTAPP&installedAppId=86ee7c3f-b7ed-4feb-b362-\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"21fd692195b4e958\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524596460,\n    \"duration\": 1111,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"d963f79f0f7111b5\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524595268,\n    \"duration\": 1131,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"66ecfa20c978ee63\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549524594717,\n    \"duration\": 524,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"05febd6cf96cfb45\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"bound-statement\",\n    \"timestamp\": 1543549524592816,\n    \"duration\": 1602,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.1.31\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user_by_uuid WHERE uuid=:uuid;\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.1.31\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"307afbc92108d64b\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524591640,\n    \"duration\": 1056,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.77.250\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1d\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.77.250\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"fb33ee8df9fd2147\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524590419,\n    \"duration\": 1124,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.6.253\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.6.253\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"0fe45bcc13b12d45\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524589314,\n    \"duration\": 1043,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.13.0\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1c\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.13.0\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"a97b767e8ad89b9f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /implicit-tokens/access\",\n    \"timestamp\": 1543549524588027,\n    \"duration\": 12215,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"54.83.153.25\",\n      \"port\": 20924\n    },\n    \"tags\": {\n      \"http.path\": \"/implicit-tokens/access\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"a97b767e8ad89b9f\",\n    \"id\": \"bed0c4dad0641bc0\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"client-select-by-id\",\n    \"timestamp\": 1543549524588631,\n    \"duration\": 646,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.149.217\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.139.111\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.139.111\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"a97b767e8ad89b9f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /implicit-tokens/access\",\n    \"timestamp\": 1543549524581578,\n    \"duration\": 20173,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.0.146.243\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/implicit-tokens/access\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"3b7023f607eb87d2\",\n    \"id\": \"af0bd91fa2c88c04\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"blacklist_get_by_id\",\n    \"timestamp\": 1543549524571876,\n    \"duration\": 751,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.188\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.140.58\",\n      \"port\": 9042\n    },\n    \"tags\": {\n      \"cassandra.consistency_level\": \"LOCAL_ONE\",\n      \"cassandra.data_center\": \"us-east\",\n      \"cassandra.keyspace\": \"auth\",\n      \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n      \"cassandra.rack\": \"1e\",\n      \"cassandra.state\": \"UP\",\n      \"cassandra.tried_hosts\": \"10.104.140.58\",\n      \"cassandra.version\": \"unknown\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"3b7023f607eb87d2\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549524571024,\n    \"duration\": 1938,\n    \"localEndpoint\": {\n      \"serviceName\": \"auth\",\n      \"ipv4\": \"10.104.144.188\",\n      \"port\": 8180\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"54.83.153.25\",\n      \"port\": 22974\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"parentId\": \"14b60fd9ae504820\",\n    \"id\": \"3b7023f607eb87d2\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post /oauth/check_token\",\n    \"timestamp\": 1543549524566268,\n    \"duration\": 8164,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.0.146.243\",\n      \"port\": 8080\n    },\n    \"tags\": {\n      \"http.path\": \"/oauth/check_token\"\n    }\n  },\n  {\n    \"traceId\": \"14b60fd9ae504820\",\n    \"id\": \"14b60fd9ae504820\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get /login/tokenauth\",\n    \"timestamp\": 1543549524565942,\n    \"duration\": 36713,\n    \"localEndpoint\": {\n      \"serviceName\": \"coreSrv\",\n      \"ipv4\": \"10.0.146.243\",\n      \"port\": 8080\n    },\n    \"remoteEndpoint\": {\n      \"ipv4\": \"10.0.65.109\",\n      \"port\": 27512\n    },\n    \"tags\": {\n      \"http.path\": \"/login/tokenAuth\",\n      \"http.status_code\": \"302\",\n      \"user.email\": \"fake@example.com\",\n      \"user.said\": \"7777\",\n      \"user.uuid\": \"342-4211-5e9c-2278-124324234\"\n    }\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/testdata/smartthings-oauth-authorization.json",
    "content": "[\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"54456c991996a6c7\",\n      \"id\": \"c8a2bcb3011b9fcd\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-store-without-refresh\",\n      \"timestamp\": 1543334727215550,\n      \"duration\": 3816,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.89\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tBEGIN BATCH\\n\\t\\t\\tINSERT INTO auth.oauth_access_token (oauth_token, access_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_authentication (authentication_id, access_token, authentication, last_modified) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_client_with_token (client_id, client_mod, user_id, oauth_token, access_token, authentication, last_modified) VALUES (?, ?, ?, ?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_user (user_id_hash, access_token, authentication, device_name) VALUES (?, ?, ?, ?);\\n\\t\\t\\tINSERT INTO auth.oauth_access_token_by_user_and_client (user_id_hash, client_id, access_token, authentication, device_name) VALUES (?, ?, ?, ?, ?);\\n\\t\\tAPPLY BATCH;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"54456c991996a6c7\",\n      \"id\": \"412ef7480c7eadb7\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727212580,\n      \"duration\": 2808,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.89\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"54456c991996a6c7\",\n      \"id\": \"0ceca358a532f441\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727211445,\n      \"duration\": 1075,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.89\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.254\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"54456c991996a6c7\",\n      \"id\": \"c5ec98152a9f6ac2\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-authentication_id\",\n      \"timestamp\": 1543334727210809,\n      \"duration\": 624,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.89\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token_by_authentication WHERE authentication_id = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.141\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"54456c991996a6c7\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /tokens/access\",\n      \"timestamp\": 1543334727210025,\n      \"duration\": 9644,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.89\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 36656\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"54456c991996a6c7\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /tokens/access\",\n      \"timestamp\": 1543334727207536,\n      \"duration\": 13171,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"0df5fb685859651f\",\n      \"id\": \"084fcc474e57b661\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334727132433,\n      \"duration\": 72725,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.105\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.31\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"QUORUM\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"DELETE FROM auth.oauth_code WHERE code = ?;\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.31\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"0df5fb685859651f\",\n      \"id\": \"776d34b22754ac1e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"authcode-select-by-code\",\n      \"timestamp\": 1543334727130888,\n      \"duration\": 1347,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.105\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.175\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.oauth_code WHERE code = ?;\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.175\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"0df5fb685859651f\",\n      \"kind\": \"SERVER\",\n      \"name\": \"delete /authorization/code/_code_\",\n      \"timestamp\": 1543334727130025,\n      \"duration\": 75546,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.105\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 51984\n      },\n      \"tags\": {\n        \"http.path\": \"/authorization/code/_code_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"0df5fb685859651f\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"delete /authorization/code/_code_\",\n      \"timestamp\": 1543334727126847,\n      \"duration\": 79763,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/authorization/code/_code_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"772e2305de77c79f\",\n      \"id\": \"bdcdfb05e48859a5\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727123808,\n      \"duration\": 1045,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.251\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.217\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.217\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"772e2305de77c79f\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727123029,\n      \"duration\": 2131,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.251\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 8634\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"772e2305de77c79f\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727120302,\n      \"duration\": 5873,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"001c695f334f9420\",\n      \"id\": \"dd0ab59f3e36fbd8\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727117728,\n      \"duration\": 1084,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"001c695f334f9420\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727114029,\n      \"duration\": 5281,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 10226\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"001c695f334f9420\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727109066,\n      \"duration\": 10376,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"f86833c3ff266b46\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727106025,\n      \"duration\": 1629,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.104.29.139\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 33948\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f86833c3ff266b46\",\n      \"id\": \"b262965706ef4d8a\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727106652,\n      \"duration\": 686,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.104.29.139\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.254\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"f86833c3ff266b46\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727103992,\n      \"duration\": 4589,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"817bed843984f73c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727101027,\n      \"duration\": 1601,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.191\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 27934\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"817bed843984f73c\",\n      \"id\": \"4cf370cb3c9eb058\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334727101708,\n      \"duration\": 615,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.191\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.217\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.217\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b69b3db88ffa3212\",\n      \"id\": \"817bed843984f73c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334727098413,\n      \"duration\": 5011,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c2fac1d86e52d441\",\n      \"id\": \"b69b3db88ffa3212\",\n      \"kind\": \"SERVER\",\n      \"name\": \"process-token-request\",\n      \"timestamp\": 1543334727098194,\n      \"duration\": 123351,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"oauth.param.client_id\": \"zipkin-sample-id\",\n        \"oauth.param.code\": \"_code_\",\n        \"oauth.param.grant_type\": \"authorization_code\",\n        \"oauth.param.principal\": \"zipkin-sample-id\",\n        \"oauth.param.redirect_uri\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"610825951ae86752\",\n      \"id\": \"c2fac1d86e52d441\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334727098066,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"610825951ae86752\",\n      \"id\": \"3463f822a0e9cc02\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334726297212,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ?code=n8ce82b2e9ed820bac2fac1d86e52d441610825951ae867520000000000000006__code_&state=A2SAAEAEKfMsszL2JvJRpVHDystCXYB4BOiYRGmArdNIv9XYhEFP22dZp4\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"610825951ae86752\",\n      \"id\": \"c2fac1d86e52d441\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"authorize\",\n      \"timestamp\": 1543334726296197,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"61d0055f29150e03\",\n      \"id\": \"e2e348dcb3334c38\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"authcode-store\",\n      \"timestamp\": 1543334725675842,\n      \"duration\": 66972,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.33\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"QUORUM\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"INSERT INTO auth.oauth_code (code, authentication) VALUES (?, ?) USING TTL ?;\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.33\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"610825951ae86752\",\n      \"id\": \"61d0055f29150e03\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /authorization/code\",\n      \"timestamp\": 1543334725674025,\n      \"duration\": 621748,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.160\",\n        \"port\": 52322\n      },\n      \"tags\": {\n        \"http.path\": \"/authorization/code\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"610825951ae86752\",\n      \"id\": \"61d0055f29150e03\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /authorization/code\",\n      \"timestamp\": 1543334725670595,\n      \"duration\": 624147,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/authorization/code\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"610825951ae86752\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /oauth/authorize\",\n      \"timestamp\": 1543334725668597,\n      \"duration\": 628505,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/authorize\",\n        \"http.status_code\": \"302\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"9d2d35b746db84f3\",\n      \"id\": \"d645461738ed0e17\",\n      \"name\": \"process.archiver\",\n      \"timestamp\": 1543334725564862,\n      \"duration\": 2695,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.187\",\n        \"port\": 8197\n      },\n      \"tags\": {\n        \"lc\": \"EventProcessor\",\n        \"pusher.eventType\": \"LEGACY_EVENT\",\n        \"pusher.timeInKafka\": \"32\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d645461738ed0e17\",\n      \"id\": \"5f35e80a5a50fdca\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /events\",\n      \"timestamp\": 1543334725564984,\n      \"duration\": 2513,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.187\",\n        \"port\": 8197\n      },\n      \"annotations\": [\n        {\n          \"timestamp\": 1543334725567000,\n          \"value\": \"Body Part Received\"\n        },\n        {\n          \"timestamp\": 1543334725567000,\n          \"value\": \"Headers Received\"\n        },\n        {\n          \"timestamp\": 1543334725567000,\n          \"value\": \"Status Received\"\n        }\n      ],\n      \"tags\": {\n        \"http.url\": \"http://archiver.st.internal/events\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"SERVER\",\n      \"name\": \"receive iot-events360\",\n      \"timestamp\": 1543334725564455,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.187\",\n        \"port\": 8197\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"bouncer\"\n      },\n      \"tags\": {\n        \"kafka.partition\": \"56\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"SERVER\",\n      \"name\": \"receive iot-events360\",\n      \"timestamp\": 1543334725560112,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.187\",\n        \"port\": 8197\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"bouncer\"\n      },\n      \"tags\": {\n        \"kafka.partition\": \"56\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"SERVER\",\n      \"name\": \"receive iot-events360\",\n      \"timestamp\": 1543334725560292,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.187\",\n        \"port\": 8197\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"bouncer\"\n      },\n      \"tags\": {\n        \"kafka.partition\": \"56\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"de67d452708a545a\",\n      \"id\": \"b4c5ce09acaf2e0e\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /deliver/\",\n      \"timestamp\": 1543334725554014,\n      \"duration\": 611,\n      \"localEndpoint\": {\n        \"serviceName\": \"paperboy\",\n        \"ipv4\": \"10.0.0.177\",\n        \"port\": 8195\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"10.0.0.79\"\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/deliver\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"de67d452708a545a\",\n      \"id\": \"b4c5ce09acaf2e0e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /deliver\",\n      \"timestamp\": 1543334725553873,\n      \"duration\": 1475,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"annotations\": [\n        {\n          \"timestamp\": 1543334725555000,\n          \"value\": \"Body Part Received\"\n        },\n        {\n          \"timestamp\": 1543334725555000,\n          \"value\": \"Headers Received\"\n        },\n        {\n          \"timestamp\": 1543334725555000,\n          \"value\": \"Status Received\"\n        }\n      ],\n      \"tags\": {\n        \"http.url\": \"http://paperboy.st.internal/deliver\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"9d2d35b746db84f3\",\n      \"id\": \"de67d452708a545a\",\n      \"name\": \"process.paperboy\",\n      \"timestamp\": 1543334725553777,\n      \"duration\": 1612,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"tags\": {\n        \"lc\": \"EventProcessor\",\n        \"pusher.eventType\": \"LEGACY_EVENT\",\n        \"pusher.timeInKafka\": \"22\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"SERVER\",\n      \"name\": \"receive iot-events360\",\n      \"timestamp\": 1543334725553308,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"bouncer\"\n      },\n      \"tags\": {\n        \"kafka.partition\": \"56\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"975e74022b72e1cd\",\n      \"id\": \"1dfd8f3332f7caca\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /deliver/\",\n      \"timestamp\": 1543334725551008,\n      \"duration\": 259,\n      \"localEndpoint\": {\n        \"serviceName\": \"dove\",\n        \"ipv4\": \"10.0.0.47\",\n        \"port\": 8215\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"10.0.0.79\"\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/deliver\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"975e74022b72e1cd\",\n      \"id\": \"1dfd8f3332f7caca\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /deliver\",\n      \"timestamp\": 1543334725550569,\n      \"duration\": 1704,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"annotations\": [\n        {\n          \"timestamp\": 1543334725552000,\n          \"value\": \"Body Part Received\"\n        },\n        {\n          \"timestamp\": 1543334725552000,\n          \"value\": \"Headers Received\"\n        },\n        {\n          \"timestamp\": 1543334725552000,\n          \"value\": \"Status Received\"\n        }\n      ],\n      \"tags\": {\n        \"http.url\": \"http://dove.st.internal/deliver\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"9d2d35b746db84f3\",\n      \"id\": \"975e74022b72e1cd\",\n      \"name\": \"process.dove\",\n      \"timestamp\": 1543334725550479,\n      \"duration\": 1843,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"tags\": {\n        \"lc\": \"EventProcessor\",\n        \"pusher.eventType\": \"LEGACY_EVENT\",\n        \"pusher.timeInKafka\": \"18\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"SERVER\",\n      \"name\": \"receive iot-events360\",\n      \"timestamp\": 1543334725549997,\n      \"localEndpoint\": {\n        \"serviceName\": \"pusher\",\n        \"ipv4\": \"10.0.0.79\",\n        \"port\": 8197\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"bouncer\"\n      },\n      \"tags\": {\n        \"kafka.partition\": \"56\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"19b91ab9a7d47f3d\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post\",\n      \"timestamp\": 1543334725532359,\n      \"duration\": 938,\n      \"localEndpoint\": {\n        \"serviceName\": \"bouncer\",\n        \"ipv4\": \"10.0.0.173\",\n        \"port\": 8194\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"10.0.0.151\"\n      },\n      \"tags\": {\n        \"http.path\": \"events\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"19b91ab9a7d47f3d\",\n      \"id\": \"9d2d35b746db84f3\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"send iot-events360\",\n      \"timestamp\": 1543334725532596,\n      \"localEndpoint\": {\n        \"serviceName\": \"bouncer\",\n        \"ipv4\": \"10.0.0.173\",\n        \"port\": 8194\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"pusher\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"19b91ab9a7d47f3d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /events\",\n      \"timestamp\": 1543334725531223,\n      \"duration\": 1668,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/events\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"687365a88b8ba954\",\n      \"id\": \"0364222cb0e20fdc\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334725360796,\n      \"duration\": 919,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.95\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"687365a88b8ba954\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334725360029,\n      \"duration\": 2014,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.95\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.04\",\n        \"port\": 41634\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"687365a88b8ba954\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334725351636,\n      \"duration\": 15871,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d4a98698eb1d7b2d\",\n      \"id\": \"1f37cd038b59c75c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334725343718,\n      \"duration\": 563,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.175\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.175\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d4a98698eb1d7b2d\",\n      \"id\": \"eca1f2f754a803e3\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334725342526,\n      \"duration\": 1145,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d4a98698eb1d7b2d\",\n      \"id\": \"68bba83007363edf\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334725341400,\n      \"duration\": 1093,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"d4a98698eb1d7b2d\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334725340024,\n      \"duration\": 4530,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.04\",\n        \"port\": 33252\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d4a98698eb1d7b2d\",\n      \"id\": \"01b2d121b174561d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334725340333,\n      \"duration\": 1034,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"875e4cd34da71421\",\n      \"id\": \"d4a98698eb1d7b2d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334725329264,\n      \"duration\": 20837,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"fde62e1dd7a28f55\",\n      \"id\": \"875e4cd34da71421\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /api/proxied/locations/_uuid_/smart\",\n      \"timestamp\": 1543334725328957,\n      \"duration\": 241790,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 63036\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"fde62e1dd7a28f55\",\n      \"id\": \"875e4cd34da71421\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /api/proxied/locations/_uuid_/smartapps\",\n      \"timestamp\": 1543334725322726,\n      \"duration\": 252570,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"fde62e1dd7a28f55\",\n      \"id\": \"37cfec055f61ca1e\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /locations/user/:username\",\n      \"timestamp\": 1543334725319014,\n      \"duration\": 2083,\n      \"localEndpoint\": {\n        \"serviceName\": \"account\",\n        \"ipv4\": \"10.0.0.183\",\n        \"port\": 8181\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"fde62e1dd7a28f55\",\n      \"id\": \"37cfec055f61ca1e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /locations/user/user_uuid:_uuid_\",\n      \"timestamp\": 1543334725318386,\n      \"duration\": 3725,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"fde62e1dd7a28f55\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /api/locations/_uuid_/smartapps\",\n      \"timestamp\": 1543334725313097,\n      \"duration\": 264378,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"16eecb59ef681082\",\n      \"id\": \"6d34027436856f8f\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722685030,\n      \"duration\": 1730,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.166\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.05\",\n        \"port\": 42588\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6d34027436856f8f\",\n      \"id\": \"41662d84c58e9462\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722685367,\n      \"duration\": 1083,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.166\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"16eecb59ef681082\",\n      \"id\": \"6d34027436856f8f\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722675146,\n      \"duration\": 16822,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.182\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"38827242198dba3d\",\n      \"id\": \"8e83e655dd49fd63\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334722665856,\n      \"duration\": 1193,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.213\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.95\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.95\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"38827242198dba3d\",\n      \"id\": \"b0720badb933ac4a\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722664791,\n      \"duration\": 1000,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.213\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"38827242198dba3d\",\n      \"id\": \"800d65a16ed6d52c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722663533,\n      \"duration\": 1207,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.213\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"38827242198dba3d\",\n      \"id\": \"ad0f51c51fe9d5f7\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722662419,\n      \"duration\": 1086,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.213\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"16eecb59ef681082\",\n      \"id\": \"38827242198dba3d\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722662024,\n      \"duration\": 5455,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.213\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.05\",\n        \"port\": 49124\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7f1930a97b67b431\",\n      \"id\": \"16eecb59ef681082\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/proxied/locations/_uuid_/smarta\",\n      \"timestamp\": 1543334722651264,\n      \"duration\": 78267,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.182\",\n        \"port\": 8080\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.25\",\n        \"port\": 3068\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps/_app_/versions/082bb0c8-0f22-4dde-b0ce-7f2b6c4fce07/devices\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"16eecb59ef681082\",\n      \"id\": \"38827242198dba3d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722651546,\n      \"duration\": 22099,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.182\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7f1930a97b67b431\",\n      \"id\": \"16eecb59ef681082\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /api/proxied/locations/_uuid_/smartapps/_uuid_/versions/_uuid_/devices\",\n      \"timestamp\": 1543334722644843,\n      \"duration\": 90175,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.40\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps/_app_/versions/082bb0c8-0f22-4dde-b0ce-7f2b6c4fce07/devices\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7f1930a97b67b431\",\n      \"id\": \"6bfe0f47e2fe0bc0\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /locations/user/:username\",\n      \"timestamp\": 1543334722642012,\n      \"duration\": 2031,\n      \"localEndpoint\": {\n        \"serviceName\": \"account\",\n        \"ipv4\": \"10.0.0.13\",\n        \"port\": 8181\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.25\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7f1930a97b67b431\",\n      \"id\": \"6bfe0f47e2fe0bc0\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /locations/user/user_uuid:_uuid_\",\n      \"timestamp\": 1543334722640542,\n      \"duration\": 3681,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.40\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"7f1930a97b67b431\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/locations/_uuid_/smartapps/_uui\",\n      \"timestamp\": 1543334722635440,\n      \"duration\": 101152,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.40\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps/_app_/versions/082bb0c8-0f22-4dde-b0ce-7f2b6c4fce07/devices\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"312c5920ce5de8a9\",\n      \"id\": \"8c2689a5613d39ea\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722484027,\n      \"duration\": 1954,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.17\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.9\",\n        \"port\": 17140\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"8c2689a5613d39ea\",\n      \"id\": \"b28c979d5fb63133\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722484700,\n      \"duration\": 861,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.17\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"312c5920ce5de8a9\",\n      \"id\": \"8c2689a5613d39ea\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722476150,\n      \"duration\": 15308,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.9\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1a26239b7113287e\",\n      \"id\": \"11ee4af5fa6f5efa\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334722343703,\n      \"duration\": 976,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.111\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.111\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1a26239b7113287e\",\n      \"id\": \"09ae44d0fa0da9eb\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722342392,\n      \"duration\": 550,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1a26239b7113287e\",\n      \"id\": \"4e32f8453bc5c879\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722342993,\n      \"duration\": 637,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"312c5920ce5de8a9\",\n      \"id\": \"1a26239b7113287e\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722341024,\n      \"duration\": 4035,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.9\",\n        \"port\": 33768\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1a26239b7113287e\",\n      \"id\": \"f59b8da1680bb277\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722341434,\n      \"duration\": 900,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.203\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"3c27c9d57b331bb1\",\n      \"id\": \"312c5920ce5de8a9\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/proxied/smartapps/_uuid_/versio\",\n      \"timestamp\": 1543334722331351,\n      \"duration\": 178831,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.9\",\n        \"port\": 8080\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 9400\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/smartapps/_app_/versions/_installapp_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"312c5920ce5de8a9\",\n      \"id\": \"1a26239b7113287e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722331588,\n      \"duration\": 142802,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.9\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"3c27c9d57b331bb1\",\n      \"id\": \"312c5920ce5de8a9\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /api/proxied/smartapps/_uuid_/versions/_uuid_/installations\",\n      \"timestamp\": 1543334722323843,\n      \"duration\": 190598,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/smartapps/_app_/versions/_installapp_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"3c27c9d57b331bb1\",\n      \"id\": \"78cf274cc51240ea\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /locations/user/:username\",\n      \"timestamp\": 1543334722321006,\n      \"duration\": 1293,\n      \"localEndpoint\": {\n        \"serviceName\": \"account\",\n        \"ipv4\": \"10.0.0.04\",\n        \"port\": 8181\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"3c27c9d57b331bb1\",\n      \"id\": \"78cf274cc51240ea\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /locations/user/user_uuid:_uuid_\",\n      \"timestamp\": 1543334722320087,\n      \"duration\": 2830,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"3c27c9d57b331bb1\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/smartapps/_uuid_/versions/_uuid\",\n      \"timestamp\": 1543334722315825,\n      \"duration\": 201412,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/smartapps/_app_/versions/_installapp_\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"552eaca6c491fcb4\",\n      \"id\": \"7d279f33d9a1a55e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722183961,\n      \"duration\": 1201,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.66\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7224032272dbf81c\",\n      \"id\": \"552eaca6c491fcb4\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722183038,\n      \"duration\": 2408,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.66\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.04\",\n        \"port\": 45186\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7224032272dbf81c\",\n      \"id\": \"552eaca6c491fcb4\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /tokens/access/_uuid_\",\n      \"timestamp\": 1543334722174444,\n      \"duration\": 16827,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.225\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/tokens/access/_token_\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"5fdbafe9b43a2185\",\n      \"id\": \"c22c3194d0cc8ea3\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334722166380,\n      \"duration\": 441,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.122\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.122\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"5fdbafe9b43a2185\",\n      \"id\": \"d3f93489645ac562\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722165460,\n      \"duration\": 864,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"5fdbafe9b43a2185\",\n      \"id\": \"126d715053b9865c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722164517,\n      \"duration\": 900,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7224032272dbf81c\",\n      \"id\": \"5fdbafe9b43a2185\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722163025,\n      \"duration\": 4147,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.04\",\n        \"port\": 40364\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"5fdbafe9b43a2185\",\n      \"id\": \"5bfd72a09dad9d02\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"access_token-select-by-oauth_token\",\n      \"timestamp\": 1543334722163611,\n      \"duration\": 867,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT * FROM auth.oauth_access_token WHERE oauth_token = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f083061491cddbcb\",\n      \"id\": \"7224032272dbf81c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/proxied/locations/_uuid_/smarta\",\n      \"timestamp\": 1543334722152268,\n      \"duration\": 72591,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.225\",\n        \"port\": 8080\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 10954\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps/client/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7224032272dbf81c\",\n      \"id\": \"5fdbafe9b43a2185\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post /oauth/check_token\",\n      \"timestamp\": 1543334722152465,\n      \"duration\": 20863,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.225\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/check_token\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f083061491cddbcb\",\n      \"id\": \"7224032272dbf81c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /api/proxied/locations/_uuid_/smartapps/client/_uuid_\",\n      \"timestamp\": 1543334722145571,\n      \"duration\": 83715,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/proxied/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapps/client/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f083061491cddbcb\",\n      \"id\": \"5f4625942bfac6f1\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /locations/user/:username\",\n      \"timestamp\": 1543334722143022,\n      \"duration\": 1798,\n      \"localEndpoint\": {\n        \"serviceName\": \"account\",\n        \"ipv4\": \"10.0.0.180\",\n        \"port\": 8181\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f083061491cddbcb\",\n      \"id\": \"5f4625942bfac6f1\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /locations/user/user_uuid:_uuid_\",\n      \"timestamp\": 1543334722141626,\n      \"duration\": 3103,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"f083061491cddbcb\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /api/locations/_uuid_/smartapp/clien\",\n      \"timestamp\": 1543334722135730,\n      \"duration\": 95818,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.151\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/api/locations/7b37ecd0-0a7a-44f6-aa11-c099fd7a0f3b/smartapp/client/zipkin-sample-id\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"165914062a124bfb\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/components/common/spinner-sm\",\n      \"timestamp\": 1543334722132656,\n      \"duration\": 442,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/components/common/spinner-small.gif\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"a34b5be52d63a84e\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/components/common/smartthing\",\n      \"timestamp\": 1543334697726557,\n      \"duration\": 507,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/components/common/example-logo.png\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"cecf806288f6871c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/apps/oauth/oauth-app-0ee844e\",\n      \"timestamp\": 1543334697648959,\n      \"duration\": 600,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/apps/oauth/oauth-app-0ee844eeee4e731ef3a802c9750188a4.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"e76bd9f07b45a524\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/libs/bootstrap/bootstrap-e3b\",\n      \"timestamp\": 1543334697638982,\n      \"duration\": 633,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/libs/bootstrap/bootstrap-e3bccdc29cf6e1d0f3e7bc54ccf086ef.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"45e6e257ca166a24\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/base/bootstrap-framework-ca0\",\n      \"timestamp\": 1543334697572469,\n      \"duration\": 497,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/base/bootstrap-framework-ca068de267e66f972264227b95ad8cb1.css\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"7b083fa5d42084f7\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/libs/underscore/underscore-6\",\n      \"timestamp\": 1543334697567311,\n      \"duration\": 499,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/libs/underscore/underscore-6e1f1c149dc88c708f9bfce4a061187f.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"90e92bd934b0ac2f\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/base/common-77e2fb036db019b4\",\n      \"timestamp\": 1543334697567900,\n      \"duration\": 294,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/base/common-77e2fb036db019b4c759f2bb145e94d3.css\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"e874910662f6b5aa\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /assets/apps/main/main-app-5811442e0\",\n      \"timestamp\": 1543334697403527,\n      \"duration\": 637,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/assets/apps/main/main-app-5811442e0bc96e2d7374677668e7edc4.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c101766e11ee2bb4\",\n      \"id\": \"b46de0f6c2ec0139\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /locations/user/:username\",\n      \"timestamp\": 1543334697224006,\n      \"duration\": 1409,\n      \"localEndpoint\": {\n        \"serviceName\": \"account\",\n        \"ipv4\": \"10.0.0.108\",\n        \"port\": 8181\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.160\"\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c101766e11ee2bb4\",\n      \"id\": \"b46de0f6c2ec0139\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /locations/user/user_uuid:_uuid_\",\n      \"timestamp\": 1543334697222145,\n      \"duration\": 3581,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/locations/user/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c101766e11ee2bb4\",\n      \"id\": \"a6a7402ffab1c5aa\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697219026,\n      \"duration\": 2045,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.163\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.160\",\n        \"port\": 49322\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a7402ffab1c5aa\",\n      \"id\": \"327e218e9065a740\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334697219685,\n      \"duration\": 1085,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.163\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.217\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.217\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c101766e11ee2bb4\",\n      \"id\": \"a6a7402ffab1c5aa\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697216436,\n      \"duration\": 5176,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a8de54dbcc867f1d\",\n      \"id\": \"c101766e11ee2bb4\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /oauth/confirm_access\",\n      \"timestamp\": 1543334697212190,\n      \"duration\": 28438,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/confirm_access\",\n        \"oauth.param.client_id\": \"zipkin-sample-id\",\n        \"oauth.param.redirect_uri\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ\",\n        \"oauth.param.response_type\": \"code\",\n        \"oauth.param.scope\": \"app\",\n        \"oauth.param.state\": \"A2SAAEAEKfMsszL2JvJRpVHDystCXYB4BOiYRGmArdNIv9XYhEFP22dZp46YKR-UkBtNBQKNsLevOuWMb2pXKcm05lQYOLCCjsBY4VHETl7cZuDgcNBU55D1NEQu0MEeSN3yJiSL3Pqc_X0OUwARHmd4LtgwZ2cdIEsDNbWcq3jCnTATXR7ocK-h9XnxJchDx69A1jhbo7FdXE85NLliElT-iHzAO1SHkhBfNuzSZgKUGJQLLmcA_FxJSP_WxebcG1IXI2u4kf0UzX_jqCnDOCdtzhCnV2cba_CI_kLAWvZE-IBrXLT9K7QzXYWqcuKXNj96Mc5gABOF8_2hAkWImrHEau49xeMzzx6RW6wKLIk-jislsdEgo3qXjKq7nTjtwyotYYIjzVB66al2vNIJOog-LMzXtEGpfgl4LlVaRLrBgPY54ECrpd3VSugVpKN5sNNWtZje5MDSl0lUZzmxp4-sYjaEbizj0YLeBzh4pfHRY8rSESRlDHFbjJn3v1igOsRsOaZ0ch5sVthvKT9ThQh0hROkzIgaRyI6oHew2ax_IIsMOMSRDt3XpxP_tvEnp_YE9Kl114HCq-_WwpjrU9n0iCkRQCx2yIpPCqOP7zaO0POJY-iWhp5B-GZObyOTgvTqn4Wn4nJc2KPvmd3DufSK-K0DhDXoQ\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"a8de54dbcc867f1d\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334697211897,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"a8de54dbcc867f1d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334697071886,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://graph.api.example.com/oauth/confirm_access?response_type=code&scope=app&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fskill%2Flink%2FM25XHJDAAO8RHQ&state=A2SAAEAEKfMsszL2JvJRpVHDy\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"c24ca597b1819c50\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697068026,\n      \"duration\": 1947,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.142\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 50412\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c24ca597b1819c50\",\n      \"id\": \"d2c7775b59e59c7d\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334697068864,\n      \"duration\": 686,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.142\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.98\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.98\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"c24ca597b1819c50\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697065264,\n      \"duration\": 5400,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"e0b3f0c9b97afbed\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697062025,\n      \"duration\": 2355,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.66\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 12710\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"e0b3f0c9b97afbed\",\n      \"id\": \"5e8f131a443eb3f0\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334697062898,\n      \"duration\": 1095,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.66\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.3\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.3\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"e0b3f0c9b97afbed\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697059732,\n      \"duration\": 5022,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"7ce382395898c478\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697056025,\n      \"duration\": 2152,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.61\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 51782\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7ce382395898c478\",\n      \"id\": \"67a33b954d413ee5\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334697056738,\n      \"duration\": 1152,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.61\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.175\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.175\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"7ce382395898c478\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /clients/_uuid_\",\n      \"timestamp\": 1543334697053649,\n      \"duration\": 5604,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/clients/zipkin-sample-id\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"7de851d5112e61d4\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /admin/users/user_uuid%3a_uuid_\",\n      \"timestamp\": 1543334696955027,\n      \"duration\": 1581,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"54.0.0.101\",\n        \"port\": 7008\n      },\n      \"tags\": {\n        \"http.path\": \"/admin/users/user_uuid%3A567xyz\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"7de851d5112e61d4\",\n      \"id\": \"01904bc3a7dcfaef\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334696955699,\n      \"duration\": 612,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.250\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user WHERE username=:username;\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.250\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bfc02051be208bd9\",\n      \"id\": \"7de851d5112e61d4\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /admin/users/user_uuid:_uuid_\",\n      \"timestamp\": 1543334696952312,\n      \"duration\": 5016,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/admin/users/user_uuid:567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"328fac961bc6aaee\",\n      \"id\": \"bfc02051be208bd9\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /oauth/authorize\",\n      \"timestamp\": 1543334696950745,\n      \"duration\": 121013,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.255\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/authorize\",\n        \"http.status_code\": \"302\",\n        \"oauth.param.client_id\": \"zipkin-sample-id\",\n        \"oauth.param.redirect_uri\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ\",\n        \"oauth.param.response_type\": \"code\",\n        \"oauth.param.scope\": \"app\",\n        \"oauth.param.state\": \"A2SAAEAEKfMsszL2JvJRpVHDystCXYB4BOiYRGmArdNIv9XYhEFP22dZp46YKR-UkBtNBQKNsLevOuWMb2pXKcm05lQYOLCCjsBY4VHETl7cZuDgcNBU55D1NEQu0MEeSN3yJiSL3Pqc_X0OUwARHmd4LtgwZ2cdIEsDNbWcq3jCnTATXR7ocK-h9XnxJchDx69A1jhbo7FdXE85NLliElT-iHzAO1SHkhBfNuzSZgKUGJQLLmcA_FxJSP_WxebcG1IXI2u4kf0UzX_jqCnDOCdtzhCnV2cba_CI_kLAWvZE-IBrXLT9K7QzXYWqcuKXNj96Mc5gABOF8_2hAkWImrHEau49xeMzzx6RW6wKLIk-jislsdEgo3qXjKq7nTjtwyotYYIjzVB66al2vNIJOog-LMzXtEGpfgl4LlVaRLrBgPY54ECrpd3VSugVpKN5sNNWtZje5MDSl0lUZzmxp4-sYjaEbizj0YLeBzh4pfHRY8rSESRlDHFbjJn3v1igOsRsOaZ0ch5sVthvKT9ThQh0hROkzIgaRyI6oHew2ax_IIsMOMSRDt3XpxP_tvEnp_YE9Kl114HCq-_WwpjrU9n0iCkRQCx2yIpPCqOP7zaO0POJY-iWhp5B-GZObyOTgvTqn4Wn4nJc2KPvmd3DufSK-K0DhDXoQ\",\n        \"user.email\": \"joe@example.com\",\n        \"user.said\": \"abc123\",\n        \"user.uuid\": \"567xyz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"b3b0a352cb50d500\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696713123,\n      \"duration\": 794,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"30721326942c94cd\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696712273,\n      \"duration\": 797,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"b5858f7f83564a9e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"blacklist_get_by_id\",\n      \"timestamp\": 1543334696710752,\n      \"duration\": 1494,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.42\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"LOCAL_ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"\\n\\t\\tSELECT id FROM auth.blacklist WHERE kind = ? AND id = ?;\\n\\t\\t\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.42\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"822e7a00e82698de\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334696709353,\n      \"duration\": 1106,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.141\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user_by_uuid WHERE uuid=:uuid;\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.141\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"c1700e99c9f82485\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696708596,\n      \"duration\": 659,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.250\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.250\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"dbee5bb90d3fd254\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696707586,\n      \"duration\": 963,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.24\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.24\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"47aa779eb9ae60d0\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696706786,\n      \"duration\": 741,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.253\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.253\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"c0ed0e95e54ac4f5\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"client-select-by-id\",\n      \"timestamp\": 1543334696706008,\n      \"duration\": 720,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.0\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT * FROM auth.client WHERE id = ?\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.0\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"a17b4c85e1561fe0\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"batch-statement\",\n      \"timestamp\": 1543334696705468,\n      \"duration\": 501,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.117\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.117\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"ad28ef23783326d7\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"batch-statement\",\n      \"timestamp\": 1543334696704231,\n      \"duration\": 981,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.112\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.112\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"48b40ff5a17e280b\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"batch-statement\",\n      \"timestamp\": 1543334696700996,\n      \"duration\": 3179,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.58\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.rack\": \"1e\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.58\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"f76df11b410bd44c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334696699957,\n      \"duration\": 953,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.122\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user WHERE username=:username;\",\n        \"cassandra.rack\": \"1c\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.122\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"45c9f5f276a62716\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334696699156,\n      \"duration\": 649,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.254\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user_by_said WHERE samsung_account_id=:samsung_account_id;\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.254\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"a6a297b5d665be1e\",\n      \"id\": \"15fa60faead85b0e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get /v2/profile/user/getuserbyssotoken\",\n      \"timestamp\": 1543334696410745,\n      \"duration\": 288100,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"tags\": {\n        \"http.path\": \"/v2/profile/user/getUserBySsoToken\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"328fac961bc6aaee\",\n      \"id\": \"a6a297b5d665be1e\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /web/authenticate\",\n      \"timestamp\": 1543334696409046,\n      \"duration\": 306903,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.22\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.05\",\n        \"port\": 1512\n      },\n      \"tags\": {\n        \"http.path\": \"/web/authenticate\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"328fac961bc6aaee\",\n      \"id\": \"a6a297b5d665be1e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post\",\n      \"timestamp\": 1543334696358772,\n      \"duration\": 365294,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"/web/authenticate\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"328fac961bc6aaee\",\n      \"id\": \"2f3468f2385971cc\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334695894428,\n      \"duration\": 464282,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"/v2/profile/user/getUserBySsoToken\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"b8d0f0960c488930\",\n      \"id\": \"328fac961bc6aaee\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post\",\n      \"timestamp\": 1543334695822010,\n      \"duration\": 902201,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"sa/registURL\",\n        \"http.status_code\": \"302\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"fda7e30724d087a6\",\n      \"id\": \"b8d0f0960c488930\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334671909008,\n      \"duration\": 304,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.32\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"login\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f64efde205fae151\",\n      \"id\": \"e4ca41b44ea5514e\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334671805004,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"e4ca41b44ea5514e\",\n      \"id\": \"fda7e30724d087a6\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334671805005,\n      \"duration\": 299,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"\",\n        \"http.status_code\": \"307\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"f64efde205fae151\",\n      \"id\": \"e4ca41b44ea5514e\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334671433445,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://account.example.com/?redirect=https%3A%2F%2Fgraph.api.example.com%2Foauth%2Fauthorize%3Fclient_id%3Dzipkin-sample-id%26response_type%3Dcode%26state%3DA2SAAEAEKfMssz\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1af69980dad7deac\",\n      \"id\": \"8ca0d490c17c7d7c\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334671430441,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"8ca0d490c17c7d7c\",\n      \"id\": \"f64efde205fae151\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /login/auth\",\n      \"timestamp\": 1543334671430620,\n      \"duration\": 2710,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/login/auth\",\n        \"http.status_code\": \"302\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"1af69980dad7deac\",\n      \"id\": \"8ca0d490c17c7d7c\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334671334219,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://graph.api.example.com/login/auth\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"9c523c937ec3694c\",\n      \"id\": \"1af69980dad7deac\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /oauth/authorize\",\n      \"timestamp\": 1543334671332751,\n      \"duration\": 1332,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/authorize\",\n        \"http.status_code\": \"302\",\n        \"oauth.param.client_id\": \"zipkin-sample-id\",\n        \"oauth.param.redirect_uri\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ\",\n        \"oauth.param.response_type\": \"code\",\n        \"oauth.param.scope\": \"app\",\n        \"oauth.param.state\": \"IntentionallyGarbage\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"be232464081e613d\",\n      \"id\": \"9c523c937ec3694c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334661718014,\n      \"duration\": 435,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.32\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"login\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"c47bff7f7964b321\",\n      \"id\": \"2fec181f2ef22058\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"bound-statement\",\n      \"timestamp\": 1543334661607218,\n      \"duration\": 1047,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.63\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.87\",\n        \"port\": 9042\n      },\n      \"tags\": {\n        \"cassandra.consistency_level\": \"ONE\",\n        \"cassandra.data_center\": \"us-east\",\n        \"cassandra.keyspace\": \"auth\",\n        \"cassandra.query\": \"SELECT username,uuid,samsung_account_id,okta_account_id,account_non_expired,account_non_locked,credentials_non_expired,deleted,email,phone_number,enabled,full_name,password,reason_deleted,roles,country_code FROM auth.user WHERE username=:username;\",\n        \"cassandra.rack\": \"1d\",\n        \"cassandra.state\": \"UP\",\n        \"cassandra.tried_hosts\": \"10.0.0.87\",\n        \"cassandra.version\": \"unknown\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"be232464081e613d\",\n      \"id\": \"c47bff7f7964b321\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post /sso/authenticate\",\n      \"timestamp\": 1543334661606025,\n      \"duration\": 3041,\n      \"localEndpoint\": {\n        \"serviceName\": \"auth\",\n        \"ipv4\": \"10.0.0.63\",\n        \"port\": 8180\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"52.0.0.9\",\n        \"port\": 51436\n      },\n      \"tags\": {\n        \"error\": \"401\",\n        \"http.path\": \"/sso/authenticate\",\n        \"http.status_code\": \"401\"\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"be232464081e613d\",\n      \"id\": \"c47bff7f7964b321\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"post\",\n      \"timestamp\": 1543334661559392,\n      \"duration\": 56057,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.48\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"error\": \"401\",\n        \"http.method\": \"POST\",\n        \"http.path\": \"/sso/authenticate\",\n        \"http.status_code\": \"401\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"219e12d0ebe2b39a\",\n      \"id\": \"be232464081e613d\",\n      \"kind\": \"SERVER\",\n      \"name\": \"post\",\n      \"timestamp\": 1543334661559010,\n      \"duration\": 56515,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.48\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"POST\",\n        \"http.path\": \"login\",\n        \"http.status_code\": \"302\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6ed62ab3544b76fc\",\n      \"id\": \"219e12d0ebe2b39a\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627816002,\n      \"duration\": 65,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.224\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"js/chunk-vendors.c863181d.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6ed62ab3544b76fc\",\n      \"id\": \"b652298e30752bb9\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627815005,\n      \"duration\": 117,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"css/app.5dd2fcd7.css\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6ed62ab3544b76fc\",\n      \"id\": \"f3e4621668bf9d44\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627814007,\n      \"duration\": 248,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.32\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"js/chunk-common.455c9996.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6ed62ab3544b76fc\",\n      \"id\": \"be7da77f52042b9c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627716006,\n      \"duration\": 133,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.48\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"js/app.f65d12ae.js\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"6ed62ab3544b76fc\",\n      \"id\": \"e39c4c6ca709cb6c\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627637026,\n      \"duration\": 211,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.224\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"css/chunk-common.2ff68acc.css\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"4d59559cb7d1753f\",\n      \"id\": \"6ed62ab3544b76fc\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627473021,\n      \"duration\": 271,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.8\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"login\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bb44efaef3c5c894\",\n      \"id\": \"4ce318f49fb2d88b\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334627370007,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.32\",\n        \"port\": 8141\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"4ce318f49fb2d88b\",\n      \"id\": \"4d59559cb7d1753f\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get\",\n      \"timestamp\": 1543334627370008,\n      \"duration\": 447,\n      \"localEndpoint\": {\n        \"serviceName\": \"stlogin\",\n        \"ipv4\": \"10.0.0.32\",\n        \"port\": 8141\n      },\n      \"tags\": {\n        \"http.method\": \"GET\",\n        \"http.path\": \"\",\n        \"http.status_code\": \"307\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"bb44efaef3c5c894\",\n      \"id\": \"4ce318f49fb2d88b\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334626979079,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://account.example.com/?redirect=https%3A%2F%2Fgraph.api.example.com%2Foauth%2Fauthorize%3Fclient_id%3Dzipkin-sample-id%26response_type%3Dcode%26state%3DA2SAAEAEPNoaFt\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"8ce82b2e9ed820ba\",\n      \"id\": \"d70bbce77a790a35\",\n      \"kind\": \"SERVER\",\n      \"timestamp\": 1543334626973644,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"shared\": true\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"d70bbce77a790a35\",\n      \"id\": \"bb44efaef3c5c894\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /login/auth\",\n      \"timestamp\": 1543334626973860,\n      \"duration\": 5090,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.196\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.path\": \"/login/auth\",\n        \"http.status_code\": \"302\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"parentId\": \"8ce82b2e9ed820ba\",\n      \"id\": \"d70bbce77a790a35\",\n      \"kind\": \"CLIENT\",\n      \"name\": \"redirect\",\n      \"timestamp\": 1543334626874647,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"tags\": {\n        \"http.redirect\": \"https://graph.api.example.com/login/auth\"\n      }\n    },\n    {\n      \"traceId\": \"8ce82b2e9ed820ba\",\n      \"id\": \"8ce82b2e9ed820ba\",\n      \"kind\": \"SERVER\",\n      \"name\": \"get /oauth/authorize\",\n      \"timestamp\": 1543334626873100,\n      \"duration\": 1429,\n      \"localEndpoint\": {\n        \"serviceName\": \"datamgmt\",\n        \"ipv4\": \"10.0.0.234\",\n        \"port\": 8080\n      },\n      \"remoteEndpoint\": {\n        \"ipv4\": \"23.0.0.0\",\n        \"port\": 23210\n      },\n      \"tags\": {\n        \"http.path\": \"/oauth/authorize\",\n        \"http.status_code\": \"302\",\n        \"oauth.param.client_id\": \"zipkin-sample-id\",\n        \"oauth.param.redirect_uri\": \"https://example.com/api/skill/link/M25XHJDAAO8RHQ\",\n        \"oauth.param.response_type\": \"code\",\n        \"oauth.param.scope\": \"app\",\n        \"oauth.param.state\": \"IntentionallyGarbage\"\n      }\n    }\n  ]\n  "
  },
  {
    "path": "zipkin-lens/testdata/yelp.json",
    "content": "[\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"f5f268651b2a2b34\",\n    \"id\": \"15fc03927f0f68df\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1571896375322000,\n    \"duration\": 14000,\n    \"localEndpoint\": {\n      \"serviceName\": \"mobile_api\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"blt\",\n      \"port\": 31882\n    },\n    \"tags\": {\n      \"client_status_code\": \"200\",\n      \"http.uri.client\": \"/visits\",\n      \"request_budget\": \"9980\",\n      \"tracer\": \"syslog2scribe.haproxy-synapse\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"f5f268651b2a2b34\",\n    \"id\": \"7a778764a0d0b594\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get\",\n    \"timestamp\": 1571896375310000,\n    \"duration\": 3000,\n    \"localEndpoint\": {\n      \"serviceName\": \"mobile_api\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"spectre\",\n      \"port\": 31286\n    },\n    \"tags\": {\n      \"client_status_code\": \"200\",\n      \"http.uri.client\": \"/visits\",\n      \"request_budget\": \"9989\",\n      \"tracer\": \"syslog2scribe.haproxy-synapse\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"f5f268651b2a2b34\",\n    \"id\": \"7a778764a0d0b594\",\n    \"kind\": \"SERVER\",\n    \"name\": \"get\",\n    \"timestamp\": 1571896375310740,\n    \"duration\": 1490,\n    \"localEndpoint\": {\n      \"serviceName\": \"spectre\"\n    },\n    \"tags\": {\n      \"ecosystem\": \"prod\",\n      \"habitat\": \"uswest1aprod\",\n      \"http.uri.client\": \"/token/abcdefgh123456\",\n      \"region\": \"uswest1-prod\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"f5f268651b2a2b34\",\n    \"id\": \"6a65182ea4f684c3\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"set mobile_api_nonce\",\n    \"timestamp\": 1571896375302030,\n    \"duration\": 1026,\n    \"localEndpoint\": {\n      \"serviceName\": \"mobile_api\",\n      \"port\": 31049\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"memcache\"\n    },\n    \"tags\": {\n      \"driver\": \"yelp_memcache\",\n      \"method\": \"set\",\n      \"requests\": \"1\",\n      \"system\": \"mobile_api_nonce\",\n      \"ttl\": \"\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"f5f268651b2a2b34\",\n    \"id\": \"cb4d73f31cd90cae\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get_multi mobile_api_nonce\",\n    \"timestamp\": 1571896375300642,\n    \"duration\": 1066,\n    \"localEndpoint\": {\n      \"serviceName\": \"mobile_api\",\n      \"port\": 31049\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"memcache\"\n    },\n    \"tags\": {\n      \"driver\": \"yelp_memcache\",\n      \"hits\": \"0\",\n      \"method\": \"get_multi\",\n      \"requests\": \"1\",\n      \"system\": \"mobile_api_nonce\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"2e8cfb154b59a41f\",\n    \"id\": \"f5f268651b2a2b34\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /location/update/v4\",\n    \"timestamp\": 1571896375297103,\n    \"duration\": 41740,\n    \"localEndpoint\": {\n      \"serviceName\": \"mobile_api\",\n      \"port\": 31049\n    },\n    \"tags\": {\n      \"ecosystem\": \"prod\",\n      \"habitat\": \"uswest1bprod\",\n      \"http.route\": \"/location/update/v4\",\n      \"http.uri\": \"/location/update/v4\",\n      \"http.uri.qs\": \"/location/update/v4\",\n      \"region\": \"uswest1-prod\",\n      \"response_status_code\": \"200\",\n      \"version_SHA\": \"6535284b1699df0a766384a648dc95c462a7313d\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"2e8cfb154b59a41f\",\n    \"id\": \"f5f268651b2a2b34\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1571896375287000,\n    \"duration\": 56000,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"mobile_api\",\n      \"port\": 31049\n    },\n    \"tags\": {\n      \"client_status_code\": \"200\",\n      \"http.uri.client\": \"/location/update/v4\",\n      \"request_budget\": \"10003\",\n      \"tracer\": \"syslog2scribe.haproxy-synapse\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"241cea1aa4cb2884\",\n    \"id\": \"0facde7c9130fd93\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get_multi my_cache_name_v1\",\n    \"timestamp\": 1571896375272125,\n    \"duration\": 233,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"memcache\"\n    },\n    \"tags\": {\n      \"driver\": \"core_memcache\",\n      \"hits\": \"1\",\n      \"method\": \"get_multi\",\n      \"requests\": \"1\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"241cea1aa4cb2884\",\n    \"id\": \"50b57281525a99d8\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"commit\",\n    \"timestamp\": 1571896375272604,\n    \"duration\": 374,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"mysql\"\n    },\n    \"tags\": {\n      \"query\": \"COMMIT\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"241cea1aa4cb2884\",\n    \"id\": \"2b68987704862c4f\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get user_details_cache-20150901\",\n    \"timestamp\": 1571896375270438,\n    \"duration\": 1068,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"memcache\"\n    },\n    \"tags\": {\n      \"driver\": \"core_memcache\",\n      \"hits\": \"1\",\n      \"method\": \"get\",\n      \"requests\": \"1\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"668ed78ad94b35a1\",\n    \"id\": \"241cea1aa4cb2884\",\n    \"name\": \"txn: user_get_basic_and_scout_info\",\n    \"timestamp\": 1571896375269210,\n    \"duration\": 3884,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"tags\": {\n      \"calling_method\": \"src/logic/db/user.py:1234:get_user\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"241cea1aa4cb2884\",\n    \"id\": \"b593cd7513dc736e\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"begin\",\n    \"timestamp\": 1571896375269732,\n    \"duration\": 445,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"mysql\"\n    },\n    \"tags\": {\n      \"query\": \"************\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"668ed78ad94b35a1\",\n    \"id\": \"e7d1a2d5a788ac81\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"get my_cache_name_v2\",\n    \"timestamp\": 1571896375268015,\n    \"duration\": 993,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp-main\",\n      \"port\": 31523\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"memcache\"\n    },\n    \"tags\": {\n      \"driver\": \"core_memcache\",\n      \"hits\": \"1\",\n      \"method\": \"get\",\n      \"requests\": \"1\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"2e8cfb154b59a41f\",\n    \"id\": \"668ed78ad94b35a1\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post api proxy proxy\",\n    \"timestamp\": 1571896375264995,\n    \"duration\": 88935,\n    \"localEndpoint\": {\n      \"serviceName\": \"yelp_main/api_proxy\",\n      \"port\": 31523\n    },\n    \"annotations\": [\n      {\n        \"timestamp\": 1571896375355436,\n        \"value\": \"py_zipkin.logging_end\"\n      }\n    ],\n    \"tags\": {\n      \"cprofile_enabled\": \"False\",\n      \"datacenter\": \"us-west-1\",\n      \"ecosystem\": \"prod\",\n      \"habitat\": \"uswest1aprod\",\n      \"host\": \"<host>\",\n      \"http.route\": \"/*path\",\n      \"http.uri\": \"/location/update/v4\",\n      \"http.uri.qs\": \"/location/update/v4\",\n      \"locale\": \"en_US\",\n      \"logged_in\": \"False\",\n      \"natural\": \"False\",\n      \"owner_email\": \"\",\n      \"paasta\": \"True\",\n      \"region\": \"uswest1-prod\",\n      \"request_budget\": \"10003\",\n      \"request_budget_soft\": \"5003\",\n      \"response_status_code\": \"200\",\n      \"servlet\": \"proxy\",\n      \"servlet_action\": \"proxy\",\n      \"site\": \"api\",\n      \"version_SHA\": \"5e83958d2c\"\n    },\n    \"shared\": true\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"parentId\": \"2e8cfb154b59a41f\",\n    \"id\": \"668ed78ad94b35a1\",\n    \"kind\": \"CLIENT\",\n    \"name\": \"post\",\n    \"timestamp\": 1571896375239000,\n    \"duration\": 125000,\n    \"localEndpoint\": {\n      \"serviceName\": \"unknown\"\n    },\n    \"remoteEndpoint\": {\n      \"serviceName\": \"yelp-main.mobile_api\",\n      \"port\": 31523\n    },\n    \"tags\": {\n      \"client_status_code\": \"200\",\n      \"http.uri.client\": \"/location/update/v4\",\n      \"request_budget\": \"10003\",\n      \"tracer\": \"syslog2scribe.envoy\"\n    }\n  },\n  {\n    \"traceId\": \"a03ee8fff1dcd9b9\",\n    \"id\": \"2e8cfb154b59a41f\",\n    \"kind\": \"SERVER\",\n    \"name\": \"post /location/update/v4\",\n    \"timestamp\": 1571896375237354,\n    \"duration\": 131848,\n    \"localEndpoint\": {\n      \"serviceName\": \"routing\"\n    },\n    \"tags\": {\n      \"ecosystem\": \"prod\",\n      \"habitat\": \"uswest1aprod\",\n      \"http.uri.client\": \"/location/update/v4\",\n      \"region\": \"uswest1-prod\",\n      \"response_status_code\": \"200\"\n    },\n    \"shared\": true\n  }\n]\n"
  },
  {
    "path": "zipkin-lens/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"alwaysStrict\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"esnext\", \"es2018\", \"es2017\", \"es2016\", \"es2015\", \"dom\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"newLine\": \"lf\",\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"target\": \"esnext\",\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "zipkin-lens/vite.config.ts",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/// <reference types=\"vitest\" />\n\nimport {defineConfig, UserConfig} from 'vite'\nimport * as path from \"path\";\n\n// baseUrl is the default path to lookup assets.\nconst baseUrl = process.env.BASE_URL || '/zipkin';\n\nconst zipkinProxyConfig = {\n  target: process.env.API_BASE || 'http://localhost:9411',\n  changeOrigin: true,\n};\n\nexport default defineConfig(():UserConfig => {\n  // https://vitejs.dev/config/\n  return {\n    server: {\n      port: 3000,\n      proxy: {\n        '/zipkin/api/v2': zipkinProxyConfig,\n        '/zipkin/config.json': zipkinProxyConfig,\n      },\n    },\n    resolve: {\n      alias: {\n        src: path.resolve('src/'),\n      },\n    },\n    // @ts-ignore\n    test: {\n      environment: 'happy-dom'\n    },\n    base: baseUrl,\n    build: {\n      outDir: 'build',\n      // use the same path patterns as the original react-scripts lens build\n      assetsDir: \"static\",\n      // uncomment to build with sourcemap, for troubleshooting zipkin-server\n      // sourcemap: true,\n      rollupOptions: {\n        output: {\n          assetFileNames({name}):string {\n            if (name?.includes('.css')) return 'static/css/[name].[hash].css'\n            if (name?.includes('.png')) return 'static/media/[name].[hash].png'\n            return 'static/[name].[hash][extname]'\n          },\n          chunkFileNames: `static/js/[name].[hash].js`,\n          entryFileNames: `static/js/[name].[hash].js`,\n        },\n      },\n    },\n  }\n})\n"
  },
  {
    "path": "zipkin-server/RATIONALE.md",
    "content": "# zipkin-server rationale\n\n## Custom Servers not supported\nTODO: list all the reasons why this has caused us pain. Also considerations that this helps with,\nsuch as our ability to change spring boot or armeria whenever we want.\n\n## Java 17\n\nAs Zipkin Server is a Spring Boot 3 application, it requires minimum JRE 17 to\nrun. Its collector and storage modules are also used by zipkin-dependencies\nwhich as of version 3.2 can operate on JRE 17 (due to Spark 3.4+).\n\n## Modules\n\n### Impact of auto-configuration being optional\nMost typically, we'd register `META-INF/spring.factories` for each module and property defaults in\n`zipkin-server-${moduleName}.yml`. However, this would rely on auto-configuration, which has a\nmeasurable performance impact and may be disabled.\n\nInstead, we perform discovery via a dict at the yaml path `zipkin.internal.module`. The entry would\nlook like this:\n```yaml\nzipkin:\n  internal:\n    module:\n      sqs: zipkin.module.collector.sqs.ZipkinSQSCollectorModule\n```\n\nThe above gives a very similar feeling towards auto-configuration, and with the ability to combine\nvalues together. However, it implies the named module loads all of its own module dependencies\nexplicitly. This is currently a non-issue as all of our extensions are self-contained, only\ndepending on configuration supplied by themselves or the Zipkin server.\n\n### yaml syntax\nStarting with Spring Boot 2.0, merging YAML lists from different profiles is no longer\nsupported. Here's what breaks.\n\nGiven zipkin-server-sqs.yml:\n```yaml\nzipkin:\n  internal:\n    module:\n      - zipkin.module.collector.sqs.ZipkinSQSCollectorModule\n```\n\n.. and zipkin-server-kinesis.yml\n```yaml\nzipkin:\n  internal:\n    module:\n      - zipkin.module.collector.kinesis.ZipkinKinesisCollectorModule\n```\n\nWhen both profiles are present, a `List<String>` property of `zipkin.internal.module` results in one\nof the above, not both. Those not checking the `/health` endpoint may not notice the problem.\n\n\nInstead, we use what is supported: map based property merging.\n\nGiven zipkin-server-sqs.yml:\n```yaml\nzipkin:\n  internal:\n    module:\n      sqs: zipkin.module.collector.sqs.ZipkinSQSCollectorModule\n```\n\n.. and zipkin-server-kinesis.yml\n```yaml\nzipkin:\n  internal:\n    module:\n      kinesis: zipkin.module.collector.kinesis.ZipkinKinesisCollectorModule\n```\n\nWhen both profiles are present, a `Map<String, String>` property of `zipkin.internal.module` results\nin both entries.\n\n## Performance related optimizations and impacts\n\nMany users expect Zipkin to start as fast as other metrics tools, such as Prometheus. In other words\ninstantly. While some have very fast laptops, performance can be quite different in non-laptop\nscenarios, particularly when using docker. Even some laptop users had reported actual startup times\nbetween 10 and 30 seconds. Slow start has caused at least one site to change products, and some\necosystem players to change products also. Hence, we take this very seriously.\n\nOur goal is to be at the second range for laptops and a few seconds or less in Docker inclusive of\nJVM start time.\n\nIt is equally important the amount of time until first healthy request. In other words, cheating by\ndeferring evaluation to make startup seem faster will not solve this. We have sites that block until\nhealthy, so moving the time around will not make things easier for them.\n\nLuckily, we do not support custom servers. This allows us many options that a typical Spring Boot\napplication will not be able to do. For example, we can shut off AutoConfiguration and strip out\ncomponents we don't use ourselves. Below discusses some of these tradeoffs\n\n### Disabling auto configuration\n\nIn the zipkin server yaml, we disable a significant amount of auto-configuration, and we also are\nvery strict about dependencies. This means there is less auto-configuration work going on in Zipkin\nserver than a normal Spring Boot application. Starting Zipkin with no options (just in-memory) may\nnot take notably longer than if auto configuration was disabled.\n\nWhen multiple configuration options are set, Zipkin can start measurably faster with auto-\nconfiguration disabled. For example, using storage throttling and elasticsearch storage, a\nmeasurement of best in 5 runs enabled vs disabled resulted in 4-7% improvement, depending on whether\nthe terms are in JVM total time, or time in Spring Boot.\n\nDisabling auto-configuration interferes with some functionality that uses implicit configuration,\nsuch as Actuator. That said, the way we disabled auto-configuration, was property based, which means\nany integration can re-enable it in worst case.\n\n### Making Actuator optional\n\nActuator is both a source of size and slowness. We have a profile called `skipActuator`, which\nallows us to track performance as other factors change such as the version of Spring Boot.\n\nWhile our build includes the ability to opt-out of actuator, the default should load what we haven't\ndisabled in yaml. `ActuatorImporter` reads the path \"zipkin.internal.actuator.include\" to find types\nauto-configuration would have otherwise discovered from `META-INF/spring.factories`. By reading just\nthe type names, we avoid a compile dependency which would break the `skipActuator` build feature.\n\nFuture versions (>v2.1) of Spring Boot may reduce the size or slowness of the actuator subsystem and\ncould imply a revisit. If such happens take close attention to size, not just startup time, as we\nskip actuator for both reasons.\n"
  },
  {
    "path": "zipkin-server/README.md",
    "content": "# zipkin-server\nZipkin Server is a Java 17+ service, packaged as an executable jar.\n\nSpan storage and collectors are [configurable](#configuration). By default, storage is in-memory,\nthe HTTP collector (POST /api/v2/spans endpoint) is enabled, and the server listens on port 9411.\n\nZipkin Server is implemented with [Armeria](https://github.com/line/armeria). While it uses [Spring Boot](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)\ninternally, Zipkin Server should not be considered a normal Spring Boot application.\n\n## Custom servers are not supported\n\nBy Custom servers we mean trying to use/embed `zipkin` as part of _an application you package_ (e.g. adding `zipkin-server` dependency to a Spring-boot application) instead of the packaged application we release.\n\nFor proper usage, see the guides below.\n\n## Quick-start\n\nThe quickest way to get started is to fetch the [latest released server](https://central.sonatype.com/search?q=zipkin&namespace=io.zipkin&name=zipkin-server&sort=published) as a self-contained executable jar. Note that the Zipkin server requires minimum JRE 8. For example:\n\n```bash\n$ curl -sSL https://zipkin.io/quickstart.sh | bash -s\n$ java -jar zipkin.jar\n```\n\nOnce you've started, browse to http://localhost:9411/zipkin to find traces!\n\n## Endpoints\n\nThe following endpoints are defined under the base url `http://your_host:9411`\n* / - [UI](../zipkin-lens)\n* /config.json - [Configuration for the UI](#ui)\n* /api/v2 - [API](https://zipkin.io/zipkin-api/#/)\n* /health - Returns 200 status if OK\n* /info - Provides the version of the running instance\n* /metrics - Includes collector metrics broken down by transport type\n* /prometheus - Prometheus scrape endpoint\n\nThe [legacy /api/v1 API](https://zipkin.io/zipkin-api/#/) is still supported. Backends are decoupled from the\nHTTP API via data conversion. This means you can still accept legacy data on new backends and visa versa. Enter\n`https://zipkin.io/zipkin-api/zipkin-api.yaml` into the explore box of the Swagger UI to view the old definition\n\n### CORS (Cross-origin Resource Sharing)\n\nBy default, all endpoints under `/api/v2` are configured to **allow** cross-origin requests.\n\nThis can be changed by modifying the property `zipkin.query.allowed-origins`.\n\nFor example, to allow CORS requests from `http://foo.bar.com`:\n\n```\nZIPKIN_QUERY_ALLOWED_ORIGINS=http://foo.bar.com\n```\n\nSee [Configuration](#configuration) for more about how Zipkin is configured.\n\n### Service and Span names query\nThe [Zipkin API](https://zipkin.io/zipkin-api/#/default/get_services) does not include\na parameter for how far back to look for service or span names. In order\nto prevent excessive load, service and span name queries are limited by\n`QUERY_LOOKBACK`, which defaults to 24hrs (two daily buckets: one for\ntoday and one for yesterday)\n\n## Logging\n\nBy default, zipkin writes log messages to the console at INFO level and above. You can adjust\ncategories using the `logging.level.XXX` property.\n\nFor example, if you want to enable debug logging for all zipkin categories, you can start the server like so:\n\n```bash\n$ java -jar zipkin.jar --logging.level.zipkin2=DEBUG\n```\n\nSee [Configuration](#configuration) for more about how Zipkin is configured.\n\n### Advanced Logging Configuration\nUnder the covers, the server uses [Spring Boot - Logback integration](http://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html#howto-configure-logback-for-logging).\nFor example, you can add `--logging.exception-conversion-word=%wEx{full}` to dump full stack traces\ninstead of truncated ones.\n\n## Metrics\n\nCollector Metrics are exported to the path `/metrics`. These and additional metrics are exported\nto the path `/prometheus`.\n\n### Example Prometheus configuration\nHere's an example `/prometheus` configuration, using the Prometheus\nexposition [text format version 0.0.4](https://prometheus.io/docs/instrumenting/exposition_formats/)\n\n```yaml\n  - job_name: 'zipkin'\n    scrape_interval: 5s\n    metrics_path: '/prometheus'\n    static_configs:\n      - targets: ['localhost:9411']\n    metric_relabel_configs:\n      # Response code count\n      - source_labels: [__name__]\n        regex: '^status_(\\d+)_(.*)$'\n        replacement: '${1}'\n        target_label: status\n      - source_labels: [__name__]\n        regex: '^status_(\\d+)_(.*)$'\n        replacement: '${2}'\n        target_label: path\n      - source_labels: [__name__]\n        regex: '^status_(\\d+)_(.*)$'\n        replacement: 'http_requests_total'\n        target_label: __name__\n```\n\n### Collector\n\nCollector metrics are broken down by transport, where the defaults are \"http\" and \"grpc\". The\nfollowing are exported to the \"/metrics\" endpoint:\n\n| Metric                                               | Description                                                                           |\n|------------------------------------------------------|---------------------------------------------------------------------------------------|\n| counter.zipkin_collector.messages.$transport         | cumulative messages received; should relate to messages reported by instrumented apps |\n| counter.zipkin_collector.messages_dropped.$transport | cumulative messages dropped; reasons include client disconnects or malformed content  |\n| counter.zipkin_collector.bytes.$transport            | cumulative message bytes                                                              |\n| counter.zipkin_collector.spans.$transport            | cumulative spans read; should relate to messages reported by instrumented apps        |\n| counter.zipkin_collector.spans_dropped.$transport    | cumulative spans dropped; reasons include sampling or storage failures                |\n| gauge.zipkin_collector.message_spans.$transport      | last count of spans in a message                                                      |\n| gauge.zipkin_collector.message_bytes.$transport      | last count of bytes in a message                                                      |\n\n## Configuration\nWe support ENV variable configuration, such as `STORAGE_TYPE=cassandra3`, as they are familiar to\nadministrators and easy to use in runtime environments such as Docker.\n\nHere are the top-level configuration of Zipkin:\n* `QUERY_PORT`: Listen port for the HTTP API and web UI; Defaults to 9411\n* `QUERY_ENABLED`: `false` disables the HTTP read endpoints under '/api/v2'. This also disables the\nUI, as it relies on the API. If your only goal is to restrict search, use `SEARCH_ENABLED` instead.\nIf your only goal is to disable the UI, use `UI_ENABLED` instead. Defaults to true\n* `SEARCH_ENABLED`: `false` disables searching in the query API and any indexing or post-processing\nin the collector to support search. This does not disable the entire UI, as trace by ID and\ndependency queries still operate. Disable this when you use another service (such as logs) to find\ntrace IDs. Defaults to true\n* `UI_ENABLED`: `false` disables the web UI, mounted at '/zipkin'. Defaults to true\n* `QUERY_TIMEOUT`: Sets the hard timeout for query requests. Accepts any duration string (e.g., 100ms).\nA value of 0 will disable the timeout completely. Defaults to 11s.\n* `QUERY_LOG_LEVEL`: Log level written to the console; Defaults to INFO\n* `QUERY_NAMES_MAX_AGE`: Controls the value of the `max-age` header zipkin-server responds with on\n http requests for autocompleted values in the UI (service names for example). Defaults to 300 seconds.\n* `QUERY_LOOKBACK`: How many milliseconds queries can look back from endTs; Defaults to 24 hours (two daily buckets: one for today and one for yesterday)\n* `STORAGE_TYPE`: SpanStore implementation: one of `mem`, `mysql`, `cassandra3`, `elasticsearch`\n* `COLLECTOR_SAMPLE_RATE`: Percentage of traces to retain, defaults to always sample (1.0).\n* `AUTOCOMPLETE_KEYS`: list of span tag keys which will be returned by the `/api/v2/autocompleteTags` endpoint; Tag keys should be comma separated e.g. \"instance_id,user_id,env\"\n* `AUTOCOMPLETE_TTL`: How long in milliseconds to suppress calls to write the same autocomplete key/value pair. Default 3600000 (1 hr)\n\n### Configuration file overrides\nUnder the scenes, all configuration are managed by Spring Boot. This means that properties may also\nbe overridden by system properties or any other alternative [supported by Spring Boot](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html).\n\nWe use [yaml configuration](src/main/resources/zipkin-server-shared.yml) to bind shorter or more\nidiomatic ENV variables to the Spring properties ultimately in use. While most users should only use\nenvironment variables, some may desire a properties file approach to override settings. For example,\nknowing we set `spring.config.name=zipkin-server`, Spring Boot will automatically look for a file\nnamed `zipkin-server.properties` in the current directory, and the same properties we set in yaml\ncan be overridden that way.\n\nIf you choose to use property-based configuration instead of ENV variables, you are choosing to\nself-support your configuration. This means you'll use [Spring Boot documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html)\nor [StackOverflow](https://stackoverflow.com/questions/tagged/spring-boot) to resolve concerns\nrelated to property resolution as opposed to raising issues or using our chat support. We have to\nmention this because configuration of Spring implies vast responsibility and our resources must be\nconserved for Zipkin related tasks.\n\n## UI\nZipkin has a web UI, automatically included in the exec jar, and is hosted by default on port 9411\nunder the path '/zipkin'. This is enabled unless `UI_ENABLED` or `QUERY_ENABLED` are set to false.\n\nWhen the UI loads, it reads default configuration from the `/config.json` endpoint.\n\n| Attribute                | Property                             | Description                                                                                                                                                                                                                                       |\n|--------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| environment              | zipkin.ui.environment                | The value here becomes a label in the top-right corner. Not required.                                                                                                                                                                             |\n| defaultLookback          | zipkin.ui.default-lookback           | Default duration in millis to look back when finding traces. Affects the \"Start time\" element in the UI. Defaults to 900000 (15 minutes in millis).                                                                                               |\n| searchEnabled            | zipkin.ui.search-enabled             | If the Discover screen is enabled. Defaults to true.                                                                                                                                                                                              |\n| queryLimit               | zipkin.ui.query-limit                | Default limit for Find Traces. Defaults to 10.                                                                                                                                                                                                    |\n| instrumented             | zipkin.ui.instrumented               | Which sites this Zipkin UI covers. Regex syntax. e.g. `http:\\/\\/example.com\\/.*` Defaults to match all websites (`.*`).                                                                                                                           |\n| logsUrl                  | zipkin.ui.logs-url                   | Logs query service url pattern. If specified, a button will appear on the trace page and will replace {traceId} in the url by the traceId. Not required.                                                                                          |\n| supportUrl               | zipkin.ui.support-url                | A URL where a user can ask for support. If specified, a link will be placed in the side menu to this URL, for example a page to file support tickets. Not required.                                                                               |\n| archivePostUrl           | zipkin.ui.archive-post-url           | Url to POST the current trace in Zipkin v2 json format. e.g. `https://longterm/api/v2/spans`. If specified, a button will appear on the trace page accordingly. Not required.                                                                     |\n| archiveUrl               | zipkin.ui.archive-url                | Url to a web application serving an archived trace, templated by '{traceId}'. e.g. `https://longterm/zipkin/trace/{traceId}`. This is shown in a confirmation message after a trace is successfully POSTed to the `archivePostUrl`. Not required. |\n| dependency.enabled       | zipkin.ui.dependency.enabled         | If the Dependencies screen is enabled. Defaults to true.                                                                                                                                                                                          |\n| dependency.lowErrorRate  | zipkin.ui.dependency.low-error-rate  | The rate of error calls on a dependency link that turns it yellow. Defaults to 0.5 (50%) set to >1 to disable.                                                                                                                                    |\n| dependency.highErrorRate | zipkin.ui.dependency.high-error-rate | The rate of error calls on a dependency link that turns it red. Defaults to 0.75 (75%) set to >1 to disable.                                                                                                                                      |\n| basePath                 | zipkin.ui.basepath                   | path prefix placed into the <base> tag in the UI HTML; useful when running behind a reverse proxy. Default \"/zipkin\"                                                                                                                              |\n\nTo map properties to environment variables, change them to upper-underscore case format. For\nexample, if using docker you can set `ZIPKIN_UI_QUERY_LIMIT=100` to affect `$.queryLimit` in `/config.json`.\n\n### Trace archival\nMost production Zipkin clusters store traces with a limited TTL. This makes it a bit inconvenient to\nshare a trace, as the link to it will expire after a few days.\n\nThe \"archive a trace\" feature helps with this. Launch a second zipkin server pointing to a storage with a longer\nTTL than the regular one and set the archivePostUrl and archiveUrl UI configs pointing to this second server.\nOnce archivePostUrl is set, a new \"Archive Trace\" button will appear on the trace view page.\n\n## Storage\n\n### In-Memory Storage\nZipkin's [In-Memory Storage](../zipkin/src/main/java/zipkin2/storage/InMemoryStorage.java) holds all\ndata in memory, purging older data upon a span limit. It applies when `STORAGE_TYPE` is unset or\nset to the value `mem`.\n\n    * `MEM_MAX_SPANS`: Oldest traces (and their spans) will be purged first when this limit is exceeded. Default 500000\n\nExample usage:\n```bash\n$ java -jar zipkin.jar\n```\n\nNote: this storage component was primarily developed for testing and as a means to get Zipkin server\nup and running quickly without external dependencies. It is not viable for high work loads. That\nsaid, if you encounter out-of-memory errors, try decreasing `MEM_MAX_SPANS` or increasing the heap\nsize (-Xmx).\n\nExampled of doubling the amount of spans held in memory:\n```bash\n$ MEM_MAX_SPANS=1000000 java -Xmx1G -jar zipkin.jar\n```\n\n### Cassandra Storage\nZipkin's [Cassandra storage component](../zipkin-storage/cassandra) supports Cassandra 3.11.3+\nand applies when `STORAGE_TYPE` is set to `cassandra3`:\n\n    * `CASSANDRA_KEYSPACE`: The keyspace to use. Defaults to \"zipkin2\"\n    * `CASSANDRA_CONTACT_POINTS`: Comma separated list of host addresses part of Cassandra cluster. You can also specify a custom port with 'host:port'. Defaults to localhost on port 9042.\n    * `CASSANDRA_LOCAL_DC`: Name of the datacenter that will be considered \"local\" for load balancing. Defaults to \"datacenter1\"\n    * `CASSANDRA_ENSURE_SCHEMA`: Ensuring cassandra has the latest schema. If enabled tries to execute scripts in the classpath prefixed with `cassandra-schema-cql3`. Defaults to true\n    * `CASSANDRA_USERNAME` and `CASSANDRA_PASSWORD`: Cassandra authentication. Will throw an exception on startup if authentication fails. No default\n    * `CASSANDRA_USE_SSL`: Requires `javax.net.ssl.trustStore` and `javax.net.ssl.trustStorePassword`, defaults to false.\n    * `CASSANDRA_SSL_HOSTNAME_VALIDATION`: Controls validation of Cassandra server hostname. defaults to true.\n\nThe following are tuning parameters which may not concern all users:\n\n    * `CASSANDRA_MAX_CONNECTIONS`: Max pooled connections per datacenter-local host. Defaults to 8\n    * `CASSANDRA_INDEX_CACHE_MAX`: Maximum trace index metadata entries to cache. Zero disables caching. Defaults to 100000.\n    * `CASSANDRA_INDEX_CACHE_TTL`: How many seconds to cache index metadata about a trace. Defaults to 60.\n    * `CASSANDRA_INDEX_FETCH_MULTIPLIER`: How many more index rows to fetch than the user-supplied query limit. Defaults to 3.\n\nExample usage with Cassandra with request logging (TRACE shows query values):\n```bash\n$ STORAGE_TYPE=cassandra3 java -jar zipkin.jar \\\n--logging.level.com.datastax.oss.driver.internal.core.tracker.RequestLogger=DEBUG\n```\n\n### Elasticsearch Storage\nZipkin's [Elasticsearch storage component](../zipkin-storage/elasticsearch)\nsupports versions Elasticsearch 7-8.x and OpenSearch 2.x and applies when\n`STORAGE_TYPE` is set to `elasticsearch`\n\nThe following apply when `STORAGE_TYPE` is set to `elasticsearch`:\n\n    * `ES_HOSTS`: A comma separated list of elasticsearch base urls to connect to ex. http://host:9200.\n                  Defaults to \"http://localhost:9200\".\n    * `ES_PIPELINE`: Indicates the ingest pipeline used before spans are indexed. No default.\n    * `ES_TIMEOUT`: Controls the connect, read and write socket timeouts (in milliseconds) for\n                    Elasticsearch / OpenSearch API. Defaults to 10000 (10 seconds)\n    * `ES_INDEX`: The index prefix to use when generating daily index names. Defaults to zipkin.\n    * `ES_DATE_SEPARATOR`: The date separator to use when generating daily index names. Defaults to '-'.\n    * `ES_INDEX_SHARDS`: The number of shards to split the index into. Each shard and its replicas\n                         are assigned to a machine in the cluster. Increasing the number of shards\n                         and machines in the cluster will improve read and write performance. Number\n                         of shards cannot be changed for existing indices, but new daily indices\n                         will pick up changes to the setting. Defaults to 5.\n    * `ES_INDEX_REPLICAS`: The number of replica copies of each shard in the index. Each shard and\n                           its replicas are assigned to a machine in the cluster. Increasing the\n                           number of replicas and machines in the cluster will improve read\n                           performance, but not write performance. Number of replicas can be changed\n                           for existing indices. Defaults to 1. It is highly discouraged to set this\n                           to 0 as it would mean a machine failure results in data loss.\n    * `ES_ENSURE_TEMPLATES`: Installs Zipkin index templates when missing. Setting this to false can\n                             lead to corrupted data when index templates mismatch expectations. If\n                             you set this to false, you choose to troubleshoot your own data or\n                             migration problems as opposed to relying on the community for this.\n                             Defaults to true.\n    * `ES_USERNAME` and `ES_PASSWORD`: Elasticsearch / OpenSearch basic authentication, which defaults to empty string.\n                                       Use when X-Pack security (formerly Shield) is in place.\n    * `ES_CREDENTIALS_FILE`: The location of a file containing Elasticsearch / OpenSearch basic authentication\n                             credentials, as properties. The username property is\n                             `zipkin.storage.elasticsearch.username`, password `zipkin.storage.elasticsearch.password`.\n                             This file is reloaded periodically, using `ES_CREDENTIALS_REFRESH_INTERVAL`\n                             as the interval. This parameter takes precedence over ES_USERNAME and\n                              ES_PASSWORD when specified.\n    * `ES_CREDENTIALS_REFRESH_INTERVAL`: Credentials refresh interval in seconds, which defaults to\n                                         1 second. This is the maximum amount of time spans will drop due to stale\n                                         credentials. Any errors reading the credentials file occur in logs at this rate.\n    * `ES_HTTP_LOGGING`: When set, controls the volume of HTTP logging of the Elasticsearch / OpenSearch API.\n                         Options are BASIC, HEADERS, BODY\n    * `ES_SSL_NO_VERIFY`: When true, disables the verification of server's key certificate chain.\n                          This is not appropriate for production. Defaults to false.\n    * `ES_TEMPLATE_PRIORITY`: The priority value of the composable index templates. This is only applicable\n                              for ES version 7.8 or above. Must be set, even to 0, to use composable template\n\nExample usage:\n\nTo connect normally:\n```bash\n$ STORAGE_TYPE=elasticsearch ES_HOSTS=http://myhost:9200 java -jar zipkin.jar\n```\n\nTo log Elasticsearch / OpenSearch API requests:\n```bash\n$ STORAGE_TYPE=elasticsearch ES_HTTP_LOGGING=BASIC java -jar zipkin.jar\n```\n\n#### Using a custom Key Store or Trust Store (SSL)\nIf your Elasticsearch / OpenSearch endpoint customized SSL configuration (for example self-signed) certificates,\nyou can use any of the following [subset of JSSE properties](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#T6) to connect.\n\n * javax.net.ssl.keyStore\n * javax.net.ssl.keyStorePassword\n * javax.net.ssl.keyStoreType\n * javax.net.ssl.trustStore\n * javax.net.ssl.trustStorePassword\n * javax.net.ssl.trustStoreType\n\nUsage example:\n```bash\n$ JAVA_OPTS='-Djavax.net.ssl.keyStore=keystore.p12 -Djavax.net.ssl.keyStorePassword=keypassword -Djavax.net.ssl.keyStoreType=PKCS12 -Djavax.net.ssl.trustStore=truststore.p12 -Djavax.net.ssl.trustStorePassword=trustpassword -Djavax.net.ssl.trustStoreType=PKCS12'\n$ STORAGE_TYPE=elasticsearch java $JAVA_OPTS -jar zipkin.jar\n```\n\nUnder the scenes, these map to properties prefixed `zipkin.storage.elasticsearch.ssl.`, which affect\nthe Armeria client used to connect to Elasticsearch / OpenSearch.\n\nThe above properties allow the most common SSL setup to work out of box. If you need more\ncustomization, please make a comment in [this issue](https://github.com/openzipkin/zipkin/issues/2774).\n\n#### Automatic Index Creation\nZipkin will automatically create new indices as needed. Elasticsearch / OpenSearch by default [allows](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation) automatic creation of said indices, though your local install may have been configured to disallow it. You can verify this in the cluster settings: `action.auto_create_index: false`.\n\n### Legacy (v1) storage components\nThe following components are no longer encouraged, but exist to help aid\ntransition to supported ones. These are indicated as \"v1\" as they use\ndata layouts based on Zipkin's V1 Thrift model, as opposed to the\nsimpler v2 data model currently used.\n\n#### MySQL Storage\nZipkin's [MySQL component](../zipkin-storage/mysql-v1) is tested against MySQL\n5.7 and applies when `STORAGE_TYPE` is set to `mysql`:\n\n    * `MYSQL_DB`: The database to use. Defaults to \"zipkin\".\n    * `MYSQL_USER` and `MYSQL_PASS`: MySQL authentication, which defaults to empty string.\n    * `MYSQL_HOST`: Defaults to localhost\n    * `MYSQL_TCP_PORT`: Defaults to 3306\n    * `MYSQL_MAX_CONNECTIONS`: Maximum concurrent connections, defaults to 10\n    * `MYSQL_USE_SSL`: Requires `javax.net.ssl.trustStore` and `javax.net.ssl.trustStorePassword`, defaults to false.\n\nNote: This module is not recommended for production usage. Before using this,\nyou must [apply the schema](../zipkin-storage/mysql-v1#applying-the-schema).\n\nAlternatively you can use `MYSQL_JDBC_URL` and specify the complete JDBC url yourself. Note that the URL constructed by\nusing the separate settings above will also include the following parameters:\n`?autoReconnect=true&useSSL=false&useUnicode=yes&characterEncoding=UTF-8`. If you specify the JDBC url yourself, add\nthese parameters as well.\n\nExample usage:\n\n```bash\n$ STORAGE_TYPE=mysql MYSQL_USER=root java -jar zipkin.jar\n```\n\n### Throttled Storage (Experimental)\nThese settings can be used to help tune the rate at which Zipkin flushes data to another, underlying\n`StorageComponent` (such as Elasticsearch):\n\n    * `STORAGE_THROTTLE_ENABLED`: Enables throttling\n    * `STORAGE_THROTTLE_MIN_CONCURRENCY`: Minimum number of Threads to use for writing to storage.\n    * `STORAGE_THROTTLE_MAX_CONCURRENCY`: Maximum number of Threads to use for writing to storage.\n    * `STORAGE_THROTTLE_MAX_QUEUE_SIZE`: How many messages to buffer while all Threads are writing data before abandoning a message (0 = no buffering).\n\nAs this feature is experimental, it is not recommended to run this in production environments.\n\n## Collector\n\n### HTTP Collector\nThe HTTP collector is enabled by default. It accepts spans via `POST /api/v1/spans` and\n`POST /api/v2/spans`, on the `${QUERY_PORT}` which defaults to 9411.\n\nThe HTTP collector supports the following configuration:\n\n| Property                        | Environment Variable     | Description                                              |\n|---------------------------------|--------------------------|----------------------------------------------------------|\n| `zipkin.collector.http.enabled` | `COLLECTOR_HTTP_ENABLED` | `false` disables the HTTP collector. Defaults to `true`. |\n\n### Scribe (Legacy) Collector\nA collector supporting Scribe is enabled when `COLLECTOR_SCRIBE_ENABLED=true`. New\nsites are discouraged from using this collector as Scribe is an archived\ntechnology.\n\n| Environment Variable | Property                           | Description                                                         |\n|----------------------|------------------------------------|---------------------------------------------------------------------|\n| `COLLECTOR_PORT`     | `zipkin.collector.scribe.port`     | The port to listen for thrift RPC scribe requests. Defaults to 9410 |\n| `SCRIBE_CATEGORY`    | `zipkin.collector.scribe.category` | Category zipkin spans will be consumed from. Defaults to `zipkin`   |\n\n### ActiveMQ Collector\nThe [ActiveMQ Collector](../zipkin-collector/activemq) is enabled when `ACTIVEMQ_URL` is set to a v5.x broker. The following settings apply in this case.\n\n| Environment Variable         | Property                                     | Description                                                                                                                                                                  |\n|------------------------------|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `COLLECTOR_ACTIVEMQ_ENABLED` | `zipkin.collector.activemq.enabled`          | `false` disables the ActiveMQ collector. Defaults to `true`.                                                                                                                 |\n| `ACTIVEMQ_URL`               | `zipkin.collector.activemq.url`              | [Connection URL](https://activemq.apache.org/uri-protocols) to the ActiveMQ broker, ex. `tcp://localhost:61616` or `failover:(tcp://localhost:61616,tcp://remotehost:61616)` |\n| `ACTIVEMQ_QUEUE`             | `zipkin.collector.activemq.queue`            | Queue from which to collect span messages. Defaults to `zipkin`                                                                                                              |\n| `ACTIVEMQ_CLIENT_ID_PREFIX`  | `zipkin.collector.activemq.client-id-prefix` | Client ID prefix for queue consumers. Defaults to `zipkin`                                                                                                                   |\n| `ACTIVEMQ_CONCURRENCY`       | `zipkin.collector.activemq.concurrency`      | Number of concurrent span consumers. Defaults to `1`                                                                                                                         |\n| `ACTIVEMQ_USERNAME`          | `zipkin.collector.activemq.username`         | Optional username to connect to the broker                                                                                                                                   |\n| `ACTIVEMQ_PASSWORD`          | `zipkin.collector.activemq.password`         | Optional password to connect to the broker                                                                                                                                   |\n\nExample usage:\n\n```bash\n$ ACTIVEMQ_URL=tcp://localhost:61616 java -jar zipkin.jar\n```\n\n### Kafka Collector\nThe Kafka collector is enabled when `KAFKA_BOOTSTRAP_SERVERS` is set to\na v0.10+ server. The following settings apply in this case. Some settings\ncorrespond to \"New Consumer Configs\" in [Kafka documentation](https://kafka.apache.org/documentation/#consumerconfigs).\n\n| Variable                  | New Consumer Config | Description                                                                                  |\n|---------------------------|---------------------|----------------------------------------------------------------------------------------------|\n| `COLLECTOR_KAFKA_ENABLED` | N/A                 | `false` disables the Kafka collector. Defaults to `true`.                                    |\n| `KAFKA_BOOTSTRAP_SERVERS` | bootstrap.servers   | Comma-separated list of brokers, ex. 127.0.0.1:9092. No default                              |\n| `KAFKA_GROUP_ID`          | group.id            | The consumer group this process is consuming on behalf of. Defaults to `zipkin`              |\n| `KAFKA_TOPIC`             | N/A                 | Comma-separated list of topics that zipkin spans will be consumed from. Defaults to `zipkin` |\n| `KAFKA_STREAMS`           | N/A                 | Count of threads consuming the topic. Defaults to `1`                                        |\n\nExample usage:\n\n```bash\n$ KAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 \\\n    java -jar zipkin.jar\n```\n\n#### Other Kafka consumer properties\nYou may need to set other\n[Kafka consumer properties](https://kafka.apache.org/documentation/#consumerconfigs), in\naddition to the ones with explicit properties defined by the collector. In this case, you need to\nprefix that property name with `zipkin.collector.kafka.overrides` and pass it as a system property\nargument.\n\nFor example, to override `auto.offset.reset`, you can set a system property named\n`zipkin.collector.kafka.overrides.auto.offset.reset`:\n\n```bash\n$ KAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 \\\n    java -Dzipkin.collector.kafka.overrides.auto.offset.reset=latest -jar zipkin.jar\n```\n\n\n### Pulsar Collector\nThe Pulsar collector is enabled when `PULSAR_SERVICE_URL` is set to\na v4.x+ server. The following settings apply in this case.\nSome settings correspond to \"New Client Configs\" in [Pulsar client properties](https://github.com/apache/pulsar/blob/master/pulsar-client/src/main/java/org/apache/pulsar/client/impl/conf/ClientConfigurationData.java)\nand \"New Consumer Configs\" in [Pulsar consumer properties](https://github.com/apache/pulsar/blob/master/pulsar-client/src/main/java/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.java).\n\n| Variable                   | Property                                    | Description                                                                    |\n|----------------------------|---------------------------------------------|--------------------------------------------------------------------------------|\n| `COLLECTOR_PULSAR_ENABLED` | `zipkin.collector.pulsar.enabled`           | `false` disables the Pulsar collector. Defaults to `true`.                     |\n| `PULSAR_SERVICE_URL`       | `zipkin.collector.pulsar.service-url`       | The service URL for the Pulsar client ex. pulsar://my-broker:6650. No default. |\n| `PULSAR_TOPIC`             | `zipkin.collector.pulsar.topic`             | Queue zipkin spans will be consumed from. Defaults to \"zipkin\".                |\n| `PULSAR_SUBSCRIPTION_NAME` | `zipkin.collector.pulsar.subscription-name` | Specify the subscription name for this consumer. No default.                   |\n| `PULSAR_CONCURRENCY`       | `zipkin.collector.pulsar.concurrency`       | Count of concurrent message consumers on the topic. Defaults to 1              |\n\nExample usage:\n\n```bash\n$ PULSAR_SERVICE_URL=pulsar://localhost:6650 \\\n    java -jar zipkin.jar\n```\n\n\n#### Other Pulsar client properties\nYou may need to set other\n[Pulsar client properties](https://github.com/apache/pulsar/blob/master/pulsar-client/src/main/java/org/apache/pulsar/client/impl/conf/ClientConfigurationData.java), in\naddition to the ones with explicit properties defined by the collector. In this case, you need to\nprefix that property name with `zipkin.collector.pulsar.client-props` and pass it as a system property\nargument.\n\nFor example, to set `num.io.threads`, you can set a system property named\n`zipkin.collector.pulsar.client-props.numIoThreads`:\n\n```bash\n$ PULSAR_SERVICE_URL=pulsar://localhost:6650 \\\n    java -Dzipkin.collector.pulsar.client-props.numIoThreads=20 -jar zipkin.jar\n```\n\n\n#### Other Pulsar consumer properties\nYou may need to set other\n[Pulsar consumer properties](https://github.com/apache/pulsar/blob/master/pulsar-client/src/main/java/org/apache/pulsar/client/impl/conf/ConsumerConfigurationData.java), in\naddition to the ones with explicit properties defined by the collector. In this case, you need to\nprefix that property name with `zipkin.collector.pulsar.consumer-props` and pass it as a system property\nargument.\n\nFor example, to set `receiver.queue.size`, you can set a system property named\n`zipkin.collector.pulsar.consumer-props.receiverQueueSize`:\n\n```bash\n$ PULSAR_SERVICE_URL=pulsar://localhost:6650 \\\n    java -Dzipkin.collector.pulsar.consumer-props.receiverQueueSize=2000 -jar zipkin.jar\n```\n\n#### Detailed examples\n\nExample targeting Kafka running in Docker:\n\n```bash\n$ export KAFKA_BOOTSTRAP_SERVERS=$(docker-machine ip `docker-machine active`)\n# Run Kafka in the background\n$ docker run -d -p 9092:9092 \\\n    --env ADVERTISED_HOST=$KAFKA_BOOTSTRAP_SERVERS \\\n    --env AUTO_CREATE_TOPICS=true \\\n    spotify/kafka\n# Start the zipkin server, which reads $KAFKA_BOOTSTRAP_SERVERS\n$ java -jar zipkin.jar\n```\n\nMultiple bootstrap servers:\n\n```bash\n$ KAFKA_BOOTSTRAP_SERVERS=broker1.local:9092,broker2.local:9092 \\\n    java -jar zipkin.jar\n```\n\nAlternate topic name(s):\n\n```bash\n$ KAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 \\\n    java -Dzipkin.collector.kafka.topic=zapkin,zipken -jar zipkin.jar\n```\n\nSpecifying bootstrap servers as a system property, instead of an environment variable:\n\n```bash\n$ java -Dzipkin.collector.kafka.bootstrap-servers=127.0.0.1:9092 \\\n    -jar zipkin.jar\n```\n\n### RabbitMQ collector\nThe [RabbitMQ collector](../zipkin-collector/rabbitmq) will be enabled when the `addresses` or `uri` for the RabbitMQ server(s) is set.\n\nExample usage:\n\n```bash\n$ RABBIT_ADDRESSES=localhost java -jar zipkin.jar\n```\n\n### gRPC Collector\n\nThe gRPC collector is enabled by default. It accepts spans via `zipkin.proto3.SpanService/Report`,\non the `${QUERY_PORT}` which defaults to 9411.\n\nThe gRPC collector supports the following configuration:\n\n| Variable                 | Description                                            |\n|--------------------------|--------------------------------------------------------|\n| `COLLECTOR_GRPC_ENABLED` | `false` disables the gRPC service. Defaults to `true`. |\n\nThe proto definition is here: https://github.com/openzipkin/zipkin-api/blob/master/zipkin.proto\n\n## Service Registration\n\n### Eureka\n\nZipkin can register itself in [Eureka](https://github.com/Netflix/eureka), allowing traced services\nto discover its listen address and health state. This is enabled when `EUREKA_SERVICE_URL` is set to\na valid v2 endpoint of the [Eureka REST API](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations).\n\n| Variable                   | Instance field | Description                                                             |\n|----------------------------|----------------|-------------------------------------------------------------------------|\n| `DISCOVERY_EUREKA_ENABLED` | N/A            | `false` disables Eureka registration. Defaults to `true`.               |\n| `EUREKA_SERVICE_URL`       | N/A            | v2 endpoint of Eureka, e.g. `https://eureka-prod/eureka/v2`. No default |\n| `EUREKA_APP_NAME`          | .app           | The application this instance registers to. Defaults to `zipkin`        |\n| `EUREKA_HOSTNAME`          | .hostName      | The instance `hostName`. Defaults to detect.                            |\n| `EUREKA_INSTANCE_ID`       | .instanceId    | Defaults to `${EUREKA_HOSTNAME}:${EUREKA_APP_NAME}:${QUERY_PORT}`.      |\n\nExample usage:\n\n```bash\n$ EUREKA_SERVICE_URL=http://localhost:8761/eureka/v2 java -jar zipkin.jar\n```\n\nIf you are using a containerized environment, you may need to set `EUREKA_HOSTNAME` to avoid\ndetecting the wrong hostname. For example, if using docker-compose, set `EUREKA_HOSTNAME` to\nzipkin's `container_name`.\n\nIf your Eureka server requires authentication, adjust `EUREKA_SERVICE_URL` accordingly. If user info\nis present, those credentials will be used for BASIC authentication. For example, if the URL is\n`https://myuser:mypassword@1.1.3.1/eureka/v2/`, requests to Eureka will authenticate with the user\n\"myuser\" and password \"mypassword\".\n\nNote: Eureka server registration only includes host and port details. Tracers need to resolve this\nto the POST endpoint \"/api/v2/spans\".\n\n## Self-Tracing\nSelf tracing exists to help troubleshoot performance of the zipkin-server. Production deployments\nwho enable self-tracing should lower the sample rate from 1.0 (100%) to a much smaller rate, like\n0.001 (0.1% or 1 out of 1000).\n\nWhen `zipkin.self-tracing.enabled=true`, Zipkin will self-trace calls to the API under the service\nname \"zipkin-server\".\n\n| Variable                    | Property                           | Description                                                              |\n|-----------------------------|------------------------------------|--------------------------------------------------------------------------|\n| SELF_TRACING_ENABLED        | zipkin.self-tracing.enabled        | Set to true to enable self-tracing. Defaults to false                    |\n| SELF_TRACING_SAMPLE_RATE    | zipkin.self-tracing.sample-rate    | Percentage of self-traces to retain, defaults to always sample (1.0).    |\n| SELF_TRACING_FLUSH_INTERVAL | zipkin.self-tracing.flush-interval | Interval in seconds to flush self-tracing data to storage. Defaults to 1 |\n\n### 128-bit trace IDs\n\nZipkin supports 64 and 128-bit trace identifiers, typically serialized\nas 16 or 32 character hex strings. By default, spans reported to zipkin\nwith the same trace ID will be considered in the same trace.\n\nFor example, `463ac35c9f6413ad48485a3953bb6124` is a 128-bit trace ID,\nwhile `48485a3953bb6124` is a 64-bit one.\n\nNote: Span (or parent) IDs within a trace are 64-bit regardless of the\nlength or value of their trace ID.\n\n#### Migrating from 64 to 128-bit trace IDs\n\nUnless you only issue 128-bit traces when all applications support them,\nthe process of updating applications from 64 to 128-bit trace IDs results\nin a mixed state. This mixed state is mitigated by the setting\n`STRICT_TRACE_ID=false`, explained below. Once a migration is complete,\nremove the setting `STRICT_TRACE_ID=false` or set it to true.\n\nHere are a few trace IDs the help what happens during this setting.\n\n* Trace ID A: 463ac35c9f6413ad48485a3953bb6124\n* Trace ID B: 48485a3953bb6124\n* Trace ID C: 463ac35c9f6413adf1a48a8cff464e0e\n* Trace ID D: 463ac35c9f6413ad\n\nIn a 64-bit environment, trace IDs will look like B or D above. When an\napplication upgrades to 128-bit instrumentation and decides to create a\n128-bit trace, its trace IDs will look like A or C above.\n\nApplications who aren't yet 128-bit capable typically only retain the\nright-most 16 characters of the trace ID. When this happens, the same\ntrace could be reported as trace ID A or trace ID B.\n\nBy default, Zipkin will think these are different trace IDs, as they are\ndifferent strings. During a transition from 64-128 bit trace IDs, spans\nwould appear split across two IDs. For example, it might start as trace\nID A, but the next hop might truncate it to trace ID B. This would render\nthe system unusable for applications performing upgrades.\n\nOne way to address this problem is to not use 128-bit trace IDs until\nall applications support them. This prevents a mixed scenario at the cost\nof coordination. Another way is to set `STRICT_TRACE_ID=false`.\n\nWhen `STRICT_TRACE_ID=false`, only the right-most 16 of a 32 character\ntrace ID are considered when grouping or retrieving traces. This setting\nshould only be applied when transitioning from 64 to 128-bit trace IDs\nand removed once the transition is complete.\n\nSee https://github.com/openzipkin/b3-propagation/issues/6 for the status\nof known open source libraries on 128-bit trace identifiers.\n\nSee `zipkin2.storage.StorageComponent.Builder` for even more details!\n\n## TLS/SSL\nZipkin-server can be made to run with TLS if needed:\n\n```bash\n# assuming you generate the key like this\nkeytool -genkeypair -alias mysite -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore zipkin.p12 -validity 3650\n\njava -jar zipkin.jar --armeria.ssl.key-store=zipkin.p12 --armeria.ssl.key-store-type=PKCS12 --armeria.ssl.key-store-password=123123 --armeria.ssl.key-alias=mysite  --armeria.ssl.enabled=true --armeria.ports[0].port=9411 --armeria.ports[0].protocols[0]=https\n```\n\n## Running with Docker\nReleased versions of zipkin-server are published to Docker Hub as `openzipkin/zipkin`.\nSee [docker-zipkin](https://github.com/openzipkin/docker-zipkin) for details.\n\n## Building locally\n\nTo build and run the server from the currently checked out source, enter the following.\n```bash\n# Build the server and also make its dependencies\n$ ./mvnw -T1C -q --batch-mode -DskipTests --also-make -pl zipkin-server clean package\n# Run the server\n$ java -jar ./zipkin-server/target/zipkin-server-*exec.jar\n# or Run the slim server\n$ java -jar ./zipkin-server/target/zipkin-server-*slim.jar\n```\n"
  },
  {
    "path": "zipkin-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-server</artifactId>\n  <name>Zipkin Server</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n\n    <!-- Spring Boot 3 requires JRE 17 -->\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <maven.compiler.release>17</maven.compiler.release>\n\n    <!-- Sometimes we need to override Armeria's Brave version -->\n    <brave.version>6.1.0</brave.version>\n    <zipkin-reporter.version>3.5.0</zipkin-reporter.version>\n    <log4j.version>2.23.1</log4j.version>\n    <proto.generatedSourceDirectory>${project.build.directory}/generated-test-sources/wire</proto.generatedSourceDirectory>\n  </properties>\n\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>org.apache.logging.log4j</groupId>\n        <artifactId>log4j-bom</artifactId>\n        <version>${log4j.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <dependency>\n        <groupId>io.netty</groupId>\n        <artifactId>netty-bom</artifactId>\n        <version>${netty.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <dependency>\n        <groupId>io.zipkin.brave</groupId>\n        <artifactId>brave-bom</artifactId>\n        <version>${brave.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <dependency>\n        <groupId>com.fasterxml.jackson</groupId>\n        <artifactId>jackson-bom</artifactId>\n        <version>${jackson.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <!-- reconcile armeria and spring-boot versions -->\n      <dependency>\n        <groupId>io.micrometer</groupId>\n        <artifactId>micrometer-bom</artifactId>\n        <version>${micrometer.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <dependencies>\n    <!-- Makes sure spring doesn't eagerly bind tomcat or slf4j -->\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter</artifactId>\n      <version>${spring-boot.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>org.springframework.boot</groupId>\n          <artifactId>spring-boot-starter-logging</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-actuator</artifactId>\n      <version>${spring-boot.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>org.springframework.boot</groupId>\n          <artifactId>spring-boot-starter-logging</artifactId>\n        </exclusion>\n        <!-- We only include a couple actuator endpoints, and metrics are served directly -->\n        <exclusion>\n          <groupId>io.micrometer</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n\n    <!-- module support requires org.springframework.boot.loader.launch.PropertiesLauncher\n         From version 3, this is no longer included in spring-boot-starter. -->\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-loader-classic</artifactId>\n      <version>${spring-boot.version}</version>\n    </dependency>\n\n    <!-- Use log4j 2 as default logging implementation -->\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-log4j2</artifactId>\n      <version>${spring-boot.version}</version>\n    </dependency>\n\n    <!-- More efficient, gRPC ready server with brave tracing built-in -->\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-spring-boot3-autoconfigure</artifactId>\n      <version>${armeria.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>com.linecorp.armeria</groupId>\n          <artifactId>armeria-logback</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>javax.validation</groupId>\n          <artifactId>validation-api</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-brave6</artifactId>\n      <version>${armeria.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-grpc-protocol</artifactId>\n      <version>${armeria.version}</version>\n    </dependency>\n\n    <!-- Eureka service discovery -->\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-eureka</artifactId>\n      <version>${armeria.version}</version>\n    </dependency>\n\n    <!--Prometheus metrics-->\n    <dependency>\n      <groupId>io.micrometer</groupId>\n      <artifactId>micrometer-registry-prometheus</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>io.micrometer</groupId>\n      <artifactId>micrometer-registry-prometheus-simpleclient</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.netflix.concurrency-limits</groupId>\n      <artifactId>concurrency-limits-core</artifactId>\n      <version>0.5.4</version>\n    </dependency>\n    <dependency>\n      <groupId>io.micrometer</groupId>\n      <artifactId>micrometer-core</artifactId>\n    </dependency>\n\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-api</artifactId>\n      <version>${slf4j.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>jul-to-slf4j</artifactId>\n      <version>${slf4j.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>jcl-over-slf4j</artifactId>\n      <version>${slf4j.version}</version>\n    </dependency>\n\n    <!-- the \"exec\" jar will exclude this -->\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-simple</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <!-- the \"slim\" jar will exclude this -->\n    <dependency>\n      <groupId>org.apache.logging.log4j</groupId>\n      <artifactId>log4j-core</artifactId>\n      <scope>provided</scope>\n    </dependency>\n\n    <!-- Optional, but packaged in the supported distribution -->\n\n    <!-- Cassandra backend -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-cassandra</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- Elasticsearch http -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-elasticsearch</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- MySQL backend -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-storage-mysql-v1</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n    <dependency>\n      <groupId>org.mariadb.jdbc</groupId>\n      <artifactId>mariadb-java-client</artifactId>\n      <version>${mariadb-java-client.version}</version>\n      <optional>true</optional>\n    </dependency>\n    <dependency>\n      <groupId>com.zaxxer</groupId>\n      <artifactId>HikariCP</artifactId>\n      <version>${HikariCP.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- ActiveMQ Collector -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector-activemq</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- Kafka Collector -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector-kafka</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- RabbitMQ Collector -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector-rabbitmq</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- Scribe Collector -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector-scribe</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- Pulsar Collector -->\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-collector-pulsar</artifactId>\n      <version>${project.version}</version>\n      <optional>true</optional>\n    </dependency>\n\n    <!-- Trace api controller activity with Brave -->\n    <dependency>\n      <groupId>io.zipkin.brave</groupId>\n      <artifactId>brave</artifactId>\n      <optional>true</optional>\n    </dependency>\n    <dependency>\n      <groupId>io.zipkin.brave</groupId>\n      <artifactId>brave-context-slf4j</artifactId>\n      <optional>true</optional>\n    </dependency>\n    <dependency>\n      <groupId>io.zipkin.reporter2</groupId>\n      <artifactId>zipkin-reporter-brave</artifactId>\n      <version>${zipkin-reporter.version}</version>\n      <optional>true</optional>\n    </dependency>\n<!--    <dependency>-->\n<!--      <groupId>io.zipkin.brave.cassandra</groupId>-->\n<!--      <artifactId>brave-instrumentation-cassandra-driver</artifactId>-->\n<!--      <version>0.10.3</version>-->\n<!--      <optional>true</optional>-->\n<!--    </dependency>-->\n\n    <!-- Test dependencies -->\n    <!-- to test the experimental grpc endpoint with the square/wire library -->\n    <dependency>\n      <groupId>io.zipkin.proto3</groupId>\n      <artifactId>zipkin-proto3</artifactId>\n      <version>${zipkin-proto3.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- Our json codec is shaded, but Intellij doesn't understand that when running tests. -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.squareup.wire</groupId>\n      <artifactId>wire-runtime-jvm</artifactId>\n      <version>${wire.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.squareup.okhttp3</groupId>\n      <artifactId>okhttp</artifactId>\n      <version>${okhttp.version}</version>\n      <scope>test</scope>\n      <exclusions>\n        <!-- let wire control the okio version -->\n        <exclusion>\n          <groupId>com.squareup.okio</groupId>\n          <artifactId>okio</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-junit5</artifactId>\n      <version>${armeria.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n      <version>${testcontainers.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <!-- Don't use spring-boot-starter-test as it complicates the dependency tree and causes heavier\n         downloads. We only need the spring-provided libraries -->\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-test-autoconfigure</artifactId>\n      <version>${spring-boot.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>*</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-test</artifactId>\n      <version>${spring-boot.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>*</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-test</artifactId>\n      <version>${spring.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- Needed by ZipkinHttpClientSender -->\n    <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-web</artifactId>\n      <version>${spring.version}</version>\n      <exclusions>\n        <exclusion>\n          <groupId>*</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n      </exclusions>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>${project.groupId}.zipkin2</groupId>\n      <artifactId>zipkin-tests</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.awaitility</groupId>\n      <artifactId>awaitility</artifactId>\n      <version>${awaitility.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.jayway.jsonpath</groupId>\n      <artifactId>json-path</artifactId>\n      <version>2.10.0</version>\n      <scope>test</scope>\n    </dependency>\n\n    <!-- Main code uses jul and tests log with log4j -->\n    <dependency>\n      <groupId>org.apache.logging.log4j</groupId>\n      <artifactId>log4j-jul</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <!-- Don't add log4j-slf4j-impl as simplelogger is in the classpath! -->\n    <dependency>\n      <groupId>org.apache.logging.log4j</groupId>\n      <artifactId>log4j-1.2-api</artifactId>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <profiles>\n    <profile>\n      <id>actuator</id>\n      <activation>\n        <property>\n          <name>!skipActuator</name>\n        </property>\n      </activation>\n      <dependencies>\n        <!-- /actuator endpoints -->\n        <dependency>\n          <groupId>com.linecorp.armeria</groupId>\n          <artifactId>armeria-spring-boot3-actuator-autoconfigure</artifactId>\n          <version>${armeria.version}</version>\n          <optional>true</optional>\n        </dependency>\n      </dependencies>\n    </profile>\n    <profile>\n      <id>include-lens</id>\n      <activation>\n        <property>\n          <name>!skipLens</name>\n        </property>\n      </activation>\n      <dependencies>\n        <!-- Static content for the Lens UI -->\n        <dependency>\n          <groupId>${project.groupId}</groupId>\n          <artifactId>zipkin-lens</artifactId>\n          <version>${project.version}</version>\n        </dependency>\n      </dependencies>\n    </profile>\n  </profiles>\n\n  <build>\n    <resources>\n      <resource>\n        <directory>src/main/resources</directory>\n        <filtering>true</filtering>\n      </resource>\n    </resources>\n    <plugins>\n      <!-- wire-maven-plugin cannot get proto definitions from dependencies, so we will -->\n      <plugin>\n        <artifactId>maven-dependency-plugin</artifactId>\n      </plugin>\n\n      <!-- Ensure jul -> log4j -->\n      <plugin>\n        <artifactId>maven-failsafe-plugin</artifactId>\n        <configuration>\n          <systemPropertyVariables>\n            <java.util.logging.manager>org.apache.logging.log4j.jul.LogManager</java.util.logging.manager>\n          </systemPropertyVariables>\n        </configuration>\n      </plugin>\n\n      <plugin>\n        <groupId>de.m3y.maven</groupId>\n        <artifactId>wire-maven-plugin</artifactId>\n        <executions>\n          <execution>\n            <phase>generate-test-sources</phase>\n            <goals>\n              <goal>generate-sources</goal>\n            </goals>\n            <configuration>\n              <generatedSourceDirectory>${proto.generatedSourceDirectory}</generatedSourceDirectory>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n      <!-- Adds the output directory from proto source generation for the test compiler -->\n      <plugin>\n        <groupId>org.codehaus.mojo</groupId>\n        <artifactId>build-helper-maven-plugin</artifactId>\n        <version>${build-helper-maven-plugin.version}</version>\n        <executions>\n          <execution>\n            <id>add-test-source</id>\n            <phase>generate-test-sources</phase>\n            <goals>\n              <goal>add-test-source</goal>\n            </goals>\n            <configuration>\n              <sources>\n                <source>${proto.generatedSourceDirectory}</source>\n              </sources>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n\n      <plugin>\n        <groupId>pl.project13.maven</groupId>\n        <artifactId>git-commit-id-plugin</artifactId>\n        <version>${git-commit-id.version}</version>\n        <executions>\n          <execution>\n            <id>extract-git-info</id>\n            <goals>\n              <goal>revision</goal>\n            </goals>\n          </execution>\n        </executions>\n        <configuration>\n          <failOnNoGitDirectory>false</failOnNoGitDirectory>\n        </configuration>\n      </plugin>\n\n      <plugin>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-maven-plugin</artifactId>\n        <version>${spring-boot.version}</version>\n        <configuration>\n          <mainClass>zipkin.server.ZipkinServer</mainClass>\n          <executable>true</executable>\n        </configuration>\n        <executions>\n          <execution>\n            <id>exec</id>\n            <goals>\n              <goal>repackage</goal>\n            </goals>\n            <configuration>\n              <classifier>exec</classifier>\n              <excludes>\n                <dependency>\n                  <groupId>org.slf4j</groupId>\n                  <artifactId>slf4j-simple</artifactId>\n                </dependency>\n              </excludes>\n            </configuration>\n          </execution>\n\n          <!-- Make a jar less than half the size, which only supports armeria-native features -->\n          <execution>\n            <id>slim</id>\n            <goals>\n              <goal>repackage</goal>\n            </goals>\n            <configuration>\n              <classifier>slim</classifier>\n              <!-- https://github.com/spring-projects/spring-boot/issues/3426 transitive exclude doesn't work -->\n              <excludeGroupIds>\n                com.google.auto.value,com.google.guava,io.dropwizard.metrics,org.apache.cassandra,com.github.jnr,org.ow2.asm,org.jooq,javax.xml.bind,org.mariadb.jdbc,com.zaxxer,org.apache.activemq,org.apache.geronimo.specs,org.fusesource.hawtbuf,org.apache.kafka,com.github.luben,org.lz4,org.xerial.snappy,com.rabbitmq,jakarta.annotation,org.apache.thrift,org.apache.logging.log4j\n              </excludeGroupIds>\n              <excludes>\n                <!-- Actuator -->\n                <dependency>\n                  <groupId>com.linecorp.armeria</groupId>\n                  <artifactId>armeria-spring-boot3-actuator-autoconfigure</artifactId>\n                </dependency>\n                <dependency>\n                  <groupId>org.springframework.boot</groupId>\n                  <artifactId>spring-boot-actuator-autoconfigure</artifactId>\n                </dependency>\n                <dependency>\n                  <groupId>org.springframework.boot</groupId>\n                  <artifactId>spring-boot-actuator</artifactId>\n                </dependency>\n                <dependency>\n                  <groupId>com.fasterxml.jackson.datatype</groupId>\n                  <artifactId>jackson-datatype-jsr310</artifactId>\n                </dependency>\n\n                <!-- Log4J 2 -->\n                <dependency>\n                  <groupId>org.springframework.boot</groupId>\n                  <artifactId>spring-boot-starter-log4j2</artifactId>\n                </dependency>\n\n                <!-- Unnecessary netty deps -->\n                <dependency>\n                  <groupId>io.netty</groupId>\n                  <artifactId>netty-codec-haproxy</artifactId>\n                </dependency>\n\n                <!-- removes boringssl from jar as all archs end up too big\n                     we add this back in docker -->\n                <dependency>\n                  <groupId>io.netty</groupId>\n                  <artifactId>netty-tcnative-boringssl-static</artifactId>\n                </dependency>\n\n                <!-- Unnecessary micrometer deps -->\n                <!-- TODO: https://github.com/micrometer-metrics/micrometer/issues/1599 -->\n<!--                <dependency>-->\n<!--                  &lt;!&ndash; only used by TimeWindowPercentileHistogram &ndash;&gt;-->\n<!--                  <groupId>org.hdrhistogram</groupId>-->\n<!--                  <artifactId>HdrHistogram</artifactId>-->\n<!--                </dependency>-->\n<!--                <dependency>-->\n<!--                  &lt;!&ndash; we don't use pause detector &ndash;&gt;-->\n<!--                  <groupId>org.latencyutils</groupId>-->\n<!--                  <artifactId>LatencyUtils</artifactId>-->\n<!--                </dependency>-->\n\n                <!-- storage and collectors which have 3rd party deps -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-storage-cassandra</artifactId>\n                </dependency>\n                <dependency>\n                  <groupId>io.zipkin.brave.cassandra</groupId>\n                  <artifactId>brave-instrumentation-cassandra-driver</artifactId>\n                </dependency>\n\n                <!-- MySQL backend -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-storage-mysql-v1</artifactId>\n                </dependency>\n\n                <!-- ActiveMQ Collector -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-collector-activemq</artifactId>\n                </dependency>\n\n                <!-- Kafka Collector -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-collector-kafka</artifactId>\n                </dependency>\n\n                <!-- RabbitMQ Collector -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-collector-rabbitmq</artifactId>\n                </dependency>\n\n                <!-- Scribe Collector -->\n                <dependency>\n                  <groupId>${project.groupId}.zipkin2</groupId>\n                  <artifactId>zipkin-collector-scribe</artifactId>\n                </dependency>\n                <exclude>\n                  <groupId>com.linecorp.armeria</groupId>\n                  <artifactId>armeria-thrift0.18</artifactId>\n                </exclude>\n              </excludes>\n            </configuration>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n</project>\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin/server/ZipkinServer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin.server;\n\nimport org.slf4j.bridge.SLF4JBridgeHandler;\nimport org.springframework.boot.SpringBootConfiguration;\nimport org.springframework.boot.autoconfigure.EnableAutoConfiguration;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport zipkin2.server.internal.EnableZipkinServer;\nimport zipkin2.server.internal.ZipkinActuatorImporter;\nimport zipkin2.server.internal.ZipkinModuleImporter;\nimport zipkin2.server.internal.banner.ZipkinBanner;\n\n/**\n * This adds the {@link EnableAutoConfiguration} annotation, but disables it by default to save\n * startup time.\n *\n * <p>Supported Zipkin modules like zipkin-gcp need to explicitly configure themselves.\n *\n * <p>For example, add the following to {@code src/main/resources/zipkin-server-stackdriver.yml}:\n * <pre>{@code\n * zipkin:\n *   internal:\n *     module:\n *       stackdriver: zipkin.module.storage.stackdriver.ZipkinStackdriverStorageModule\n * }</pre>\n */\n@SpringBootConfiguration\n@EnableAutoConfiguration\n@EnableZipkinServer\npublic class ZipkinServer {\n  static {\n    SLF4JBridgeHandler.removeHandlersForRootLogger();\n    SLF4JBridgeHandler.install();\n  }\n\n  public static void main(String[] args) {\n    new SpringApplicationBuilder(ZipkinServer.class)\n      .banner(new ZipkinBanner())\n      .initializers(new ZipkinModuleImporter(), new ZipkinActuatorImporter())\n      // Avoids potentially expensive DNS lookup and inaccurate startup timing\n      .logStartupInfo(false)\n      .properties(\n        EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY + \"=false\",\n        \"spring.config.name=zipkin-server\").run(args);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/BodyIsExceptionMessage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.annotation.ExceptionHandlerFunction;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.internal.ClosedComponentException;\n\nimport static com.linecorp.armeria.common.HttpStatus.BAD_REQUEST;\nimport static com.linecorp.armeria.common.HttpStatus.INTERNAL_SERVER_ERROR;\nimport static com.linecorp.armeria.common.MediaType.ANY_TEXT_TYPE;\n\nfinal class BodyIsExceptionMessage implements ExceptionHandlerFunction {\n  static final Logger LOGGER = LoggerFactory.getLogger(BodyIsExceptionMessage.class);\n  @Override\n  public HttpResponse handleException(ServiceRequestContext ctx, HttpRequest req, Throwable cause) {\n    if (req.method() == HttpMethod.POST && req.path().startsWith(\"/api/v\")) {\n      ZipkinHttpCollector.metrics.incrementMessagesDropped();\n    }\n\n    String message = cause.getMessage();\n    if (message == null) message = cause.getClass().getSimpleName();\n    if (cause instanceof IllegalArgumentException) {\n      return HttpResponse.of(BAD_REQUEST, ANY_TEXT_TYPE, message);\n    } else {\n      // Don't fill logs with exceptions about closed components.\n      if (!(cause instanceof ClosedComponentException)) {\n        LOGGER.warn(\"Unexpected error handling {} {}\", req.method(), req.path());\n      }\n\n      return HttpResponse.of(INTERNAL_SERVER_ERROR, ANY_TEXT_TYPE, message);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ConditionalOnSelfTracing.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.boot.autoconfigure.condition.ConditionOutcome;\nimport org.springframework.boot.autoconfigure.condition.SpringBootCondition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.annotation.AnnotationAttributes;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\n\n/**\n * This helps solve a number of problems including the following, which sometimes only break in an\n * alpine JRE. The solution is to go with pure properties instead.\n *\n * <pre>\n * <p>ConditionalOnClass(name = \"brave.Tracing\")\n * <p>ConditionalOnBean(Brave.class)\n * </pre>\n */\n@Conditional(ConditionalOnSelfTracing.SelfTracingCondition.class)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.TYPE, ElementType.METHOD})\npublic @interface ConditionalOnSelfTracing {\n  String storageType() default \"\";\n\n  class SelfTracingCondition extends SpringBootCondition {\n    static final boolean BRAVE_PRESENT = checkForBrave();\n\n    static boolean checkForBrave() {\n      try {\n        Class.forName(\"brave.Tracing\");\n        return true;\n      } catch (ClassNotFoundException e) {\n        return false;\n      }\n    }\n\n    @Override\n    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata a) {\n      if (!BRAVE_PRESENT) {\n        return ConditionOutcome.noMatch(\"Brave must be in the classpath\");\n      }\n\n      String selfTracingEnabled = context.getEnvironment()\n          .getProperty(\"zipkin.self-tracing.enabled\");\n\n      if (!Boolean.valueOf(selfTracingEnabled)) {\n        return ConditionOutcome.noMatch(\"zipkin.self-tracing.enabled isn't true\");\n      }\n\n      String expectedStorageType = AnnotationAttributes.fromMap(\n          a.getAnnotationAttributes(ConditionalOnSelfTracing.class.getName())\n      ).getString(\"storageType\");\n\n      if (expectedStorageType.isEmpty()) {\n        return ConditionOutcome.match();\n      }\n\n      String storageType = context.getEnvironment().getProperty(\"zipkin.storage.type\");\n      return expectedStorageType.equals(storageType) ?\n          ConditionOutcome.match() :\n          ConditionOutcome.noMatch(\n              \"zipkin.storage.type was: \" + storageType + \" expected \" + expectedStorageType);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ConditionalOnThrottledStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.boot.autoconfigure.condition.ConditionOutcome;\nimport org.springframework.boot.autoconfigure.condition.SpringBootCondition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\n\n@Conditional(ConditionalOnThrottledStorage.ThrottledStorageCondition.class)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.TYPE, ElementType.METHOD})\n@interface ConditionalOnThrottledStorage {\n  class ThrottledStorageCondition extends SpringBootCondition {\n    @Override\n    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata a) {\n      String throttleEnabled = context.getEnvironment()\n              .getProperty(\"zipkin.storage.throttle.enabled\");\n\n      if (!Boolean.valueOf(throttleEnabled)) {\n        return ConditionOutcome.noMatch(\"zipkin.storage.throttle.enabled isn't true\");\n      }\n\n      return ConditionOutcome.match();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/EnableZipkinServer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.context.annotation.Import;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import(InternalZipkinConfiguration.class)\npublic @interface EnableZipkinServer {\n\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/InternalZipkinConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.spring.ArmeriaAutoConfiguration;\nimport org.springframework.context.annotation.Import;\nimport zipkin2.server.internal.activemq.ZipkinActiveMQCollectorConfiguration;\nimport zipkin2.server.internal.brave.ZipkinSelfTracingConfiguration;\nimport zipkin2.server.internal.cassandra3.ZipkinCassandra3StorageConfiguration;\nimport zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageConfiguration;\nimport zipkin2.server.internal.eureka.ZipkinEurekaDiscoveryConfiguration;\nimport zipkin2.server.internal.health.ZipkinHealthController;\nimport zipkin2.server.internal.kafka.ZipkinKafkaCollectorConfiguration;\nimport zipkin2.server.internal.mysql.ZipkinMySQLStorageConfiguration;\nimport zipkin2.server.internal.prometheus.ZipkinMetricsController;\nimport zipkin2.server.internal.prometheus.ZipkinPrometheusMetricsConfiguration;\nimport zipkin2.server.internal.pulsar.ZipkinPulsarCollectorConfiguration;\nimport zipkin2.server.internal.rabbitmq.ZipkinRabbitMQCollectorConfiguration;\nimport zipkin2.server.internal.scribe.ZipkinScribeCollectorConfiguration;\nimport zipkin2.server.internal.ui.ZipkinUiConfiguration;\n\n@Import({\n  ArmeriaAutoConfiguration.class,\n  ZipkinConfiguration.class,\n  ZipkinHttpConfiguration.class,\n  ZipkinUiConfiguration.class,\n  ZipkinCassandra3StorageConfiguration.class,\n  ZipkinElasticsearchStorageConfiguration.class,\n  ZipkinMySQLStorageConfiguration.class,\n  ZipkinScribeCollectorConfiguration.class,\n  ZipkinSelfTracingConfiguration.class,\n  ZipkinQueryApiV2.class,\n  ZipkinHttpCollector.class,\n  ZipkinGrpcCollector.class,\n  ZipkinActiveMQCollectorConfiguration.class,\n  ZipkinKafkaCollectorConfiguration.class,\n  ZipkinRabbitMQCollectorConfiguration.class, \n  ZipkinPulsarCollectorConfiguration.class,\n  ZipkinMetricsController.class,\n  ZipkinHealthController.class,\n  ZipkinPrometheusMetricsConfiguration.class,\n  ZipkinEurekaDiscoveryConfiguration.class,\n})\npublic class InternalZipkinConfiguration {\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/JsonUtil.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.util.DefaultIndenter;\nimport com.fasterxml.jackson.core.util.DefaultPrettyPrinter;\nimport java.io.IOException;\nimport java.io.Writer;\n\n/**\n * Utilities for working with JSON.\n */\npublic final class JsonUtil {\n\n  static final JsonFactory JSON_FACTORY = new JsonFactory();\n  static final DefaultPrettyPrinter.Indenter TWOSPACES_LF_INDENTER =\n    new DefaultIndenter(\"  \", \"\\n\");\n\n  /**\n   * Creates a new {@link JsonGenerator} with pretty-printing enabled forcing {@code '\\n'}\n   * between lines, as opposed to Jackson's default which uses the system line separator.\n   */\n  public static JsonGenerator createGenerator(Writer writer) throws IOException {\n    JsonGenerator generator = JSON_FACTORY.createGenerator(writer);\n    DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();\n    prettyPrinter.indentArraysWith(TWOSPACES_LF_INDENTER);\n    prettyPrinter.indentObjectsWith(TWOSPACES_LF_INDENTER);\n    generator.setPrettyPrinter(prettyPrinter);\n    return generator;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/MicrometerCollectorMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.internal.Nullable;\n\n/**\n * This is a simple metric service that exports the following to the \"/metrics\" endpoint:\n *\n * <pre>\n * <ul>\n *     <li>counter.zipkin_collector.messages.$transport - cumulative messages received; should\n * relate to messages reported by instrumented apps</li>\n *     <li>counter.zipkin_collector.messages_dropped.$transport - cumulative messages dropped;\n * reasons include client disconnects or malformed content</li>\n *     <li>counter.zipkin_collector.bytes.$transport - cumulative message bytes</li>\n *     <li>counter.zipkin_collector.spans.$transport - cumulative spans read; should relate to\n * messages reported by instrumented apps</li>\n *     <li>counter.zipkin_collector.spans_dropped.$transport - cumulative spans dropped; reasons\n * include sampling or storage failures</li>\n *     <li>gauge.zipkin_collector.message_spans.$transport - last count of spans in a message</li>\n *     <li>gauge.zipkin_collector.message_bytes.$transport - last count of bytes in a message</li>\n * </ul>\n * </pre>\n *\n * See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html\n */\npublic final class MicrometerCollectorMetrics implements CollectorMetrics {\n  final MeterRegistry registryInstance;\n  final Counter messages, messagesDropped, bytes, spans, spansDropped;\n  final AtomicInteger messageBytes, messageSpans;\n\n  public MicrometerCollectorMetrics(MeterRegistry registry) {\n    this(null, registry);\n  }\n\n  MicrometerCollectorMetrics(@Nullable String transport, MeterRegistry meterRegistry) {\n    this.registryInstance = meterRegistry;\n    if (transport == null) {\n      messages = messagesDropped = bytes = spans = spansDropped = null;\n      messageBytes = messageSpans = null;\n      return;\n    }\n    this.messages =\n      Counter.builder(\"zipkin_collector.messages\")\n        .description(\"cumulative amount of messages received\")\n        .tag(\"transport\", transport)\n        .register(registryInstance);\n    this.messagesDropped =\n      Counter.builder(\"zipkin_collector.messages_dropped\")\n        .description(\"cumulative amount of messages received that were later dropped\")\n        .tag(\"transport\", transport)\n        .register(registryInstance);\n\n    this.bytes =\n        Counter.builder(\"zipkin_collector.bytes\")\n            .description(\"cumulative amount of bytes received\")\n            .tag(\"transport\", transport)\n            .baseUnit(\"bytes\")\n            .register(registryInstance);\n    this.spans =\n        Counter.builder(\"zipkin_collector.spans\")\n            .description(\"cumulative amount of spans received\")\n            .tag(\"transport\", transport)\n            .register(registryInstance);\n    this.spansDropped =\n        Counter.builder(\"zipkin_collector.spans_dropped\")\n            .description(\"cumulative amount of spans received that were later dropped\")\n            .tag(\"transport\", transport)\n            .register(registryInstance);\n\n    this.messageSpans = new AtomicInteger(0);\n    Gauge.builder(\"zipkin_collector.message_spans\", messageSpans, AtomicInteger::get)\n        .description(\"count of spans per message\")\n        .tag(\"transport\", transport)\n        .register(registryInstance);\n    this.messageBytes = new AtomicInteger(0);\n    Gauge.builder(\"zipkin_collector.message_bytes\", messageBytes, AtomicInteger::get)\n        .description(\"size of a message containing serialized spans\")\n        .tag(\"transport\", transport)\n        .baseUnit(\"bytes\")\n        .register(registryInstance);\n  }\n\n  @Override\n  public MicrometerCollectorMetrics forTransport(String transportType) {\n    if (transportType == null) throw new NullPointerException(\"transportType == null\");\n    return new MicrometerCollectorMetrics(transportType, registryInstance);\n  }\n\n  @Override\n  public void incrementMessages() {\n    checkScoped();\n    messages.increment();\n  }\n\n  @Override\n  public void incrementMessagesDropped() {\n    checkScoped();\n    messagesDropped.increment();\n  }\n\n  @Override\n  public void incrementSpans(int quantity) {\n    checkScoped();\n    messageSpans.set(quantity);\n    spans.increment(quantity);\n  }\n\n  @Override\n  public void incrementBytes(int quantity) {\n    checkScoped();\n    messageBytes.set(quantity);\n    bytes.increment(quantity);\n  }\n\n  @Override\n  public void incrementSpansDropped(int quantity) {\n    checkScoped();\n    spansDropped.increment(quantity);\n  }\n\n  void checkScoped() {\n    if (messages == null) {\n      throw new IllegalStateException(\"always scope with ActuateCollectorMetrics.forTransport\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/WrappingExecutorService.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\n/** Used to implement a context propagating executor service which wraps tasks */\n// copy/pasted from Brave\npublic abstract class WrappingExecutorService implements ExecutorService {\n  protected WrappingExecutorService() {\n  }\n\n  protected abstract ExecutorService delegate();\n\n  protected abstract <C> Callable<C> wrap(Callable<C> task);\n\n  protected abstract Runnable wrap(Runnable task);\n\n  @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {\n    return delegate().awaitTermination(timeout, unit);\n  }\n\n  @Override  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)\n      throws InterruptedException {\n    return delegate().invokeAll(wrap(tasks));\n  }\n\n  @Override public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,\n      TimeUnit unit) throws InterruptedException {\n    return delegate().invokeAll(wrap(tasks), timeout, unit);\n  }\n\n  @Override public <T> T invokeAny(Collection<? extends Callable<T>> tasks)\n      throws InterruptedException, ExecutionException {\n    return delegate().invokeAny(wrap(tasks));\n  }\n\n  @Override  public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)\n      throws InterruptedException, ExecutionException, TimeoutException {\n    return delegate().invokeAny(wrap(tasks), timeout, unit);\n  }\n\n  @Override public boolean isShutdown() {\n    return delegate().isShutdown();\n  }\n\n  @Override public boolean isTerminated() {\n    return delegate().isTerminated();\n  }\n\n  @Override public void shutdown() {\n    delegate().shutdown();\n  }\n\n  @Override public List<Runnable> shutdownNow() {\n    return delegate().shutdownNow();\n  }\n\n  @Override public void execute(Runnable task) {\n    delegate().execute(wrap(task));\n  }\n\n  @Override public <T> Future<T> submit(Callable<T> task) {\n    return delegate().submit(wrap(task));\n  }\n\n  @Override public Future<?> submit(Runnable task) {\n    return delegate().submit(wrap(task));\n  }\n\n  @Override public <T> Future<T> submit(Runnable task, T result) {\n    return delegate().submit(wrap(task), result);\n  }\n\n  <T> Collection<? extends Callable<T>> wrap(Collection<? extends Callable<T>> tasks) {\n    ArrayList<Callable<T>> result = new ArrayList<>(tasks.size());\n    for (Callable<T> task : tasks) {\n      result.add(wrap(task));\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinActuatorImporter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.util.Arrays;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.context.ApplicationContextInitializer;\nimport org.springframework.context.annotation.ImportSelector;\nimport org.springframework.context.support.GenericApplicationContext;\nimport org.springframework.core.env.ConfigurableEnvironment;\n\n/**\n * When auto-configuration is enabled, actuator and all of its subordinate endpoints such as {@code\n * BeansEndpointAutoConfiguration} load, subject to further conditions like {@code\n * management.endpoint.health.enabled=false}. When auto-configuration is disabled, these\n * configuration discovered indirectly via {@code META-INF/spring.factories} are no longer loaded.\n * This type helps load the actuator functionality we currently support without a compilation\n * dependency on actuator, and without relying on auto-configuration being enabled.\n *\n * <p><h3>Implementation note</h3>\n * <p>It may be possible to re-implement this as {@link ImportSelector} to provide {@link\n * #ACTUATOR_IMPL_CLASS} and the endpoint configuration types from {@link\n * #PROPERTY_NAME_ACTUATOR_INCLUDE}.\n */\n// look at RATIONALE.md and update if relevant when changing this file\npublic final class ZipkinActuatorImporter\n  implements ApplicationContextInitializer<GenericApplicationContext> {\n  static final Logger LOG = LoggerFactory.getLogger(ZipkinActuatorImporter.class);\n\n  static final String ACTUATOR_IMPL_CLASS =\n    \"com.linecorp.armeria.spring.actuate.ArmeriaSpringActuatorAutoConfiguration\";\n  static final String PROPERTY_NAME_ACTUATOR_ENABLED = \"zipkin.internal.actuator.enabled\";\n  static final String PROPERTY_NAME_ACTUATOR_INCLUDE = \"zipkin.internal.actuator.include\";\n\n  final String actuatorImplClass;\n\n  public ZipkinActuatorImporter() {\n    this(ACTUATOR_IMPL_CLASS);\n  }\n\n  ZipkinActuatorImporter(String actuatorImplClass) { // visible for testing\n    this.actuatorImplClass = actuatorImplClass;\n  }\n\n  @Override public void initialize(GenericApplicationContext context) {\n    ConfigurableEnvironment env = context.getEnvironment();\n    if (\"false\".equalsIgnoreCase(env.getProperty(PROPERTY_NAME_ACTUATOR_ENABLED))) {\n      LOG.debug(\"skipping actuator as it is disabled\");\n      return;\n    }\n\n    // At this point in the life-cycle, env can directly resolve plain properties, like the boolean\n    // above. If you tried to resolve a property bound by a yaml list, it returns null, as they are\n    // not yet bound.\n    //\n    // As we are in a configurable environment, we can bind lists properties. We expect this to take\n    // includes from PROPERTY_NAME_ACTUATOR_INCLUDE yaml path of zipkin-server-shared.yml.\n    String[] includes =\n      Binder.get(env).bind(PROPERTY_NAME_ACTUATOR_INCLUDE, String[].class).orElse(null);\n    if (includes == null || includes.length == 0) {\n      LOG.debug(\"no actuator configuration found under path {}\", PROPERTY_NAME_ACTUATOR_INCLUDE);\n      return;\n    }\n\n    LOG.debug(\"attempting to load actuator configuration: {}\", Arrays.toString(includes));\n    try {\n      context.registerBean(Class.forName(actuatorImplClass));\n    } catch (Exception e) {\n      LOG.debug(\"skipping actuator as implementation is not available\", e);\n      return;\n    }\n\n    for (String include : includes) {\n      try {\n        context.registerBean(Class.forName(include));\n      } catch (Exception e) {\n        // Skip any classes that didn't match due to drift\n        LOG.debug(\"skipping unloadable actuator config {}\", include, e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport brave.Tracing;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport java.util.List;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.beans.factory.BeanFactoryAware;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.server.internal.brave.TracingStorageComponent;\nimport zipkin2.server.internal.throttle.ThrottledStorageComponent;\nimport zipkin2.server.internal.throttle.ZipkinStorageThrottleProperties;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.StorageComponent;\n\n/** Base collector and storage configurations needed for higher-level integrations */\n@Import({\n  ZipkinConfiguration.InMemoryConfiguration.class,\n  ZipkinConfiguration.ThrottledStorageComponentEnhancer.class,\n  ZipkinConfiguration.TracingStorageComponentEnhancer.class\n})\npublic class ZipkinConfiguration {\n\n  @Bean CollectorSampler traceIdSampler(@Value(\"${zipkin.collector.sample-rate:1.0}\") float rate) {\n    return CollectorSampler.create(rate);\n  }\n\n  @Bean CollectorMetrics metrics(MeterRegistry registry) {\n    return new MicrometerCollectorMetrics(registry);\n  }\n\n  @EnableConfigurationProperties(ZipkinStorageThrottleProperties.class)\n  @ConditionalOnThrottledStorage\n  static class ThrottledStorageComponentEnhancer implements BeanPostProcessor, BeanFactoryAware {\n\n    /**\n     * Need this to resolve cyclic instantiation issue with spring when instantiating with metrics\n     * and tracing.\n     *\n     * <p>Ref: <a href=\"https://stackoverflow.com/a/19688634\">Tracking down cause of Spring's \"not\n     * eligible for auto-proxying\"</a></p>\n     */\n    BeanFactory beanFactory;\n\n    @Override public Object postProcessAfterInitialization(Object bean, String beanName) {\n      if (bean instanceof StorageComponent component) {\n        ZipkinStorageThrottleProperties throttleProperties =\n          beanFactory.getBean(ZipkinStorageThrottleProperties.class);\n        return new ThrottledStorageComponent(component,\n          beanFactory.getBean(MeterRegistry.class),\n          beanFactory.containsBean(\"tracing\") ? beanFactory.getBean(Tracing.class) : null,\n          throttleProperties.getMinConcurrency(),\n          throttleProperties.getMaxConcurrency(),\n          throttleProperties.getMaxQueueSize());\n      }\n      return bean;\n    }\n\n    @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException {\n      this.beanFactory = beanFactory;\n    }\n  }\n\n  @ConditionalOnSelfTracing\n  static class TracingStorageComponentEnhancer implements BeanPostProcessor, BeanFactoryAware {\n    /**\n     * Need this to resolve cyclic instantiation issue with spring when instantiating with tracing.\n     *\n     * <p>Ref: <a href=\"https://stackoverflow.com/a/19688634\">Tracking down cause of Spring's \"not\n     * eligible for auto-proxying\"</a></p>\n     */\n    BeanFactory beanFactory;\n\n    @Override public Object postProcessBeforeInitialization(Object bean, String beanName) {\n      return bean;\n    }\n\n    @Override public Object postProcessAfterInitialization(Object bean, String beanName) {\n      if (bean instanceof StorageComponent component && beanFactory.containsBean(\"tracing\")) {\n        Tracing tracing = beanFactory.getBean(Tracing.class);\n        return new TracingStorageComponent(tracing, component);\n      }\n      return bean;\n    }\n\n    @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException {\n      this.beanFactory = beanFactory;\n    }\n  }\n\n  /**\n   * This is a special-case configuration if there's no StorageComponent of any kind. In-Mem can\n   * supply both read apis, so we add two beans here.\n   */\n  @Conditional(StorageTypeMemAbsentOrEmpty.class)\n  @ConditionalOnMissingBean(StorageComponent.class)\n  static class InMemoryConfiguration {\n    @Bean\n    StorageComponent storage(\n      @Value(\"${zipkin.storage.strict-trace-id:true}\") boolean strictTraceId,\n      @Value(\"${zipkin.storage.search-enabled:true}\") boolean searchEnabled,\n      @Value(\"${zipkin.storage.mem.max-spans:500000}\") int maxSpans,\n      @Value(\"${zipkin.storage.autocomplete-keys:}\") List<String> autocompleteKeys) {\n      return InMemoryStorage.newBuilder()\n        .strictTraceId(strictTraceId)\n        .searchEnabled(searchEnabled)\n        .maxSpanCount(maxSpans)\n        .autocompleteKeys(autocompleteKeys)\n        .build();\n    }\n  }\n\n  static final class StorageTypeMemAbsentOrEmpty implements Condition {\n    @Override\n    public boolean matches(ConditionContext condition, AnnotatedTypeMetadata ignored) {\n      String storageType = condition.getEnvironment().getProperty(\"zipkin.storage.type\");\n      if (storageType == null) return true;\n      storageType = storageType.trim();\n      if (storageType.isEmpty()) return true;\n      return storageType.equals(\"mem\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinGrpcCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.grpc.protocol.AbstractUnsafeUnaryGrpcService;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport zipkin2.Callback;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.StorageComponent;\n\n/** Collector for receiving spans on a gRPC endpoint. */\n@ConditionalOnProperty(name = \"zipkin.collector.grpc.enabled\", matchIfMissing = true)\nfinal class ZipkinGrpcCollector {\n\n  @Bean ArmeriaServerConfigurator grpcCollectorConfigurator(StorageComponent storage,\n    CollectorSampler sampler, CollectorMetrics metrics) {\n    CollectorMetrics grpcMetrics = metrics.forTransport(\"grpc\");\n    Collector collector = Collector.newBuilder(getClass())\n      .storage(storage)\n      .sampler(sampler)\n      .metrics(grpcMetrics)\n      .build();\n\n    return sb ->\n      sb.service(\"/zipkin.proto3.SpanService/Report\", new SpanService(collector, grpcMetrics));\n  }\n\n  static final class SpanService extends AbstractUnsafeUnaryGrpcService {\n\n    final Collector collector;\n    final CollectorMetrics metrics;\n\n    SpanService(Collector collector, CollectorMetrics metrics) {\n      this.collector = collector;\n      this.metrics = metrics;\n    }\n\n    @Override\n    protected CompletionStage<ByteBuf> handleMessage(ServiceRequestContext ctx, ByteBuf bytes) {\n      metrics.incrementMessages();\n      metrics.incrementBytes(bytes.readableBytes());\n\n      if (!bytes.isReadable()) {\n        return CompletableFuture.completedFuture(bytes); // lenient on empty messages\n      }\n\n      try {\n        CompletableFutureCallback result = new CompletableFutureCallback();\n        collector.acceptSpans(bytes.nioBuffer(), SpanBytesDecoder.PROTO3, result, ctx.blockingTaskExecutor());\n        return result;\n      } finally {\n        bytes.release();\n      }\n    }\n  }\n\n  static final class CompletableFutureCallback extends CompletableFuture<ByteBuf>\n    implements Callback<Void> {\n\n    @Override public void onSuccess(Void value) {\n      complete(Unpooled.EMPTY_BUFFER);\n    }\n\n    @Override public void onError(Throwable t) {\n      completeExceptionally(t);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.client.encoding.StreamDecoderFactory;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.annotation.Consumes;\nimport com.linecorp.armeria.server.annotation.ConsumesJson;\nimport com.linecorp.armeria.server.annotation.ExceptionHandler;\nimport com.linecorp.armeria.server.annotation.Post;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.nio.ByteBuffer;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.Executor;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport zipkin2.Callback;\nimport zipkin2.Span;\nimport zipkin2.SpanBytesDecoderDetector;\nimport zipkin2.codec.BytesDecoder;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.Collector;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.StorageComponent;\n\nimport static zipkin2.Call.propagateIfFatal;\n\n@ConditionalOnProperty(name = \"zipkin.collector.http.enabled\", matchIfMissing = true)\n@ExceptionHandler(BodyIsExceptionMessage.class)\npublic class ZipkinHttpCollector {\n  static final Logger LOGGER = LoggerFactory.getLogger(ZipkinHttpCollector.class);\n  static volatile CollectorMetrics metrics;\n  final Collector collector;\n\n  @SuppressWarnings(\"StaticAssignmentInConstructor\")\n  ZipkinHttpCollector(\n    StorageComponent storage, CollectorSampler sampler, CollectorMetrics metrics) {\n    metrics = metrics.forTransport(\"http\");\n    collector =\n      Collector.newBuilder(getClass()).storage(storage).sampler(sampler).metrics(metrics).build();\n    ZipkinHttpCollector.metrics = metrics; // converter instances aren't injected by Spring\n  }\n\n  @Post(\"/api/v2/spans\")\n  public HttpResponse uploadSpans(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.JSON_V2, ctx, req);\n  }\n\n  @Post(\"/api/v2/spans\")\n  @ConsumesJson\n  public HttpResponse uploadSpansJson(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.JSON_V2, ctx, req);\n  }\n\n  @Post(\"/api/v2/spans\")\n  @ConsumesProtobuf\n  public HttpResponse uploadSpansProtobuf(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.PROTO3, ctx, req);\n  }\n\n  @Post(\"/api/v1/spans\")\n  public HttpResponse uploadSpansV1(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.JSON_V1, ctx, req);\n  }\n\n  @Post(\"/api/v1/spans\")\n  @ConsumesJson\n  public HttpResponse uploadSpansV1Json(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.JSON_V1, ctx, req);\n  }\n\n  @Post(\"/api/v1/spans\")\n  @ConsumesThrift\n  public HttpResponse uploadSpansV1Thrift(ServiceRequestContext ctx, HttpRequest req) {\n    return validateAndStoreSpans(SpanBytesDecoder.THRIFT, ctx, req);\n  }\n\n  /** This synchronously decodes the message so that users can see data errors. */\n  @SuppressWarnings(\"FutureReturnValueIgnored\")\n  // TODO: errorprone wants us to check this future before returning, but what would be a sensible\n  // check? Say it is somehow canceled, would we take action? Would callback.onError() be redundant?\n  HttpResponse validateAndStoreSpans(SpanBytesDecoder decoder, ServiceRequestContext ctx,\n    HttpRequest req) {\n    CompletableCallback result = new CompletableCallback();\n\n    req.aggregateWithPooledObjects(ctx.eventLoop(), ctx.alloc()).handle((msg, t) -> {\n      if (t != null) {\n        result.onError(t);\n        return null;\n      }\n\n      final HttpData requestContent;\n      try {\n        requestContent = UnzippingBytesRequestConverter.convertRequest(ctx, msg);\n      } catch (Throwable t1) {\n        propagateIfFatal(t1);\n        result.onError(t1);\n        return null;\n      }\n\n      try (HttpData content = requestContent) {\n        // logging already handled upstream in UnzippingBytesRequestConverter where request context exists\n        if (content.isEmpty()) {\n          result.onSuccess(null);\n          return null;\n        }\n\n        final ByteBuffer nioBuffer = content.byteBuf().nioBuffer();\n\n        try {\n          SpanBytesDecoderDetector.decoderForListMessage(nioBuffer);\n        } catch (IllegalArgumentException e) {\n          result.onError(new IllegalArgumentException(\"Expected a \" + decoder + \" encoded list\\n\"));\n          return null;\n        } catch (Throwable t1) {\n          result.onError(t1);\n          return null;\n        }\n\n        SpanBytesDecoder unexpectedDecoder = testForUnexpectedFormat(decoder, nioBuffer);\n        if (unexpectedDecoder != null) {\n          result.onError(new IllegalArgumentException(\n            \"Expected a \" + decoder + \" encoded list, but received: \" + unexpectedDecoder + \"\\n\"));\n          return null;\n        }\n\n        // collector.accept might block so need to move off the event loop. We make sure the\n        // callback is context aware to continue the trace.\n        Executor executor = ctx.makeContextAware(ctx.blockingTaskExecutor());\n        try {\n          collector.acceptSpans(nioBuffer, decoder, result, executor);\n        } catch (Throwable t1) {\n          result.onError(t1);\n          return null;\n        }\n      }\n\n      return null;\n    });\n\n    return HttpResponse.from(result);\n  }\n\n  static void maybeLog(String prefix, ServiceRequestContext ctx, AggregatedHttpRequest request) {\n    if (!LOGGER.isDebugEnabled()) return;\n    LOGGER.debug(\"{} sent by clientAddress->{}, userAgent->{}\",\n      prefix, ctx.clientAddress(), request.headers().get(HttpHeaderNames.USER_AGENT)\n    );\n  }\n\n  /**\n   * Some formats clash on partial data. For example, a v1 and v2 span is identical if only the span\n   * name is sent. This looks for unexpected data format.\n   */\n  static SpanBytesDecoder testForUnexpectedFormat(BytesDecoder<Span> decoder, ByteBuffer body) {\n    if (decoder == SpanBytesDecoder.JSON_V2) {\n      if (contains(body, BINARY_ANNOTATION_FIELD_SUFFIX)) {\n        return SpanBytesDecoder.JSON_V1;\n      }\n    } else if (decoder == SpanBytesDecoder.JSON_V1) {\n      if (contains(body, ENDPOINT_FIELD_SUFFIX) || contains(body, TAGS_FIELD)) {\n        return SpanBytesDecoder.JSON_V2;\n      }\n    }\n    return null;\n  }\n\n  static final byte[] BINARY_ANNOTATION_FIELD_SUFFIX =\n    {'y', 'A', 'n', 'n', 'o', 't', 'a', 't', 'i', 'o', 'n', 's', '\"'};\n  // copy-pasted from SpanBytesDecoderDetector, to avoid making it public\n  static final byte[] ENDPOINT_FIELD_SUFFIX = {'E', 'n', 'd', 'p', 'o', 'i', 'n', 't', '\"'};\n  static final byte[] TAGS_FIELD = {'\"', 't', 'a', 'g', 's', '\"'};\n\n  static boolean contains(ByteBuffer bytes, byte[] subsequence) {\n    bytes:\n    for (int i = 0; i < bytes.remaining() - subsequence.length + 1; i++) {\n      for (int j = 0; j < subsequence.length; j++) {\n        if (bytes.get(bytes.position() + i + j) != subsequence[j]) {\n          continue bytes;\n        }\n      }\n      return true;\n    }\n    return false;\n  }\n}\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.TYPE, ElementType.METHOD})\n@Consumes(\"application/x-thrift\") @interface ConsumesThrift {\n}\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.TYPE, ElementType.METHOD})\n@Consumes(\"application/x-protobuf\") @interface ConsumesProtobuf {\n}\n\nfinal class CompletableCallback extends CompletableFuture<HttpResponse>\n  implements Callback<Void> {\n\n  static final ResponseHeaders ACCEPTED_RESPONSE = ResponseHeaders.of(HttpStatus.ACCEPTED);\n\n  @Override public void onSuccess(Void value) {\n    complete(HttpResponse.of(ACCEPTED_RESPONSE));\n  }\n\n  @Override public void onError(Throwable t) {\n    completeExceptionally(t);\n  }\n}\n\nfinal class UnzippingBytesRequestConverter {\n\n  static HttpData convertRequest(ServiceRequestContext ctx, AggregatedHttpRequest request) {\n    ZipkinHttpCollector.metrics.incrementMessages();\n    String encoding = request.headers().get(HttpHeaderNames.CONTENT_ENCODING);\n    HttpData content = request.content();\n    if (!content.isEmpty() && encoding != null && encoding.contains(\"gzip\")) {\n      content = StreamDecoderFactory.gzip().newDecoder(ctx.alloc()).decode(content);\n      // The implementation of the armeria decoder is to return an empty body on failure\n      if (content.isEmpty()) {\n        ZipkinHttpCollector.maybeLog(\"Malformed gzip body\", ctx, request);\n        content.close();\n        throw new IllegalArgumentException(\"Cannot gunzip spans\");\n      }\n    }\n\n    if (content.isEmpty()) ZipkinHttpCollector.maybeLog(\"Empty POST body\", ctx, request);\n    if (content.length() == 2 && \"[]\".equals(content.toStringAscii())) {\n      ZipkinHttpCollector.maybeLog(\"Empty JSON list POST body\", ctx, request);\n      content.close();\n      content = HttpData.empty();\n    }\n\n    ZipkinHttpCollector.metrics.incrementBytes(content.length());\n    return content;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.cors.CorsService;\nimport com.linecorp.armeria.server.cors.CorsServiceBuilder;\nimport com.linecorp.armeria.server.file.HttpFile;\nimport com.linecorp.armeria.server.metric.PrometheusExpositionService;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport io.prometheus.client.CollectorRegistry;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.core.io.Resource;\nimport zipkin2.server.internal.health.ZipkinHealthController;\nimport zipkin2.server.internal.prometheus.ZipkinMetricsController;\n\n@Configuration(proxyBeanMethods = false)\npublic class ZipkinHttpConfiguration {\n  public static final MediaType MEDIA_TYPE_ACTUATOR =\n    MediaType.parse(\"application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\");\n\n  @Bean ArmeriaServerConfigurator serverConfigurator(\n    Optional<ZipkinQueryApiV2> httpQuery,\n    Optional<ZipkinHttpCollector> httpCollector,\n    Optional<ZipkinHealthController> healthController,\n    Optional<ZipkinMetricsController> metricsController,\n    Optional<CollectorRegistry> collectorRegistry,\n    @Value(\"classpath:info.json\") Resource info,\n    @Value(\"${zipkin.query.timeout:11s}\") Duration queryTimeout) throws IOException {\n    HttpData infoData = HttpData.wrap(info.getContentAsByteArray());\n    return sb -> {\n      httpQuery.ifPresent(h -> {\n        Function<HttpService, HttpService>\n          timeoutDecorator = service -> (ctx, req) -> {\n          ctx.setRequestTimeout(queryTimeout);\n          return service.serve(ctx, req);\n        };\n        sb.annotatedService(httpQuery.get(), timeoutDecorator);\n        sb.annotatedService(\"/zipkin\", httpQuery.get(), timeoutDecorator); // For UI.\n      });\n      httpCollector.ifPresent(sb::annotatedService);\n      healthController.ifPresent(sb::annotatedService);\n      metricsController.ifPresent(sb::annotatedService);\n      collectorRegistry.ifPresent(registry -> {\n        PrometheusExpositionService prometheusService = new PrometheusExpositionService(registry);\n        sb.service(\"/actuator/prometheus\", prometheusService);\n        sb.service(\"/prometheus\", prometheusService);\n      });\n\n      // Directly implement info endpoint, but use different content type for the /actuator path\n      sb.service(\"/actuator/info\", infoService(infoData, MEDIA_TYPE_ACTUATOR));\n      sb.service(\"/info\", infoService(infoData, MediaType.JSON_UTF_8));\n\n      // It's common for backend requests to have timeouts of the magic number 10s, so we go ahead\n      // and default to a slightly longer timeout on the server to be able to handle these with\n      // better error messages where possible.\n      sb.requestTimeout(Duration.ofSeconds(11));\n\n      // Block TRACE requests because https://github.com/openzipkin/zipkin/issues/2286\n      sb.routeDecorator().trace(\"prefix:/\")\n        .build((delegate, ctx, req) -> HttpResponse.of(HttpStatus.METHOD_NOT_ALLOWED));\n    };\n  }\n\n  /** Configures the server at the last because of the specified {@link Order} annotation. */\n  @Order @Bean ArmeriaServerConfigurator corsConfigurator(\n    @Value(\"${zipkin.query.allowed-origins:*}\") String allowedOrigins) {\n    CorsServiceBuilder corsBuilder = CorsService.builder(allowedOrigins.split(\",\"))\n      // NOTE: The property says query, and the UI does not use POST, but we allow POST?\n      //\n      // The reason is that our former CORS implementation accidentally allowed POST. People doing\n      // browser-based tracing relied on this, so we can't remove it by default. In the future, we\n      // could split the collector's CORS policy into a different property, still allowing POST\n      // with content-type by default.\n      .allowRequestMethods(HttpMethod.GET, HttpMethod.POST)\n      .allowRequestHeaders(HttpHeaderNames.CONTENT_TYPE,\n        // Use literals to avoid a runtime dependency on armeria-grpc types\n        HttpHeaderNames.of(\"X-GRPC-WEB\"))\n      .exposeHeaders(\"grpc-status\", \"grpc-message\", \"armeria.grpc.ThrowableProto-bin\");\n    return builder -> builder.decorator(corsBuilder::build);\n  }\n\n  HttpService infoService(HttpData info, MediaType mediaType) {\n    return HttpFile.builder(info)\n      .contentType(mediaType)\n      .build()\n      .asService();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinModuleImporter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.context.ApplicationContextInitializer;\nimport org.springframework.context.annotation.ImportSelector;\nimport org.springframework.context.support.GenericApplicationContext;\nimport org.springframework.core.env.ConfigurableEnvironment;\n\n/**\n * This loads configuration needed for modules like zipkin-aws, but without relying on\n * AutoConfiguration via spring.factories. Instead, this uses a configuration path defined in our\n * yaml here and any profiles loaded by the modules themselves.\n *\n * <p>To use this, move autoconfiguration values from {@code src/main/resources/META-INF/spring.factories}\n * to map entries under the yaml path {@link #PROPERTY_NAME_MODULE}.\n *\n * <p>For example, add the following to {@code src/main/resources/zipkin-server-stackdriver.yml}:\n * <pre>{@code\n * zipkin:\n *   internal:\n *     module:\n *       stackdriver: zipkin.autoconfigure.storage.stackdriver.ZipkinStackdriverModule\n * }</pre>\n *\n * <p><h3>Implementation note</h3>*\n * <p>It may be possible to re-implement this as {@link ImportSelector} to provide configuration\n * types from {@link #PROPERTY_NAME_MODULE}.\n */\n// look at RATIONALE.md and update if relevant when changing this file\npublic final class ZipkinModuleImporter implements ApplicationContextInitializer<GenericApplicationContext> {\n  static final Logger LOG = LoggerFactory.getLogger(ZipkinModuleImporter.class);\n\n  static final String PROPERTY_NAME_MODULE = \"zipkin.internal.module\";\n\n  @Override public void initialize(GenericApplicationContext context) {\n    ConfigurableEnvironment env = context.getEnvironment();\n\n    // At this point in the life-cycle, env can directly resolve plain properties, like the boolean\n    // above. If you tried to resolve a property bound by a yaml map, it returns null, as they are\n    // not yet bound.\n    //\n    // As we are in a configurable environment, we can bind lists properties. We expect this to take\n    // includes from PROPERTY_NAME_MODULE yaml path from all modules.\n    Map<String, String> modules =\n      Binder.get(env).bind(PROPERTY_NAME_MODULE, Map.class).orElse(null);\n    if (modules == null || modules.isEmpty()) {\n      LOG.debug(\"no modules found under path {}\", PROPERTY_NAME_MODULE);\n      return;\n    }\n\n    LOG.debug(\"attempting to load modules: {}\", modules.keySet());\n    for (Map.Entry<String, String> module : modules.entrySet()) {\n      try {\n        context.registerBean(Class.forName(module.getValue()));\n      } catch (Exception e) {\n        // Skip any classes that didn't match due to drift\n        LOG.debug(\"skipping unloadable module {}\", module.getKey(), e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ZipkinQueryApiV2.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.common.ResponseHeadersBuilder;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.annotation.Blocking;\nimport com.linecorp.armeria.server.annotation.Default;\nimport com.linecorp.armeria.server.annotation.ExceptionHandler;\nimport com.linecorp.armeria.server.annotation.Get;\nimport com.linecorp.armeria.server.annotation.Param;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.buffer.ByteBufOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.UncheckedIOException;\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.codec.DependencyLinkBytesEncoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.linecorp.armeria.common.HttpHeaderNames.CACHE_CONTROL;\nimport static com.linecorp.armeria.common.HttpStatus.BAD_REQUEST;\nimport static com.linecorp.armeria.common.HttpStatus.NOT_FOUND;\nimport static com.linecorp.armeria.common.MediaType.ANY_TEXT_TYPE;\n\n@ConditionalOnProperty(name = \"zipkin.query.enabled\", matchIfMissing = true)\n@ExceptionHandler(BodyIsExceptionMessage.class)\npublic class ZipkinQueryApiV2 {\n  final String storageType;\n  final StorageComponent storage; // don't cache spanStore here as it can cause the app to crash!\n  final long defaultLookback;\n  /**\n   * The Cache-Control max-age (seconds) for /api/v2/services /api/v2/remoteServices and\n   * /api/v2/spans\n   */\n  final int namesMaxAge;\n  final List<String> autocompleteKeys;\n\n  volatile int serviceCount; // used as a threshold to start returning cache-control headers\n\n  ZipkinQueryApiV2(\n    StorageComponent storage,\n    @Value(\"${zipkin.storage.type:mem}\") String storageType,\n    @Value(\"${zipkin.query.lookback:86400000}\") long defaultLookback, // 1 day in millis\n    @Value(\"${zipkin.query.names-max-age:300}\") int namesMaxAge, // 5 minutes\n    @Value(\"${zipkin.storage.autocomplete-keys:}\") List<String> autocompleteKeys\n  ) {\n    this.storage = storage;\n    this.storageType = storageType;\n    this.defaultLookback = defaultLookback;\n    this.namesMaxAge = namesMaxAge;\n    this.autocompleteKeys = autocompleteKeys;\n  }\n\n  @Get(\"/api/v2/dependencies\")\n  @Blocking\n  public AggregatedHttpResponse getDependencies(\n    @Param(\"endTs\") long endTs,\n    @Param(\"lookback\") Optional<Long> lookback) throws IOException {\n    Call<List<DependencyLink>> call =\n      storage.spanStore().getDependencies(endTs, lookback.orElse(defaultLookback));\n    return jsonResponse(DependencyLinkBytesEncoder.JSON_V1.encodeList(call.execute()));\n  }\n\n  @Get(\"/api/v2/services\")\n  @Blocking\n  public AggregatedHttpResponse getServiceNames(ServiceRequestContext ctx) throws IOException {\n    List<String> serviceNames = storage.serviceAndSpanNames().getServiceNames().execute();\n    serviceCount = serviceNames.size();\n    return maybeCacheNames(serviceCount > 3, serviceNames, ctx.alloc());\n  }\n\n  @Get(\"/api/v2/spans\")\n  @Blocking\n  public AggregatedHttpResponse getSpanNames(\n    @Param(\"serviceName\") String serviceName, ServiceRequestContext ctx)\n    throws IOException {\n    List<String> spanNames = storage.serviceAndSpanNames().getSpanNames(serviceName).execute();\n    return maybeCacheNames(serviceCount > 3, spanNames, ctx.alloc());\n  }\n\n  @Get(\"/api/v2/remoteServices\")\n  @Blocking\n  public AggregatedHttpResponse getRemoteServiceNames(\n    @Param(\"serviceName\") String serviceName, ServiceRequestContext ctx)\n    throws IOException {\n    List<String> remoteServiceNames =\n      storage.serviceAndSpanNames().getRemoteServiceNames(serviceName).execute();\n    return maybeCacheNames(serviceCount > 3, remoteServiceNames, ctx.alloc());\n  }\n\n  @Get(\"/api/v2/traces\")\n  @Blocking\n  public AggregatedHttpResponse getTraces(\n    @Param(\"serviceName\") Optional<String> serviceName,\n    @Param(\"remoteServiceName\") Optional<String> remoteServiceName,\n    @Param(\"spanName\") Optional<String> spanName,\n    @Param(\"annotationQuery\") Optional<String> annotationQuery,\n    @Param(\"minDuration\") Optional<Long> minDuration,\n    @Param(\"maxDuration\") Optional<Long> maxDuration,\n    @Param(\"endTs\") Optional<Long> endTs,\n    @Param(\"lookback\") Optional<Long> lookback,\n    @Default(\"10\") @Param(\"limit\") int limit)\n    throws IOException {\n    QueryRequest queryRequest =\n      QueryRequest.newBuilder()\n        .serviceName(serviceName.orElse(null))\n        .remoteServiceName(remoteServiceName.orElse(null))\n        .spanName(spanName.orElse(null))\n        .parseAnnotationQuery(annotationQuery.orElse(null))\n        .minDuration(minDuration.orElse(null))\n        .maxDuration(maxDuration.orElse(null))\n        .endTs(endTs.orElse(System.currentTimeMillis()))\n        .lookback(lookback.orElse(defaultLookback))\n        .limit(limit)\n        .build();\n\n    List<List<Span>> traces = storage.spanStore().getTraces(queryRequest).execute();\n    return jsonResponse(writeTraces(SpanBytesEncoder.JSON_V2, traces));\n  }\n\n  @Get(\"/api/v2/trace/{traceId}\")\n  @Blocking\n  public AggregatedHttpResponse getTrace(@Param(\"traceId\") String traceId) throws IOException {\n    traceId = traceId != null ? traceId.trim() : null;\n    traceId = Span.normalizeTraceId(traceId);\n    List<Span> trace = storage.traces().getTrace(traceId).execute();\n    if (trace.isEmpty()) {\n      return AggregatedHttpResponse.of(NOT_FOUND, ANY_TEXT_TYPE, traceId + \" not found\");\n    }\n    return jsonResponse(SpanBytesEncoder.JSON_V2.encodeList(trace));\n  }\n\n  @Get(\"/api/v2/traceMany\")\n  @Blocking\n  public AggregatedHttpResponse getTraces(@Param(\"traceIds\") String traceIds) throws IOException {\n    if (traceIds.isEmpty()) {\n      return AggregatedHttpResponse.of(BAD_REQUEST, ANY_TEXT_TYPE, \"traceIds parameter is empty\");\n    }\n\n    Set<String> normalized = new LinkedHashSet<>();\n    for (String traceId : traceIds.split(\",\", 1000)) {\n      if (normalized.add(Span.normalizeTraceId(traceId))) continue;\n      return AggregatedHttpResponse.of(BAD_REQUEST, ANY_TEXT_TYPE, \"redundant traceId: \" + traceId);\n    }\n\n    if (normalized.size() == 1) {\n      return AggregatedHttpResponse.of(BAD_REQUEST, ANY_TEXT_TYPE,\n        \"Use /api/v2/trace/{traceId} endpoint to retrieve a single trace\");\n    }\n\n    List<List<Span>> traces = storage.traces().getTraces(normalized).execute();\n    return jsonResponse(writeTraces(SpanBytesEncoder.JSON_V2, traces));\n  }\n\n  static AggregatedHttpResponse jsonResponse(byte[] body) {\n    return AggregatedHttpResponse.of(ResponseHeaders.builder(200)\n      .contentType(MediaType.JSON)\n      .setInt(HttpHeaderNames.CONTENT_LENGTH, body.length).build(), HttpData.wrap(body));\n  }\n\n  @Get(\"/api/v2/autocompleteKeys\")\n  @Blocking\n  public AggregatedHttpResponse getAutocompleteKeys(ServiceRequestContext ctx) {\n    return maybeCacheNames(true, autocompleteKeys, ctx.alloc());\n  }\n\n  @Get(\"/api/v2/autocompleteValues\")\n  @Blocking\n  public AggregatedHttpResponse getAutocompleteValues(\n    @Param(\"key\") String key, ServiceRequestContext ctx) throws IOException {\n    List<String> values = storage.autocompleteTags().getValues(key).execute();\n    return maybeCacheNames(values.size() > 3, values, ctx.alloc());\n  }\n\n  /**\n   * We cache names if there are more than 3 names. This helps people getting started: if we cache\n   * empty results, users have more questions. We assume caching becomes a concern when zipkin is in\n   * active use, and active use usually implies more than 3 services.\n   */\n  AggregatedHttpResponse maybeCacheNames(\n    boolean shouldCacheControl, List<String> values, ByteBufAllocator alloc) {\n    Collections.sort(values);\n    int sizeEstimate = 2; // Two brackets.\n    for (String value : values) {\n      sizeEstimate += value.length() + 1 /* comma */;\n    }\n    sizeEstimate -= 1; // Last element doesn't have a comma.\n    // If the values don't require escaping, this buffer will not be resized.\n    ByteBuf buf = alloc.buffer(sizeEstimate);\n    try (JsonGenerator gen =\n           JsonUtil.JSON_FACTORY.createGenerator((OutputStream) new ByteBufOutputStream(buf))) {\n      gen.writeStartArray(values.size());\n      for (String value : values) {\n        gen.writeString(value);\n      }\n      gen.writeEndArray();\n    } catch (IOException e) {\n      buf.release();\n      throw new UncheckedIOException(e);\n    }\n    ResponseHeadersBuilder headers = ResponseHeaders.builder(200)\n      .contentType(MediaType.JSON)\n      .setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());\n    if (shouldCacheControl) {\n      headers = headers.add(CACHE_CONTROL, \"max-age=\" + namesMaxAge + \", must-revalidate\");\n    }\n    return AggregatedHttpResponse.of(headers.build(), HttpData.wrap(buf));\n  }\n\n  // This is inlined here as there isn't enough re-use to warrant it being in the zipkin2 library\n  static byte[] writeTraces(SpanBytesEncoder codec, List<List<zipkin2.Span>> traces) {\n    // Get the encoded size of the nested list so that we don't need to grow the buffer\n    int length = traces.size();\n    int sizeInBytes = 2; // []\n    if (length > 1) sizeInBytes += length - 1; // comma to join elements\n\n    for (int i = 0; i < length; i++) {\n      List<zipkin2.Span> spans = traces.get(i);\n      int jLength = spans.size();\n      sizeInBytes += 2; // []\n      if (jLength > 1) sizeInBytes += jLength - 1; // comma to join elements\n      for (int j = 0; j < jLength; j++) {\n        sizeInBytes += codec.sizeInBytes(spans.get(j));\n      }\n    }\n\n    byte[] out = new byte[sizeInBytes];\n    int pos = 0;\n    out[pos++] = '['; // start list of traces\n    for (int i = 0; i < length; i++) {\n      pos += codec.encodeList(traces.get(i), out, pos);\n      if (i + 1 < length) out[pos++] = ',';\n    }\n    out[pos] = ']'; // stop list of traces\n    return out;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/activemq/ZipkinActiveMQCollectorConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.activemq;\n\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.collector.activemq.ActiveMQCollector;\nimport zipkin2.storage.StorageComponent;\n\n/** Auto-configuration for {@link ActiveMQCollector}. */\n@ConditionalOnClass(ActiveMQCollector.class)\n@EnableConfigurationProperties(ZipkinActiveMQCollectorProperties.class)\n@Conditional(ZipkinActiveMQCollectorConfiguration.ActiveMQUrlSet.class)\npublic class ZipkinActiveMQCollectorConfiguration {\n\n  @Bean(initMethod = \"start\")\n  ActiveMQCollector activeMq(\n    ZipkinActiveMQCollectorProperties properties,\n    CollectorSampler sampler,\n    CollectorMetrics metrics,\n    StorageComponent storage) {\n    return properties.toBuilder().sampler(sampler).metrics(metrics).storage(storage).build();\n  }\n\n  /**\n   * This condition passes when {@link ZipkinActiveMQCollectorProperties#getUrl()}} is set to\n   * non-empty.\n   *\n   * <p>This is here because the yaml defaults this property to empty like this, and spring-boot\n   * doesn't have an option to treat empty properties as unset.\n   *\n   * <pre>{@code\n   * url: ${ACTIVEMQ_URL:}\n   * }</pre>\n   */\n  static final class ActiveMQUrlSet implements Condition {\n    @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata a) {\n      return !isEmpty(\n        context.getEnvironment().getProperty(\"zipkin.collector.activemq.url\")) &&\n        notFalse(context.getEnvironment().getProperty(\"zipkin.collector.activemq.enabled\"));\n    }\n\n    private static boolean isEmpty(String s) {\n      return s == null || s.isEmpty();\n    }\n\n    private static boolean notFalse(String s){\n      return s == null || !s.equals(\"false\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/activemq/ZipkinActiveMQCollectorProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.activemq;\n\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport zipkin2.collector.activemq.ActiveMQCollector;\n\n/** Properties for configuring and building a {@link ActiveMQCollector}. */\n@ConfigurationProperties(\"zipkin.collector.activemq\")\nclass ZipkinActiveMQCollectorProperties {\n  /** URL of the ActiveMQ broker. */\n  private String url;\n\n  /** ActiveMQ queue from which to collect the Zipkin spans */\n  private String queue;\n\n  /** Client ID prefix for queue consumers */\n  private String clientIdPrefix = \"zipkin\";\n\n  /** Connection ID prefix for queue consumers */\n  private String connectionIdPrefix = \"zipkin\";\n\n  /** Number of concurrent span consumers */\n  private Integer concurrency;\n\n  /** Login user of the broker. */\n  private String username;\n\n  /** Login password of the broker. */\n  private String password;\n\n  public String getUrl() {\n    return url;\n  }\n\n  public void setUrl(String url) {\n    this.url = emptyToNull(url);\n  }\n\n  public String getQueue() {\n    return queue;\n  }\n\n  public void setQueue(String queue) {\n    this.queue = emptyToNull(queue);\n  }\n\n  public String getClientIdPrefix() {\n    return clientIdPrefix;\n  }\n\n  public void setClientIdPrefix(String clientIdPrefix) {\n    this.clientIdPrefix = clientIdPrefix;\n  }\n\n  public String getConnectionIdPrefix() {\n    return connectionIdPrefix;\n  }\n\n  public void setConnectionIdPrefix(String connectionIdPrefix) {\n    this.connectionIdPrefix = connectionIdPrefix;\n  }\n\n  public Integer getConcurrency() {\n    return concurrency;\n  }\n\n  public void setConcurrency(Integer concurrency) {\n    this.concurrency = concurrency;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = emptyToNull(username);\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = emptyToNull(password);\n  }\n\n  public ActiveMQCollector.Builder toBuilder() {\n    final ActiveMQCollector.Builder result = ActiveMQCollector.builder();\n    if (concurrency != null) result.concurrency(concurrency);\n    if (queue != null) result.queue(queue);\n\n    ActiveMQConnectionFactory connectionFactory;\n    if (username != null) {\n      connectionFactory = new ActiveMQConnectionFactory(username, password, url);\n    } else {\n      connectionFactory = new ActiveMQConnectionFactory(url);\n    }\n    connectionFactory.setClientIDPrefix(clientIdPrefix);\n    connectionFactory.setConnectionIDPrefix(connectionIdPrefix);\n    result.connectionFactory(connectionFactory);\n    return result;\n  }\n\n  private static String emptyToNull(String s) {\n    return \"\".equals(s) ? null : s;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/banner/ZipkinBanner.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.banner;\n\nimport java.io.InputStream;\nimport java.io.PrintStream;\nimport org.springframework.boot.Banner;\nimport org.springframework.boot.ansi.AnsiElement;\nimport org.springframework.boot.ansi.AnsiOutput;\nimport org.springframework.boot.ansi.AnsiStyle;\nimport org.springframework.core.env.Environment;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.util.StreamUtils;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * More efficient Banner implementation which doesn't use property sources as variables are expanded\n * at compile time using Maven resource filtering.\n */\npublic class ZipkinBanner implements Banner {\n  static final AnsiElement ZIPKIN_ORANGE = new AnsiElement() {\n    @Override public String toString() {\n      return \"38;5;208\"; // Ansi 256 color code 208 (orange)\n    }\n  };\n\n  @Override\n  public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {\n    try (InputStream stream = new ClassPathResource(\"zipkin.txt\").getInputStream()) {\n      String banner = StreamUtils.copyToString(stream, UTF_8);\n\n      // Instead of use property expansion for only 2 ansi codes, inline them\n      banner = banner.replace(\"${AnsiOrange}\", AnsiOutput.encode(ZIPKIN_ORANGE));\n      banner = banner.replace(\"${AnsiNormal}\", AnsiOutput.encode(AnsiStyle.NORMAL));\n\n      out.println(banner);\n    } catch (Exception ex) {\n      // who cares\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/brave/SelfTracingProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.brave;\n\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.convert.DurationUnit;\n\n@ConfigurationProperties(\"zipkin.self-tracing\")\nclass SelfTracingProperties {\n\n  /** Whether self-tracing is enabled. Defaults to {@code false}. */\n  private boolean enabled = false;\n  /**\n   * The percentage of traces retained when self-tracing. If 1.0 (i.e., all traces are sampled), the\n   * value of {@link #getTracesPerSecond()} will be used for sampling traces. Defaults to {@code\n   * 1.0}, sampling all traces.\n   */\n  private float sampleRate = 1.0f;\n  /**\n   * The number of traces per second to retain. If 0, an unlimited number of traces will be\n   * retained. This value has no effect if {@link #getSampleRate()} is set to something other than\n   * {@code 1.0}. Defaults to 1 trace per second.\n   */\n  private int tracesPerSecond = 1;\n  /** Timeout to flush self-tracing data to storage. */\n  @DurationUnit(ChronoUnit.SECONDS)\n  private Duration messageTimeout = Duration.ofSeconds(1);\n\n  public boolean isEnabled() {\n    return enabled;\n  }\n\n  public void setEnabled(boolean enabled) {\n    this.enabled = enabled;\n  }\n\n  public float getSampleRate() {\n    return sampleRate;\n  }\n\n  public void setSampleRate(float sampleRate) {\n    this.sampleRate = sampleRate;\n  }\n\n  public int getTracesPerSecond() {\n    return tracesPerSecond;\n  }\n\n  public void setTracesPerSecond(int tracesPerSecond) {\n    this.tracesPerSecond = tracesPerSecond;\n  }\n\n  public Duration getMessageTimeout() {\n    return messageTimeout;\n  }\n\n  public void setMessageTimeout(Duration messageTimeout) {\n    this.messageTimeout = messageTimeout;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/brave/TracedCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.brave;\n\nimport brave.ScopedSpan;\nimport brave.Span;\nimport brave.Tracer;\nimport java.io.IOException;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\npublic final class TracedCall<V> extends Call<V> {\n  final Tracer tracer;\n  final Call<V> delegate;\n  final String name;\n\n  public TracedCall(Tracer tracer, Call<V> delegate, String name) {\n    this.tracer = tracer;\n    this.delegate = delegate;\n    this.name = name;\n  }\n\n  @Override public V execute() throws IOException {\n    ScopedSpan span = tracer.startScopedSpan(name);\n    try {\n      return delegate.execute();\n    } catch (RuntimeException | IOException | Error e) {\n      span.error(e);\n      throw e;\n    } finally {\n      span.finish();\n    }\n  }\n\n  @Override public void enqueue(Callback<V> callback) {\n    Span span = tracer.nextSpan().name(name).start();\n    try {\n      if (span.isNoop()) {\n        delegate.enqueue(callback);\n      } else {\n        delegate.enqueue(new SpanFinishingCallback<>(callback, span));\n      }\n    } catch (RuntimeException | Error e) {\n      span.error(e);\n      span.finish();\n      throw e;\n    }\n  }\n\n  @Override public void cancel() {\n    delegate.cancel();\n  }\n\n  @Override public boolean isCanceled() {\n    return delegate.isCanceled();\n  }\n\n  @Override public Call<V> clone() {\n    return new TracedCall<>(tracer, delegate, name);\n  }\n\n  @Override public String toString() {\n    return \"Traced(\" + delegate + \")\";\n  }\n\n  static final class SpanFinishingCallback<V> implements Callback<V> {\n    private final Callback<V> delegate;\n    private final Span span;\n\n    SpanFinishingCallback(Callback<V> delegate, Span span) {\n      this.delegate = delegate;\n      this.span = span;\n    }\n\n    @Override public void onSuccess(V value) {\n      delegate.onSuccess(value);\n      span.finish();\n    }\n\n    @Override public void onError(Throwable t) {\n      delegate.onError(t);\n      span.error(t).finish();\n    }\n\n    @Override public String toString() {\n      return \"Traced(\" + delegate + \")\";\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/brave/TracingStorageComponent.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.brave;\n\nimport brave.Tracer;\nimport brave.Tracing;\nimport java.io.IOException;\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.storage.AutocompleteTags;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.Traces;\n\n// public for use in ZipkinServerConfiguration\npublic final class TracingStorageComponent extends ForwardingStorageComponent {\n  final Tracing tracing;\n  final StorageComponent delegate;\n\n  public TracingStorageComponent(Tracing tracing, StorageComponent delegate) {\n    this.tracing = tracing;\n    this.delegate = delegate;\n  }\n\n  @Override protected StorageComponent delegate() {\n    return delegate;\n  }\n\n  @Override public ServiceAndSpanNames serviceAndSpanNames() {\n    return new TracingServiceAndSpanNames(tracing, delegate.serviceAndSpanNames());\n  }\n\n  @Override public Traces traces() {\n    return new TracingTraces(tracing, delegate.traces());\n  }\n\n  @Override public SpanStore spanStore() {\n    return new TracingSpanStore(tracing, delegate.spanStore());\n  }\n\n  @Override public AutocompleteTags autocompleteTags() {\n    return new TracingAutocompleteTags(tracing, delegate.autocompleteTags());\n  }\n\n  @Override public SpanConsumer spanConsumer() {\n    return new TracingSpanConsumer(tracing, delegate.spanConsumer());\n  }\n\n  @Override public CheckResult check() {\n    return delegate.check();\n  }\n\n  @Override public void close() throws IOException {\n    delegate.close();\n  }\n\n  @Override public String toString() {\n    return \"Traced{\" + delegate + \"}\";\n  }\n\n  static final class TracingTraces implements Traces {\n    final Tracer tracer;\n    final Traces delegate;\n\n    TracingTraces(Tracing tracing, Traces delegate) {\n      this.tracer = tracing.tracer();\n      this.delegate = delegate;\n    }\n\n    @Override public Call<List<Span>> getTrace(String traceId) {\n      return new TracedCall<>(tracer, delegate.getTrace(traceId), \"get-trace\");\n    }\n\n    @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n      return new TracedCall<>(tracer, delegate.getTraces(traceIds), \"get-traces\");\n    }\n\n    @Override public String toString() {\n      return \"Traced{\" + delegate + \"}\";\n    }\n  }\n\n  static final class TracingSpanStore implements SpanStore {\n    final Tracer tracer;\n    final SpanStore delegate;\n\n    TracingSpanStore(Tracing tracing, SpanStore delegate) {\n      this.tracer = tracing.tracer();\n      this.delegate = delegate;\n    }\n\n    @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {\n      return new TracedCall<>(tracer, delegate.getTraces(request), \"get-traces\");\n    }\n\n    @Override @Deprecated public Call<List<Span>> getTrace(String traceId) {\n      return new TracedCall<>(tracer, delegate.getTrace(traceId), \"get-trace\");\n    }\n\n    @Override @Deprecated public Call<List<String>> getServiceNames() {\n      return new TracedCall<>(tracer, delegate.getServiceNames(), \"get-service-names\");\n    }\n\n    @Override @Deprecated public Call<List<String>> getSpanNames(String serviceName) {\n      return new TracedCall<>(tracer, delegate.getSpanNames(serviceName), \"get-span-names\");\n    }\n\n    @Override public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n      return new TracedCall<>(\n        tracer, delegate.getDependencies(endTs, lookback), \"get-dependencies\");\n    }\n\n    @Override public String toString() {\n      return \"Traced{\" + delegate + \"}\";\n    }\n  }\n\n  static final class TracingAutocompleteTags implements AutocompleteTags {\n    final Tracer tracer;\n    final AutocompleteTags delegate;\n\n    TracingAutocompleteTags(Tracing tracing, AutocompleteTags delegate) {\n      this.tracer = tracing.tracer();\n      this.delegate = delegate;\n    }\n\n    @Override public Call<List<String>> getKeys() {\n      return new TracedCall<>(tracer, delegate.getKeys(), \"get-keys\");\n    }\n\n    @Override public Call<List<String>> getValues(String key) {\n      return new TracedCall<>(tracer, delegate.getValues(key), \"get-values\");\n    }\n\n    @Override public String toString() {\n      return \"Traced{\" + delegate + \"}\";\n    }\n  }\n\n  static final class TracingServiceAndSpanNames implements ServiceAndSpanNames {\n    final Tracer tracer;\n    final ServiceAndSpanNames delegate;\n\n    TracingServiceAndSpanNames(Tracing tracing, ServiceAndSpanNames delegate) {\n      this.tracer = tracing.tracer();\n      this.delegate = delegate;\n    }\n\n    @Override public Call<List<String>> getServiceNames() {\n      return new TracedCall<>(tracer, delegate.getServiceNames(), \"get-service-names\");\n    }\n\n    @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {\n      return new TracedCall<>(tracer, delegate.getRemoteServiceNames(serviceName),\n        \"get-remote-service-names\");\n    }\n\n    @Override public Call<List<String>> getSpanNames(String serviceName) {\n      return new TracedCall<>(tracer, delegate.getSpanNames(serviceName), \"get-span-names\");\n    }\n\n    @Override public String toString() {\n      return \"Traced{\" + delegate + \"}\";\n    }\n  }\n\n  static final class TracingSpanConsumer implements SpanConsumer {\n    final Tracer tracer;\n    final SpanConsumer delegate;\n\n    TracingSpanConsumer(Tracing tracing, SpanConsumer delegate) {\n      this.tracer = tracing.tracer();\n      this.delegate = delegate;\n    }\n\n    @Override public Call<Void> accept(List<Span> spans) {\n      return new TracedCall<>(tracer, delegate.accept(spans), \"accept-spans\");\n    }\n\n    @Override public String toString() {\n      return \"Traced{\" + delegate + \"}\";\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/brave/ZipkinSelfTracingConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.brave;\n\nimport brave.Tracing;\nimport brave.context.slf4j.MDCScopeDecorator;\nimport brave.http.HttpTracing;\nimport brave.propagation.B3Propagation;\nimport brave.propagation.CurrentTraceContext;\nimport brave.propagation.ThreadLocalSpan;\nimport brave.sampler.BoundarySampler;\nimport brave.sampler.RateLimitingSampler;\nimport brave.sampler.Sampler;\nimport com.linecorp.armeria.common.brave.RequestContextCurrentTraceContext;\nimport com.linecorp.armeria.server.brave.BraveService;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.reporter.BytesMessageSender;\nimport zipkin2.reporter.Encoding;\nimport zipkin2.reporter.ReporterMetrics;\nimport zipkin2.reporter.brave.AsyncZipkinSpanHandler;\nimport zipkin2.server.internal.ConditionalOnSelfTracing;\nimport zipkin2.storage.StorageComponent;\n\n@EnableConfigurationProperties(SelfTracingProperties.class)\n@ConditionalOnSelfTracing\npublic class ZipkinSelfTracingConfiguration {\n  /** Configuration for how to buffer spans into messages for Zipkin */\n  @Bean AsyncZipkinSpanHandler reporter(BeanFactory factory, SelfTracingProperties config) {\n    return AsyncZipkinSpanHandler.newBuilder(new LocalSender(factory))\n      .threadFactory((runnable) -> new Thread(new Runnable() {\n        @Override public void run() {\n          RequestContextCurrentTraceContext.setCurrentThreadNotRequestThread(true);\n          runnable.run();\n        }\n\n        @Override public String toString() {\n          return runnable.toString();\n        }\n      }))\n      .messageTimeout(config.getMessageTimeout().toNanos(), TimeUnit.NANOSECONDS)\n      .metrics(new ReporterMetricsAdapter(factory))\n      .build();\n  }\n\n  @Bean CurrentTraceContext currentTraceContext() {\n    return RequestContextCurrentTraceContext.builder()\n      .addScopeDecorator(MDCScopeDecorator.get()) // puts trace IDs into logs\n      .build();\n  }\n\n  /**\n   * There's no attribute namespace shared across request and response. Hence, we need to save off a\n   * reference to the span in scope, so that we can close it in the response.\n   */\n  @Bean ThreadLocalSpan threadLocalSpan(Tracing tracing) {\n    return ThreadLocalSpan.create(tracing.tracer());\n  }\n\n  /**\n   * This controls the general rate. In order to not accidentally start traces started from the\n   * tracer itself, this isn't used as {@link Tracing.Builder#sampler(Sampler)}. The impact of this\n   * is that we can't currently start traces from Kafka or Rabbit (until we use a messaging\n   * sampler).\n   * <p>\n   * See https://github.com/openzipkin/brave/pull/914 for the messaging abstraction\n   */\n  @Bean Sampler sampler(SelfTracingProperties config) {\n    if (config.getSampleRate() != 1.0) {\n      if (config.getSampleRate() < 0.01) {\n        return BoundarySampler.create(config.getSampleRate());\n      } else {\n        return Sampler.create(config.getSampleRate());\n      }\n    } else if (config.getTracesPerSecond() != 0) {\n      return RateLimitingSampler.create(config.getTracesPerSecond());\n    }\n    return Sampler.ALWAYS_SAMPLE;\n  }\n\n  /** Controls aspects of tracing such as the name that shows up in the UI */\n  @Bean Tracing tracing(AsyncZipkinSpanHandler zipkinSpanHandler,\n    CurrentTraceContext currentTraceContext) {\n    return Tracing.newBuilder()\n      .localServiceName(\"zipkin-server\")\n      .sampler(Sampler.NEVER_SAMPLE) // don't sample traces at this abstraction\n      .currentTraceContext(currentTraceContext)\n      // Reduce the impact on untraced downstream http services such as Elasticsearch\n      .propagationFactory(B3Propagation.newFactoryBuilder()\n        .injectFormat(brave.Span.Kind.CLIENT, B3Propagation.Format.SINGLE)\n        .build())\n      .addSpanHandler(zipkinSpanHandler)\n      .build();\n  }\n\n  @Bean HttpTracing httpTracing(Tracing tracing, Sampler sampler) {\n    return HttpTracing.newBuilder(tracing)\n      // server starts traces for read requests under the path /api\n      .serverSampler(request -> {\n          String path = request.path();\n          if (path.startsWith(\"/api\") || path.startsWith(\"/zipkin/api\")) {\n            return sampler.isSampled(0L); // use the global rate limit\n          }\n          return false;\n        }\n      )\n      .build();\n  }\n\n  @Bean ArmeriaServerConfigurator tracingConfigurator(HttpTracing tracing) {\n    return server -> server.decorator(BraveService.newDecorator(tracing));\n  }\n\n  /** Lazily looks up the storage component in order to avoid proxying. */\n  static final class LocalSender extends BytesMessageSender.Base {\n    final BeanFactory factory;\n    volatile StorageComponent delegate; // volatile to prevent stale reads\n\n    LocalSender(BeanFactory factory) {\n      // TODO: less memory efficient, but not a huge problem for self-tracing which is rarely on\n      // https://github.com/openzipkin/zipkin-reporter-java/issues/178\n      super(Encoding.JSON);\n      this.factory = factory;\n    }\n\n    @Override public int messageMaxBytes() {\n      return 5 * 1024 * 1024; // arbitrary\n    }\n\n    @Override public void send(List<byte[]> encodedSpans) throws IOException {\n      List<Span> spans = new ArrayList<>(encodedSpans.size());\n      for (byte[] encodedSpan : encodedSpans) {\n        Span v2Span = SpanBytesDecoder.JSON_V2.decodeOne(encodedSpan);\n        spans.add(v2Span);\n      }\n\n      delegate().spanConsumer().accept(spans).execute();\n    }\n\n    @Override public String toString() {\n      // Avoid using the delegate to avoid eagerly loading the bean during initialization\n      return \"StorageComponent\";\n    }\n\n    @Override public void close() {\n      // don't close delegate as we didn't open it!\n    }\n\n    /** Lazy lookup to avoid proxying */\n    StorageComponent delegate() {\n      StorageComponent result = delegate;\n      if (result != null) return delegate;\n      // synchronization is not needed as redundant calls have no ill effects\n      result = factory.getBean(StorageComponent.class);\n      if (result instanceof TracingStorageComponent component) {\n        result = component.delegate;\n      }\n      return delegate = result;\n    }\n  }\n\n  static final class ReporterMetricsAdapter implements ReporterMetrics {\n    final BeanFactory factory;\n    volatile CollectorMetrics delegate; // volatile to prevent stale reads\n\n    ReporterMetricsAdapter(BeanFactory factory) {\n      this.factory = factory;\n    }\n\n    @Override public void incrementMessages() {\n      delegate().incrementMessages();\n    }\n\n    @Override public void incrementMessagesDropped(Throwable throwable) {\n      delegate().incrementMessagesDropped();\n    }\n\n    @Override public void incrementSpans(int i) {\n      delegate().incrementSpans(i);\n    }\n\n    @Override public void incrementSpanBytes(int i) {\n      delegate().incrementBytes(i);\n    }\n\n    @Override public void incrementMessageBytes(int i) {\n    }\n\n    @Override public void incrementSpansDropped(int i) {\n      delegate().incrementMessagesDropped();\n    }\n\n    @Override public void updateQueuedSpans(int i) {\n    }\n\n    @Override public void updateQueuedBytes(int i) {\n    }\n\n    /** Lazy lookup to avoid proxying */\n    CollectorMetrics delegate() {\n      CollectorMetrics result = delegate;\n      if (result != null) return delegate;\n      // synchronization is not needed as redundant calls have no ill effects\n      return delegate = factory.getBean(CollectorMetrics.class).forTransport(\"local\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/cassandra3/ZipkinCassandra3StorageConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.cassandra3;\n\nimport java.util.List;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.beans.factory.BeanFactoryAware;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport zipkin2.server.internal.ConditionalOnSelfTracing;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.cassandra.CassandraStorage;\nimport zipkin2.storage.cassandra.CassandraStorage.SessionFactory;\n\n/**\n * This storage accepts Cassandra logs in a specified category. Each log entry is expected to\n * contain a single span, which is TBinaryProtocol big-endian, then base64 encoded. Decoded spans\n * are stored asynchronously.\n */\n@ConditionalOnClass(CassandraStorage.class)\n@EnableConfigurationProperties(ZipkinCassandra3StorageProperties.class)\n@ConditionalOnProperty(name = \"zipkin.storage.type\", havingValue = \"cassandra3\")\n@ConditionalOnMissingBean(StorageComponent.class)\n@Import(ZipkinCassandra3StorageConfiguration.TracingSessionFactoryEnhancer.class)\n// This component is named .*Cassandra3.* even though the package already says cassandra3 because\n// Spring Boot configuration endpoints only printout the simple name of the class\npublic class ZipkinCassandra3StorageConfiguration {\n  @Bean SessionFactory sessionFactory() {\n    return SessionFactory.DEFAULT;\n  }\n  @Bean @ConditionalOnMissingBean StorageComponent storage(\n      ZipkinCassandra3StorageProperties properties,\n      SessionFactory sessionFactory,\n      @Value(\"${zipkin.storage.strict-trace-id:true}\") boolean strictTraceId,\n      @Value(\"${zipkin.storage.search-enabled:true}\") boolean searchEnabled,\n      @Value(\"${zipkin.storage.autocomplete-keys:}\") List<String> autocompleteKeys,\n      @Value(\"${zipkin.storage.autocomplete-ttl:3600000}\") int autocompleteTtl,\n      @Value(\"${zipkin.storage.autocomplete-cardinality:20000}\") int autocompleteCardinality) {\n    return properties.toBuilder()\n      .strictTraceId(strictTraceId)\n      .searchEnabled(searchEnabled)\n      .autocompleteKeys(autocompleteKeys)\n      .autocompleteTtl(autocompleteTtl)\n      .autocompleteCardinality(autocompleteCardinality)\n      .sessionFactory(sessionFactory).build();\n  }\n\n  @ConditionalOnSelfTracing\n  static class TracingSessionFactoryEnhancer  implements BeanPostProcessor, BeanFactoryAware {\n    /**\n     * Need this to resolve cyclic instantiation issue with spring when instantiating with tracing.\n     *\n     * <p>Ref: <a href=\"https://stackoverflow.com/a/19688634\">Tracking down cause of Spring's \"not\n     * eligible for auto-proxying\"</a></p>\n     */\n    BeanFactory beanFactory;\n\n    @Override public Object postProcessBeforeInitialization(Object bean, String beanName) {\n      return bean;\n    }\n\n    @Override public Object postProcessAfterInitialization(Object bean, String beanName) {\n      //if (bean instanceof SessionFactory && beanFactory.containsBean(\"tracing\")) {\n      //  SessionFactory delegate = (SessionFactory) bean;\n      //  Tracing tracing = beanFactory.getBean(Tracing.class);\n      //  return (SessionFactory) storage -> TracingSession.create(tracing, delegate.create(storage));\n      //}\n      return bean;\n    }\n\n    @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException {\n      this.beanFactory = beanFactory;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/cassandra3/ZipkinCassandra3StorageProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.cassandra3;\n\nimport java.io.Serializable;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport zipkin2.storage.cassandra.CassandraStorage;\n\n@ConfigurationProperties(\"zipkin.storage.cassandra3\")\nclass ZipkinCassandra3StorageProperties implements Serializable { // for Spark jobs\n  private static final long serialVersionUID = 0L;\n\n  private String keyspace = \"zipkin3\";\n  private String contactPoints = \"localhost\";\n  private String localDc = \"datacenter1\";\n  private int maxConnections = 8;\n  private boolean ensureSchema = true;\n  private boolean useSsl = false;\n  private boolean sslHostnameValidation = true;\n  private String username;\n  private String password;\n  /** See {@link CassandraStorage.Builder#indexFetchMultiplier(int)} */\n  private int indexFetchMultiplier = 3;\n\n  public String getKeyspace() {\n    return keyspace;\n  }\n\n  public void setKeyspace(String keyspace) {\n    this.keyspace = keyspace;\n  }\n\n  public String getContactPoints() {\n    return contactPoints;\n  }\n\n  public void setContactPoints(String contactPoints) {\n    this.contactPoints = contactPoints;\n  }\n\n  public String getLocalDc() {\n    return localDc;\n  }\n\n  public void setLocalDc(String localDc) {\n    this.localDc = \"\".equals(localDc) ? null : localDc;\n  }\n\n  public int getMaxConnections() {\n    return maxConnections;\n  }\n\n  public void setMaxConnections(int maxConnections) {\n    this.maxConnections = maxConnections;\n  }\n\n  public boolean isEnsureSchema() {\n    return ensureSchema;\n  }\n\n  public void setEnsureSchema(boolean ensureSchema) {\n    this.ensureSchema = ensureSchema;\n  }\n\n  public boolean isUseSsl() {\n    return useSsl;\n  }\n\n  public void setUseSsl(boolean useSsl) {\n    this.useSsl = useSsl;\n  }\n\n  public boolean isSslHostnameValidation() {\n    return sslHostnameValidation;\n  }\n\n  public void setSslHostnameValidation(boolean sslHostnameValidation) {\n    this.sslHostnameValidation = sslHostnameValidation;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = \"\".equals(username) ? null : username;\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = \"\".equals(password) ? null : password;\n  }\n\n  public int getIndexFetchMultiplier() {\n    return indexFetchMultiplier;\n  }\n\n  public void setIndexFetchMultiplier(int indexFetchMultiplier) {\n    this.indexFetchMultiplier = indexFetchMultiplier;\n  }\n\n  public CassandraStorage.Builder toBuilder() {\n    return CassandraStorage.newBuilder()\n      .keyspace(keyspace)\n      .contactPoints(contactPoints)\n      .localDc(localDc)\n      .maxConnections(maxConnections)\n      .ensureSchema(ensureSchema)\n      .useSsl(useSsl)\n      .sslHostnameValidation(sslHostnameValidation)\n      .username(username)\n      .password(password)\n      .indexFetchMultiplier(indexFetchMultiplier);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/BasicAuthInterceptor.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.ClientRequestContext;\nimport com.linecorp.armeria.client.HttpClient;\nimport com.linecorp.armeria.client.SimpleDecoratingHttpClient;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\n\n/**\n * Adds basic auth username and password to every request.\n *\n * <p>Ref: <a href=\"https://www.elastic.co/guide/en/x-pack/current/how-security-works.html\"> How\n * Elasticsearch security works</a></p>\n */\nfinal class BasicAuthInterceptor extends SimpleDecoratingHttpClient {\n\n  final BasicCredentials basicCredentials;\n\n  BasicAuthInterceptor(HttpClient client, BasicCredentials basicCredentials) {\n    super(client);\n    this.basicCredentials = basicCredentials;\n  }\n\n  @Override\n  public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception {\n    String credentials = basicCredentials.getCredentials();\n    if (credentials != null) {\n      ctx.addAdditionalRequestHeader(HttpHeaderNames.AUTHORIZATION, credentials);\n    }\n    return unwrap().execute(ctx, req);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/BasicCredentials.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport java.util.Base64;\nimport zipkin2.internal.Nullable;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * Generate Elasticsearch basic user credentials.\n *\n * <p>Ref: <a href=\"https://www.elastic.co/guide/en/x-pack/current/how-security-works.html\"> How\n * Elasticsearch security works</a></p>\n */\nfinal class BasicCredentials {\n\n  private volatile String basicCredentials;\n\n  BasicCredentials() {\n\n  }\n\n  BasicCredentials(String username, String password) {\n    updateCredentials(username, password);\n  }\n\n  void updateCredentials(String username, String password) {\n    String token = username + ':' + password;\n    basicCredentials = \"Basic \" + Base64.getEncoder().encodeToString(token.getBytes(UTF_8));\n  }\n\n  @Nullable\n  String getCredentials() {\n    return basicCredentials;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/DynamicCredentialsFileLoader.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.util.Properties;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.internal.Nullable;\n\nimport static zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageConfiguration.PASSWORD;\nimport static zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageConfiguration.USERNAME;\n\n/**\n * Loads username/password from credentials file.\n *\n * <p><em>NOTE:</em> This implementation loops instead of using {@link java.nio.file.WatchService}.\n * This means that spans will drop and api failures will occur for any time remaining in the refresh\n * interval. A future version can tighten this by also using poll events.\n */\nclass DynamicCredentialsFileLoader implements Runnable {\n  static final Logger LOGGER = LoggerFactory.getLogger(DynamicCredentialsFileLoader.class);\n\n  private final String credentialsFile;\n\n  private final BasicCredentials basicCredentials;\n\n  public DynamicCredentialsFileLoader(BasicCredentials basicCredentials,\n    String credentialsFile) {\n    this.basicCredentials = basicCredentials;\n    this.credentialsFile = credentialsFile;\n  }\n\n  @Override public void run() {\n    try {\n      updateCredentialsFromProperties();\n    } catch (Exception e) {\n      LOGGER.error(\"Error loading elasticsearch credentials\", e);\n    }\n  }\n\n  void updateCredentialsFromProperties() throws IOException {\n    Properties properties = new Properties();\n    try (FileInputStream is = new FileInputStream(credentialsFile)) {\n      properties.load(is);\n    }\n    String username = ensureNotEmptyOrNull(properties, credentialsFile, USERNAME);\n    String password = ensureNotEmptyOrNull(properties, credentialsFile, PASSWORD);\n    basicCredentials.updateCredentials(username, password);\n  }\n\n  @Nullable static String ensureNotEmptyOrNull(Properties properties, String fileName, String name) {\n    String value = properties.getProperty(name);\n    if (value == null) {\n      throw new IllegalStateException(\"no \" + name + \" property in \" + fileName);\n    }\n    value = value.trim();\n    if (value.isEmpty()) {\n      throw new IllegalStateException(\"empty \" + name + \" property in \" + fileName);\n    }\n    return value;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/HttpClientFactory.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.ClientFactory;\nimport com.linecorp.armeria.client.ClientOptions;\nimport com.linecorp.armeria.client.ClientOptionsBuilder;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.encoding.DecodingClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroup;\nimport com.linecorp.armeria.client.logging.ContentPreviewingClient;\nimport com.linecorp.armeria.client.logging.LoggingClient;\nimport com.linecorp.armeria.client.logging.LoggingClientBuilder;\nimport com.linecorp.armeria.client.metric.MetricCollectingClient;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpHeaders;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport com.linecorp.armeria.common.logging.LogLevel;\nimport com.linecorp.armeria.common.metric.MeterIdPrefixFunction;\nimport java.io.Closeable;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageProperties.HttpLogging;\n\n// Exposed as a bean so that zipkin-aws can use this for api requests to get initial endpoints.\npublic class HttpClientFactory implements Function<EndpointGroup, WebClient>, Closeable {\n  final SessionProtocol protocol;\n  final ClientOptions options;\n  final ClientFactory clientFactory;\n  final int timeout;\n  final List<Consumer<ClientOptionsBuilder>> customizers;\n\n  HttpClientFactory(ZipkinElasticsearchStorageProperties es, ClientFactory factory,\n    SessionProtocol protocol, List<Consumer<ClientOptionsBuilder>> customizers\n  ) {\n    this.clientFactory = factory;\n    this.protocol = protocol;\n    this.customizers = customizers;\n    this.timeout = es.getTimeout();\n    HttpLogging httpLogging = es.getHttpLogging();\n    ClientOptionsBuilder options = ClientOptions.builder()\n      .decorator(MetricCollectingClient.newDecorator(\n        MeterIdPrefixFunction.ofDefault(\"elasticsearch\")))\n      .decorator(DecodingClient.newDecorator());\n\n    configureHttpLogging(httpLogging, options);\n    this.options = configureOptionsExceptHttpLogging(options).build();\n  }\n\n  void configureHttpLogging(HttpLogging httpLogging, ClientOptionsBuilder options) {\n    if (httpLogging == HttpLogging.NONE) return;\n    LoggingClientBuilder loggingBuilder = LoggingClient.builder()\n      .requestLogLevel(LogLevel.INFO)\n      .successfulResponseLogLevel(LogLevel.INFO)\n      .requestHeadersSanitizer((ctx, headers) -> {\n        if (!headers.contains(HttpHeaderNames.AUTHORIZATION)) {\n          return headers;\n        }\n        // TODO(anuraaga): Add unit tests after https://github.com/line/armeria/issues/2220\n        return headers.toBuilder().set(HttpHeaderNames.AUTHORIZATION, \"****\").build();\n      });\n    switch (httpLogging) {\n      case HEADERS:\n        loggingBuilder.contentSanitizer((ctx, unused) -> \"\");\n        break;\n      case BASIC:\n        loggingBuilder.contentSanitizer((ctx, unused) -> \"\");\n        loggingBuilder.headersSanitizer((ctx, unused) -> HttpHeaders.of());\n        break;\n      case BODY:\n      default:\n        break;\n    }\n    options.decorator(loggingBuilder.newDecorator());\n    if (httpLogging == HttpLogging.BODY) {\n      options.decorator(ContentPreviewingClient.newDecorator(Integer.MAX_VALUE));\n    }\n  }\n\n  @Override public WebClient apply(EndpointGroup endpoint) {\n    return WebClient.builder(protocol, endpoint)\n      .options(options)\n      .build();\n  }\n\n  @Override public void close() {\n    clientFactory.close();\n  }\n\n  /** This takes care to not expose health checks into wire level logging */\n  ClientOptionsBuilder configureOptionsExceptHttpLogging(ClientOptionsBuilder options) {\n    options.factory(clientFactory).responseTimeoutMillis(timeout).writeTimeoutMillis(timeout);\n    customizers.forEach(c -> c.accept(options));\n    return options;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/InitialEndpointSupplier.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.Endpoint;\nimport com.linecorp.armeria.client.endpoint.EndpointGroup;\nimport com.linecorp.armeria.client.endpoint.dns.DnsAddressEndpointGroup;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.internal.Nullable;\n\nfinal class InitialEndpointSupplier implements Supplier<EndpointGroup> {\n  static final Logger LOGGER = LoggerFactory.getLogger(InitialEndpointSupplier.class);\n\n  final String hosts;\n  final SessionProtocol sessionProtocol;\n\n  InitialEndpointSupplier(SessionProtocol sessionProtocol, @Nullable String hosts) {\n    if (sessionProtocol == null) throw new NullPointerException(\"sessionProtocol == null\");\n    this.sessionProtocol = sessionProtocol;\n    this.hosts =\n      hosts == null || hosts.isEmpty() ? sessionProtocol.uriText() + \"://localhost:9200\" : hosts;\n  }\n\n  @Override public EndpointGroup get() {\n    List<EndpointGroup> endpointGroups = new ArrayList<>();\n    for (String hostText : hosts.split(\",\", 100)) {\n      if (\"\".equals(hostText)) continue; // possibly extra comma\n\n      URI url;\n      if (hostText.startsWith(\"http://\") || hostText.startsWith(\"https://\")) {\n        url = URI.create(hostText);\n      } else if (!sessionProtocol.isTls() && hostText.indexOf(':') == -1) {\n        url = URI.create(sessionProtocol.uriText() + \"://\" + hostText + \":9200\");\n      } else {\n        url = URI.create(sessionProtocol.uriText() + \"://\" + hostText);\n      }\n\n      String host = url.getHost();\n      if (host == null) {\n        LOGGER.warn(\"Skipping invalid ES host {}\", url);\n        continue;\n      }\n\n      int port = getPort(url);\n\n      if (port == 9300) {\n        LOGGER.warn(\"Native transport no longer supported. Changing {} to http port 9200\", host);\n        port = 9200;\n      }\n\n      if (isIpAddress(host) || host.equals(\"localhost\")) {\n        endpointGroups.add(EndpointGroup.of(Endpoint.of(host, port)));\n      } else {\n        // A host that isn't an IP may resolve to multiple IP addresses, so we use a endpoint\n        // group to round-robin over them. Users can mix addresses that resolve to multiple IPs\n        // with single IPs freely, they'll all get used.\n        endpointGroups.add(DnsAddressEndpointGroup.builder(host).port(port).build());\n      }\n    }\n\n    if (endpointGroups.isEmpty()) {\n      throw new IllegalArgumentException(\"No valid endpoints found in ES hosts: \" + hosts);\n    }\n\n    return EndpointGroup.of(endpointGroups);\n  }\n\n  int getPort(URI url) {\n    int port = url.getPort();\n    return port != -1 ? port : sessionProtocol.defaultPort();\n  }\n\n  static boolean isIpAddress(String address) {\n    return zipkin2.Endpoint.newBuilder().parseIp(address);\n  }\n\n  @Override public String toString() {\n    return hosts;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/LazyHttpClientImpl.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.Endpoint;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroup;\nimport com.linecorp.armeria.client.endpoint.healthcheck.HealthCheckedEndpointGroup;\nimport com.linecorp.armeria.client.metric.MetricCollectingClient;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport com.linecorp.armeria.common.metric.MeterIdPrefixFunction;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport java.util.function.Supplier;\nimport zipkin2.elasticsearch.ElasticsearchStorage.LazyHttpClient;\n\nfinal class LazyHttpClientImpl implements LazyHttpClient {\n  final HttpClientFactory factory;\n  final SessionProtocol protocol;\n  final Supplier<EndpointGroup> initialEndpoints;\n  final ZipkinElasticsearchStorageProperties.HealthCheck healthCheck;\n  final int timeoutMillis;\n  final MeterRegistry meterRegistry;\n\n  volatile WebClient result;\n\n  LazyHttpClientImpl(HttpClientFactory factory, SessionProtocol protocol,\n    Supplier<EndpointGroup> initialEndpoints, ZipkinElasticsearchStorageProperties es,\n    MeterRegistry meterRegistry) {\n    this.factory = factory;\n    this.protocol = protocol;\n    this.initialEndpoints = initialEndpoints;\n    this.healthCheck = es.getHealthCheck();\n    this.timeoutMillis = es.getTimeout();\n    this.meterRegistry = meterRegistry;\n  }\n\n  @Override public WebClient get() {\n    if (result == null) {\n      synchronized (this) {\n        if (result == null) {\n          result = factory.apply(getEndpoint());\n        }\n      }\n    }\n    return result;\n  }\n\n  EndpointGroup getEndpoint() {\n    EndpointGroup initial = initialEndpoints.get();\n    // Only health-check when there are alternative endpoints. There aren't when instanceof Endpoint\n    if (initial instanceof Endpoint || !healthCheck.isEnabled()) return initial;\n\n    // Wrap the result when health checking is enabled.\n    return decorateHealthCheck(initial);\n  }\n\n  // Enables health-checking of an endpoint group, so we only send requests to endpoints that are up\n  HealthCheckedEndpointGroup decorateHealthCheck(EndpointGroup endpointGroup) {\n    HealthCheckedEndpointGroup healthChecked =\n      HealthCheckedEndpointGroup.builder(endpointGroup, \"/_cluster/health\")\n        .protocol(protocol)\n        .useGet(true)\n        .selectionTimeoutMillis(timeoutMillis)\n        .clientFactory(factory.clientFactory)\n        .withClientOptions(options -> {\n          factory.configureHttpLogging(healthCheck.getHttpLogging(), options);\n          factory.configureOptionsExceptHttpLogging(options);\n          options.decorator(MetricCollectingClient.newDecorator(\n            MeterIdPrefixFunction.ofDefault(\"elasticsearch-healthcheck\")));\n          options.decorator((delegate, ctx, req) -> {\n            ctx.logBuilder().name(\"health-check\");\n            return delegate.execute(ctx, req);\n          });\n          return options;\n        })\n        .retryInterval(healthCheck.getInterval())\n        .build();\n    healthChecked.newMeterBinder(\"elasticsearch\").bindTo(meterRegistry);\n    return healthChecked;\n  }\n\n  @Override public final String toString() {\n    return initialEndpoints.toString();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/SslUtil.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport java.net.URL;\nimport java.security.KeyStore;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.TrustManagerFactory;\nimport org.springframework.util.ResourceUtils;\nimport zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageProperties.Ssl;\n\n// snippets adapted from com.linecorp.armeria.internal.spring.ArmeriaConfigurationUtil\nfinal class SslUtil {\n\n  static KeyManagerFactory getKeyManagerFactory(Ssl ssl) throws Exception {\n    KeyStore store =\n      loadKeyStore(ssl.getKeyStoreType(), ssl.getKeyStore(), ssl.getKeyStorePassword());\n\n    KeyManagerFactory keyManagerFactory =\n      KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\n\n    String keyPassword = ssl.getKeyStorePassword();\n    keyManagerFactory.init(store, keyPassword != null ? keyPassword.toCharArray() : null);\n    return keyManagerFactory;\n  }\n\n  static TrustManagerFactory getTrustManagerFactory(Ssl ssl) throws Exception {\n    KeyStore store =\n      loadKeyStore(ssl.getTrustStoreType(), ssl.getTrustStore(), ssl.getTrustStorePassword());\n\n    TrustManagerFactory trustManagerFactory =\n      TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\n    trustManagerFactory.init(store);\n    return trustManagerFactory;\n  }\n\n  static KeyStore loadKeyStore(String type, String resource, String password) throws Exception {\n    if (resource == null) return null;\n    KeyStore store = KeyStore.getInstance(type != null ? type : \"JKS\");\n    URL url = ResourceUtils.getURL(resource);\n    store.load(url.openStream(), password != null ? password.toCharArray() : null);\n    return store;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/ZipkinElasticsearchStorageConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport brave.CurrentSpanCustomizer;\nimport brave.SpanCustomizer;\nimport brave.http.HttpTracing;\nimport com.linecorp.armeria.client.ClientFactory;\nimport com.linecorp.armeria.client.ClientFactoryBuilder;\nimport com.linecorp.armeria.client.ClientOptionsBuilder;\nimport com.linecorp.armeria.client.brave.BraveClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroup;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport com.linecorp.armeria.common.logging.RequestLog;\nimport com.linecorp.armeria.common.logging.RequestLogProperty;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.util.NamedThreadFactory;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.TrustManagerFactory;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.server.internal.ConditionalOnSelfTracing;\nimport zipkin2.storage.StorageComponent;\n\nimport static zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageProperties.Ssl;\n\n@Configuration(proxyBeanMethods = false)\n@EnableConfigurationProperties(ZipkinElasticsearchStorageProperties.class)\n@ConditionalOnProperty(name = \"zipkin.storage.type\", havingValue = \"elasticsearch\")\n@ConditionalOnMissingBean(StorageComponent.class)\npublic class ZipkinElasticsearchStorageConfiguration {\n  static final String QUALIFIER = \"zipkinElasticsearch\";\n  static final String USERNAME = \"zipkin.storage.elasticsearch.username\";\n  static final String PASSWORD = \"zipkin.storage.elasticsearch.password\";\n  static final String CREDENTIALS_FILE =\n    \"zipkin.storage.elasticsearch.credentials-file\";\n  static final String CREDENTIALS_REFRESH_INTERVAL =\n    \"zipkin.storage.elasticsearch.credentials-refresh-interval\";\n\n  // Exposed as a bean so that zipkin-aws can override this as sourced from the AWS endpoints api\n  @Bean @Qualifier(QUALIFIER) @ConditionalOnMissingBean\n  Supplier<EndpointGroup> esInitialEndpoints(\n    SessionProtocol esSessionProtocol, ZipkinElasticsearchStorageProperties es) {\n    return new InitialEndpointSupplier(esSessionProtocol, es.getHosts());\n  }\n\n  // Exposed as a bean so that zipkin-aws can override this to always be SSL\n  @Bean @Qualifier(QUALIFIER) @ConditionalOnMissingBean\n  SessionProtocol esSessionProtocol(ZipkinElasticsearchStorageProperties es) {\n    if (es.getHosts() == null) return SessionProtocol.HTTP;\n    if (es.getHosts().contains(\"https://\")) return SessionProtocol.HTTPS;\n    return SessionProtocol.HTTP;\n  }\n\n  // exposed as a bean so that we can test TLS by swapping it out.\n  // TODO: see if we can override the TLS via properties instead as that has less surface area.\n  @Bean @Qualifier(QUALIFIER) @ConditionalOnMissingBean ClientFactory esClientFactory(\n    ZipkinElasticsearchStorageProperties es,\n    MeterRegistry meterRegistry) throws Exception {\n    ClientFactoryBuilder builder = ClientFactory.builder();\n\n    Ssl ssl = es.getSsl();\n    if (ssl.isNoVerify()) builder.tlsNoVerify();\n    // Allow use of a custom KeyStore or TrustStore when connecting to Elasticsearch\n    if (ssl.getKeyStore() != null || ssl.getTrustStore() != null) configureSsl(builder, ssl);\n\n    // Elasticsearch 7 never returns a response when receiving an HTTP/2 preface instead of the more\n    // valid behavior of returning a bad request response, so we can't use the preface.\n    // TODO: find or raise a bug with Elastic\n    return builder.useHttp2Preface(false)\n      .connectTimeoutMillis(es.getTimeout())\n      .meterRegistry(meterRegistry)\n      .build();\n  }\n\n  @Bean HttpClientFactory esHttpClientFactory(ZipkinElasticsearchStorageProperties es,\n    @Qualifier(QUALIFIER) ClientFactory factory,\n    @Qualifier(QUALIFIER) SessionProtocol protocol,\n    @Qualifier(QUALIFIER) List<Consumer<ClientOptionsBuilder>> options\n  ) {\n    return new HttpClientFactory(es, factory, protocol, options);\n  }\n\n  @Bean @ConditionalOnMissingBean StorageComponent storage(\n    ZipkinElasticsearchStorageProperties es,\n    HttpClientFactory esHttpClientFactory,\n    MeterRegistry meterRegistry,\n    @Qualifier(QUALIFIER) SessionProtocol protocol,\n    @Qualifier(QUALIFIER) Supplier<EndpointGroup> initialEndpoints,\n    @Value(\"${zipkin.query.lookback:86400000}\") int namesLookback,\n    @Value(\"${zipkin.storage.strict-trace-id:true}\") boolean strictTraceId,\n    @Value(\"${zipkin.storage.search-enabled:true}\") boolean searchEnabled,\n    @Value(\"${zipkin.storage.autocomplete-keys:}\") List<String> autocompleteKeys,\n    @Value(\"${zipkin.storage.autocomplete-ttl:3600000}\") int autocompleteTtl,\n    @Value(\"${zipkin.storage.autocomplete-cardinality:20000}\") int autocompleteCardinality) {\n    ElasticsearchStorage.Builder builder = es\n      .toBuilder(new LazyHttpClientImpl(esHttpClientFactory, protocol, initialEndpoints, es,\n        meterRegistry))\n      .namesLookback(namesLookback)\n      .strictTraceId(strictTraceId)\n      .searchEnabled(searchEnabled)\n      .autocompleteKeys(autocompleteKeys)\n      .autocompleteTtl(autocompleteTtl)\n      .autocompleteCardinality(autocompleteCardinality);\n\n    return builder.build();\n  }\n\n  @Bean @Qualifier(QUALIFIER) @Conditional(BasicAuthRequired.class)\n  Consumer<ClientOptionsBuilder> esBasicAuth(\n    @Qualifier(QUALIFIER) BasicCredentials basicCredentials) {\n    return new Consumer<ClientOptionsBuilder>() {\n      @Override public void accept(ClientOptionsBuilder client) {\n        client.decorator(\n          delegate -> new BasicAuthInterceptor(delegate, basicCredentials));\n      }\n\n      @Override public String toString() {\n        return \"BasicAuthCustomizer{basicCredentials=<redacted>}\";\n      }\n    };\n  }\n\n  @Bean @Qualifier(QUALIFIER) @Conditional(BasicAuthRequired.class)\n  BasicCredentials basicCredentials(ZipkinElasticsearchStorageProperties es) {\n    if (isEmpty(es.getUsername()) || isEmpty(es.getPassword())) {\n      return new BasicCredentials();\n    }\n    return new BasicCredentials(es.getUsername(), es.getPassword());\n  }\n\n  @Bean(destroyMethod = \"shutdown\") @Qualifier(QUALIFIER) @Conditional(DynamicRefreshRequired.class)\n  ScheduledExecutorService dynamicCredentialsScheduledExecutorService(\n    @Value(\"${\" + CREDENTIALS_FILE + \"}\") String credentialsFile,\n    @Value(\"${\" + CREDENTIALS_REFRESH_INTERVAL + \"}\") Integer credentialsRefreshInterval,\n    @Qualifier(QUALIFIER) BasicCredentials basicCredentials) throws IOException {\n    ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(\n      new NamedThreadFactory(\"zipkin-load-es-credentials\"));\n    DynamicCredentialsFileLoader credentialsFileLoader =\n      new DynamicCredentialsFileLoader(basicCredentials, credentialsFile);\n    credentialsFileLoader.updateCredentialsFromProperties();\n    ScheduledFuture<?> future = ses.scheduleAtFixedRate(credentialsFileLoader,\n        0, credentialsRefreshInterval, TimeUnit.SECONDS);\n    if (future.isDone()) throw new RuntimeException(\"credential refresh thread didn't start\");\n    return ses;\n  }\n\n  @Bean @Qualifier(QUALIFIER) @ConditionalOnSelfTracing\n  Consumer<ClientOptionsBuilder> esTracing(Optional<HttpTracing> maybeHttpTracing) {\n    if (maybeHttpTracing.isEmpty()) {\n      // TODO: is there a special cased empty consumer we can use here? I suspect debug is cluttered\n      // Alternatively, check why we would ever get here if ConditionalOnSelfTracing matches\n      return client -> {\n      };\n    }\n\n    HttpTracing httpTracing = maybeHttpTracing.get().clientOf(\"elasticsearch\");\n    SpanCustomizer spanCustomizer = CurrentSpanCustomizer.create(httpTracing.tracing());\n\n    return client -> {\n      client.decorator((delegate, ctx, req) -> {\n        // We only need the name if it's available and can unsafely access the partially filled log.\n        RequestLog log = ctx.log().partial();\n        if (log.isAvailable(RequestLogProperty.NAME)) {\n          String name = log.name();\n          if (name != null) {\n            // override the span name if set\n            spanCustomizer.name(name);\n          }\n        }\n        return delegate.execute(ctx, req);\n      });\n      // the tracing decorator is added last so that it encloses the attempt to overwrite the name.\n      client.decorator(BraveClient.newDecorator(httpTracing));\n    };\n  }\n\n  static final class BasicAuthRequired implements Condition {\n    @Override public boolean matches(ConditionContext condition, AnnotatedTypeMetadata ignored) {\n      String userName =\n        condition.getEnvironment().getProperty(USERNAME);\n      String password =\n        condition.getEnvironment().getProperty(PASSWORD);\n      String credentialsFile =\n        condition.getEnvironment().getProperty(CREDENTIALS_FILE);\n      return (!isEmpty(userName) && !isEmpty(password)) || !isEmpty(credentialsFile);\n    }\n  }\n\n  static final class DynamicRefreshRequired implements Condition {\n    @Override public boolean matches(ConditionContext condition, AnnotatedTypeMetadata ignored) {\n      return !isEmpty(condition.getEnvironment().getProperty(CREDENTIALS_FILE));\n    }\n  }\n\n  static ClientFactoryBuilder configureSsl(ClientFactoryBuilder builder, Ssl ssl) throws Exception {\n    final KeyManagerFactory keyManagerFactory = SslUtil.getKeyManagerFactory(ssl);\n    final TrustManagerFactory trustManagerFactory = SslUtil.getTrustManagerFactory(ssl);\n    return builder.tlsCustomizer(sslContextBuilder -> {\n      sslContextBuilder.keyManager(keyManagerFactory);\n      sslContextBuilder.trustManager(trustManagerFactory);\n    });\n  }\n\n  private static boolean isEmpty(String s) {\n    return s == null || s.isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/elasticsearch/ZipkinElasticsearchStorageProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.logging.Logger;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.convert.DurationUnit;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.ElasticsearchStorage.LazyHttpClient;\n\n/**\n * Settings for Elasticsearch client connection\n * <pre>{@code\n * zipkin.storage.elasticsearch:\n *   hosts: localhost:9200\n *   pipeline: my_pipeline\n *   timeout: 10000\n *   index: zipkin\n *   date-separator: -\n *   index-shards: 5\n *   index-replicas: 1\n *   ensure-templates: true\n *   username: username\n *   password: password\n *   credentials-file: credentialsFile\n *   credentials-refresh-interval: 1\n *   http-logging: HEADERS\n *   ssl:\n *     key-store: keystore.p12\n *     key-store-password: changeme\n *     key-store-type: PKCS12\n *     trust-store: truststore.p12\n *     trust-store-password: changeme\n *     trust-store-type: PKCS12\n *   health-check:\n *     enabled: true\n *     http-logging: HEADERS\n *     interval: 3s\n *   template-priority: 0\n * }</pre>\n */\n@ConfigurationProperties(\"zipkin.storage.elasticsearch\")\nclass ZipkinElasticsearchStorageProperties implements Serializable { // for Spark jobs\n  /**\n   * Sets the level of logging for HTTP requests made by the Elasticsearch client. If not set or\n   * none, logging will be disabled.\n   */\n  enum HttpLogging {\n    NONE,\n    BASIC,\n    HEADERS,\n    BODY\n  }\n\n  public static class Ssl {\n    private String keyStore = emptyToNull(System.getProperty(\"javax.net.ssl.keyStore\"));\n    private String keyStorePassword =\n      emptyToNull(System.getProperty(\"javax.net.ssl.keyStorePassword\"));\n    private String keyStoreType = emptyToNull(System.getProperty(\"javax.net.ssl.keyStoreType\"));\n    private String trustStore = emptyToNull(System.getProperty(\"javax.net.ssl.trustStore\"));\n    private String trustStorePassword =\n      emptyToNull(System.getProperty(\"javax.net.ssl.trustStorePassword\"));\n    private String trustStoreType = emptyToNull(System.getProperty(\"javax.net.ssl.trustStoreType\"));\n    /** Disables the verification of server's key certificate chain. */\n    boolean noVerify = false;\n\n    public String getKeyStore() {\n      return keyStore;\n    }\n\n    public void setKeyStore(String keyStore) {\n      this.keyStore = keyStore;\n    }\n\n    public String getKeyStorePassword() {\n      return keyStorePassword;\n    }\n\n    public void setKeyStorePassword(String keyStorePassword) {\n      this.keyStorePassword = keyStorePassword;\n    }\n\n    public String getKeyStoreType() {\n      return keyStoreType;\n    }\n\n    public void setKeyStoreType(String keyStoreType) {\n      this.keyStoreType = keyStoreType;\n    }\n\n    public String getTrustStore() {\n      return trustStore;\n    }\n\n    public void setTrustStore(String trustStore) {\n      this.trustStore = trustStore;\n    }\n\n    public String getTrustStorePassword() {\n      return trustStorePassword;\n    }\n\n    public void setTrustStorePassword(String trustStorePassword) {\n      this.trustStorePassword = trustStorePassword;\n    }\n\n    public String getTrustStoreType() {\n      return trustStoreType;\n    }\n\n    public void setTrustStoreType(String trustStoreType) {\n      this.trustStoreType = trustStoreType;\n    }\n\n    public boolean isNoVerify() {\n      return noVerify;\n    }\n\n    public void setNoVerify(boolean noVerify) {\n      this.noVerify = noVerify;\n    }\n  }\n\n  /**\n   * Configures the health-checking of endpoints by the Elasticsearch client.\n   */\n  public static class HealthCheck {\n    /** Indicates health checking is enabled. */\n    private boolean enabled = true;\n    /** When set, controls the volume of HTTP logging of the Elasticsearch API. */\n    private HttpLogging httpLogging = HttpLogging.NONE;\n\n    /** The time to wait between sending health check requests. */\n    @DurationUnit(ChronoUnit.MILLIS)\n    private Duration interval = Duration.ofSeconds(3);\n\n    public boolean isEnabled() {\n      return enabled;\n    }\n\n    public void setEnabled(boolean enabled) {\n      this.enabled = enabled;\n    }\n\n    public HttpLogging getHttpLogging() {\n      return httpLogging;\n    }\n\n    public void setHttpLogging(HttpLogging httpLogging) {\n      this.httpLogging = httpLogging;\n    }\n\n    public Duration getInterval() {\n      return interval;\n    }\n\n    public void setInterval(Duration interval) {\n      this.interval = interval;\n    }\n  }\n\n  static final Logger log = Logger.getLogger(ZipkinElasticsearchStorageProperties.class.getName());\n\n  private static final long serialVersionUID = 0L;\n\n  /** Indicates the ingest pipeline used before spans are indexed. */\n  private String pipeline;\n  /** A comma separated list of base urls to connect to. */\n  private String hosts = \"http://localhost:9200\";\n  /** The index prefix to use when generating daily index names. */\n  private String index;\n  /** The date separator used to create the index name. */\n  private String dateSeparator;\n  /** Number of shards (horizontal scaling factor) per index. */\n  private Integer indexShards;\n  /** Number of replicas (redundancy factor) per index. */\n  private Integer indexReplicas;\n  /** False disables automatic index template creation. */\n  private Boolean ensureTemplates;\n  /** username used for basic auth. Needed when Shield or X-Pack security is enabled */\n  private String username;\n  /** password used for basic auth. Needed when Shield or X-Pack security is enabled */\n  private String password;\n  /**\n   * credentialsFile is an absolute path refers to a properties-file used to store username and\n   * password\n   */\n  private String credentialsFile;\n  /** Credentials refresh interval (in seconds) */\n  private Integer credentialsRefreshInterval = 1;\n  /** When set, controls the volume of HTTP logging of the Elasticsearch API. */\n  private HttpLogging httpLogging = HttpLogging.NONE;\n  /** Connect, read and write socket timeouts (in milliseconds) for Elasticsearch API requests. */\n  private Integer timeout = 10_000;\n  /** Overrides ssl configuration relating to the Elasticsearch client connection. */\n  private Ssl ssl = new Ssl();\n\n  private Integer maxRequests; // unused\n\n  private HealthCheck healthCheck = new HealthCheck();\n\n  private Integer templatePriority;\n\n  public String getPipeline() {\n    return pipeline;\n  }\n\n  public void setPipeline(String pipeline) {\n    this.pipeline = emptyToNull(pipeline);\n  }\n\n  public String getHosts() {\n    return hosts;\n  }\n\n  public void setHosts(String hosts) {\n    this.hosts = emptyToNull(hosts);\n  }\n\n  public String getIndex() {\n    return index;\n  }\n\n  public Integer getMaxRequests() {\n    return maxRequests;\n  }\n\n  public void setMaxRequests(Integer maxRequests) {\n    this.maxRequests = maxRequests;\n  }\n\n  public void setIndex(String index) {\n    this.index = emptyToNull(index);\n  }\n\n  public Integer getIndexShards() {\n    return indexShards;\n  }\n\n  public void setIndexShards(Integer indexShards) {\n    this.indexShards = indexShards;\n  }\n\n  public Boolean isEnsureTemplates() {\n    return ensureTemplates;\n  }\n\n  public void setEnsureTemplates(Boolean ensureTemplates) {\n    this.ensureTemplates = ensureTemplates;\n  }\n\n  public String getDateSeparator() {\n    return dateSeparator;\n  }\n\n  public void setDateSeparator(String dateSeparator) {\n    String trimmed = dateSeparator.trim();\n    if (trimmed.length() > 1) {\n      throw new IllegalArgumentException(\"dateSeparator must be empty or a single character\");\n    }\n    this.dateSeparator = dateSeparator;\n  }\n\n  public Integer getIndexReplicas() {\n    return indexReplicas;\n  }\n\n  public void setIndexReplicas(Integer indexReplicas) {\n    this.indexReplicas = indexReplicas;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = emptyToNull(username);\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = emptyToNull(password);\n  }\n\n  public String getCredentialsFile() {\n    return credentialsFile;\n  }\n\n  public void setCredentialsFile(final String credentialsFile) {\n    this.credentialsFile = credentialsFile;\n  }\n\n  public Integer getCredentialsRefreshInterval() {\n    return credentialsRefreshInterval;\n  }\n\n  public void setCredentialsRefreshInterval(\n    Integer credentialsRefreshInterval) {\n    this.credentialsRefreshInterval = credentialsRefreshInterval;\n  }\n\n  public HttpLogging getHttpLogging() {\n    return httpLogging;\n  }\n\n  public void setHttpLogging(HttpLogging httpLogging) {\n    this.httpLogging = httpLogging;\n  }\n\n  public Integer getTimeout() {\n    return timeout;\n  }\n\n  public void setTimeout(Integer timeout) {\n    this.timeout = timeout;\n  }\n\n  public HealthCheck getHealthCheck() {\n    return healthCheck;\n  }\n\n  public void setHealthCheck(\n    HealthCheck healthCheck) {\n    this.healthCheck = healthCheck;\n  }\n\n  public Ssl getSsl() {\n    return ssl;\n  }\n\n  public void setSsl(Ssl ssl) {\n    this.ssl = ssl;\n  }\n\n  public Integer getTemplatePriority() { return templatePriority; }\n\n  public void setTemplatePriority(Integer templatePriority) { this.templatePriority = templatePriority; }\n\n  public ElasticsearchStorage.Builder toBuilder(LazyHttpClient httpClient) {\n    ElasticsearchStorage.Builder builder = ElasticsearchStorage.newBuilder(httpClient);\n    if (index != null) builder.index(index);\n    if (dateSeparator != null) {\n      builder.dateSeparator(dateSeparator.isEmpty() ? 0 : dateSeparator.charAt(0));\n    }\n    if (pipeline != null) builder.pipeline(pipeline);\n    if (indexShards != null) builder.indexShards(indexShards);\n    if (indexReplicas != null) builder.indexReplicas(indexReplicas);\n    if (ensureTemplates != null) builder.ensureTemplates(ensureTemplates);\n\n    if (maxRequests != null) {\n      log.warning(\"ES_MAX_REQUESTS is no longer honored. Use STORAGE_THROTTLE_ENABLED instead\");\n    }\n    if (templatePriority != null) builder.templatePriority(templatePriority);\n    return builder;\n  }\n\n  private static String emptyToNull(String s) {\n    return \"\".equals(s) ? null : s;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport com.linecorp.armeria.server.eureka.EurekaUpdatingListener;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\n\n/**\n * Auto-configuration for {@link EurekaUpdatingListener}.\n *\n * <p>See <a href=\"https://armeria.dev/docs/server-service-registration#eureka-based-service-registration-with-eurekaupdatinglistener\">armeria-eureka documentation</a>\n * <p>See <a href=\"https://github.com/Netflix/eureka/wiki/Eureka-REST-operations\">Eureka REST operations</a>\n */\n@ConditionalOnClass(EurekaUpdatingListener.class)\n@Conditional(ZipkinEurekaDiscoveryConfiguration.EurekaServiceUrlSet.class)\n@Configuration(proxyBeanMethods = false)\n@EnableConfigurationProperties(ZipkinEurekaDiscoveryProperties.class)\npublic class ZipkinEurekaDiscoveryConfiguration {\n  @Bean EurekaUpdatingListener eurekaListener(ZipkinEurekaDiscoveryProperties eureka) {\n    return eureka.toBuilder().build();\n  }\n\n  @Bean ArmeriaServerConfigurator eurekaListenerConfigurator(EurekaUpdatingListener listener) {\n    return sb -> sb.serverListener(listener);\n  }\n\n  /**\n   * This condition passes when \"zipkin.discovery.eureka.url\" is set to non-empty.\n   *\n   * <p>This is here because the yaml defaults this property to empty like this, and spring-boot\n   * doesn't have an option to treat empty properties as unset.\n   *\n   * <pre>{@code\n   * url: ${EUREKA_URL:}\n   * }</pre>\n   */\n  static final class EurekaServiceUrlSet implements Condition {\n    @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata a) {\n      return !isEmpty(\n        context.getEnvironment().getProperty(\"zipkin.discovery.eureka.service-url\")) &&\n        notFalse(context.getEnvironment().getProperty(\"zipkin.discovery.eureka.enabled\"));\n    }\n\n    private static boolean isEmpty(String s) {\n      return s == null || s.isEmpty();\n    }\n\n    private static boolean notFalse(String s) {\n      return s == null || !s.equals(\"false\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport com.linecorp.armeria.common.auth.BasicToken;\nimport com.linecorp.armeria.server.eureka.EurekaUpdatingListener;\nimport com.linecorp.armeria.server.eureka.EurekaUpdatingListenerBuilder;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n/**\n * Settings for Eureka registration.\n * <pre>{@code\n * zipkin.discovery.eureka:\n *   service-url: http://eureka:8761/eureka/v2\n *   appName: zipkin\n *   instance-id: zipkin-prod:zipkin:9411\n *   hostname: zipkin-prod\n * }</pre>\n */\n@ConfigurationProperties(\"zipkin.discovery.eureka\")\nclass ZipkinEurekaDiscoveryProperties implements Serializable { // for Spark jobs\n  /**\n   * URL of the Eureka v2 endpoint. e.g. http://eureka:8761/eureka/v2\n   *\n   * <p>Note: When present, {@link URI#getUserInfo() userInfo} credentials will be used for BASIC\n   * authentication. For example, if the URL is \"https://myuser:mypassword@1.1.3.1/eureka/v2/\",\n   * requests to Eureka will authenticate with the user \"myuser\" and password \"mypassword\".\n   */\n  private URI serviceUrl;\n  /** The appName property of this instance of zipkin. */\n  private String appName;\n  /** The instanceId property of this instance of zipkin. */\n  private String instanceId;\n  /** The hostName property of this instance of zipkin. */\n  private String hostname;\n\n  private BasicToken auth;\n\n  public URI getServiceUrl() {\n    return serviceUrl;\n  }\n\n  public void setServiceUrl(URI serviceUrl) {\n    if (serviceUrl == null || serviceUrl.toString().isEmpty()) {\n      this.serviceUrl = null;\n      return;\n    }\n    if (serviceUrl.getUserInfo() != null) {\n      String[] ui = serviceUrl.getUserInfo().split(\":\");\n      if (ui.length == 2) {\n        auth = BasicToken.ofBasic(ui[0], ui[1]);\n      }\n    }\n    this.serviceUrl = stripBaseUrl(serviceUrl);\n  }\n\n  // Strip the credentials and any invalid query or fragment from the URI:\n  // The Eureka API doesn't define any global query params or fragment.\n  // See https://github.com/Netflix/eureka/wiki/Eureka-REST-operations\n  static URI stripBaseUrl(URI serviceUrl) {\n    try {\n      return new URI(serviceUrl.getScheme(), null, serviceUrl.getHost(), serviceUrl.getPort(),\n        serviceUrl.getPath(), null, null);\n    } catch (URISyntaxException e) {\n      throw new IllegalArgumentException(e);\n    }\n  }\n\n  public String getAppName() {\n    return appName;\n  }\n\n  public void setAppName(String appName) {\n    this.appName = emptyToNull(appName);\n  }\n\n  public String getInstanceId() {\n    return instanceId;\n  }\n\n  public void setInstanceId(String instanceId) {\n    this.instanceId = emptyToNull(instanceId);\n  }\n\n  public String getHostname() {\n    return hostname;\n  }\n\n  public void setHostname(String hostname) {\n    this.hostname = emptyToNull(hostname);\n  }\n\n  EurekaUpdatingListenerBuilder toBuilder() {\n    EurekaUpdatingListenerBuilder result = EurekaUpdatingListener.builder(serviceUrl)\n      .homePageUrlPath(\"/zipkin\")\n      .healthCheckUrlPath(\"/health\")\n      .statusPageUrlPath(\"/info\");\n    if (auth != null) result.auth(auth);\n    if (appName != null) result.appName(appName);\n    if (instanceId != null) result.instanceId(instanceId);\n    if (hostname != null) result.hostname(hostname);\n    return result;\n  }\n\n  private static String emptyToNull(String s) {\n    return \"\".equals(s) ? null : s;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/health/ComponentHealth.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\nimport zipkin2.internal.ClosedComponentException;\nimport zipkin2.internal.Nullable;\n\nfinal class ComponentHealth {\n  static final Logger LOGGER = LoggerFactory.getLogger(ComponentHealth.class);\n  static final String STATUS_UP = \"UP\", STATUS_DOWN = \"DOWN\";\n\n  static ComponentHealth ofComponent(Component component) {\n    Throwable t = null;\n    try {\n      CheckResult check = component.check();\n      if (!check.ok()) t = check.error();\n    } catch (Throwable unexpected) {\n      Call.propagateIfFatal(unexpected);\n      t = unexpected;\n    }\n    if (t == null) return new ComponentHealth(component.toString(), STATUS_UP, null);\n    String message = t.getMessage();\n    String error;\n    if (t instanceof ClosedComponentException) {\n      error = message;\n    } else {\n      error = t.getClass().getSimpleName() + (message != null ? \": \" + message : \"\");\n    }\n    LOGGER.debug(error);\n    return new ComponentHealth(component.toString(), STATUS_DOWN, error);\n  }\n\n  final String name;\n  final String status;\n  @Nullable final String error;\n\n  ComponentHealth(String name, String status, String error) {\n    this.name = name;\n    this.status = status;\n    this.error = error;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/health/ZipkinHealthController.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.annotation.Get;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Collectors;\nimport zipkin2.Component;\nimport zipkin2.server.internal.JsonUtil;\n\nimport static zipkin2.server.internal.ZipkinHttpConfiguration.MEDIA_TYPE_ACTUATOR;\nimport static zipkin2.server.internal.health.ComponentHealth.STATUS_DOWN;\nimport static zipkin2.server.internal.health.ComponentHealth.STATUS_UP;\n\npublic class ZipkinHealthController {\n  final List<Component> components;\n\n  ZipkinHealthController(List<Component> components) {\n    this.components = components;\n  }\n\n  @Get(\"/actuator/health\")\n  public CompletableFuture<HttpResponse> getActuatorHealth(ServiceRequestContext ctx) {\n    return health(ctx, MEDIA_TYPE_ACTUATOR);\n  }\n\n  @Get(\"/health\")\n  public CompletableFuture<HttpResponse> getHealth(ServiceRequestContext ctx) {\n    return health(ctx, MediaType.JSON_UTF_8);\n  }\n\n  @SuppressWarnings(\"FutureReturnValueIgnored\")\n  CompletableFuture<HttpResponse> health(ServiceRequestContext ctx, MediaType mediaType) {\n    CompletableFuture<HttpResponse> responseFuture = new CompletableFuture<>();\n    ctx.whenRequestTimingOut().handle((unused, unused2) -> {\n      try {\n        String healthJson = writeJsonError(\"\"\"\n          Timed out computing health status. \\\n          This often means your storage backend is unreachable.\\\n          \"\"\");\n        responseFuture.complete(newHealthResponse(STATUS_DOWN, mediaType, healthJson));\n      } catch (Throwable e) {\n        // Shouldn't happen since we serialize to an array.\n        responseFuture.completeExceptionally(e);\n      }\n      return null;\n    });\n\n    List<CompletableFuture<ComponentHealth>> futures =  components.stream()\n      .map(component ->\n        CompletableFuture.supplyAsync(\n          () -> ComponentHealth.ofComponent(component),\n          // Computing health of a component may block so we make sure to invoke in the blocking\n          // executor.\n          ctx.blockingTaskExecutor()))\n      .toList();\n\n    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\n      .handle((unused, t) -> {\n        if (t != null) {\n          responseFuture.completeExceptionally(t);\n        } else {\n          responseFuture.complete(newHealthResponse(\n            futures.stream()\n              .map(CompletableFuture::join)\n              .collect(Collectors.toList()),\n            mediaType));\n        }\n        return null;\n      });\n\n    return responseFuture;\n  }\n\n  static HttpResponse newHealthResponse(List<ComponentHealth> healths, MediaType mediaType) {\n\n    String overallStatus = STATUS_UP;\n    for (ComponentHealth health : healths) {\n      if (health.status.equals(STATUS_DOWN)) {\n        overallStatus = STATUS_DOWN;\n        break;\n      }\n    }\n\n    final String healthJson;\n    try {\n      healthJson = writeJson(overallStatus, healths);\n    } catch (IOException e) {\n      // Can't have an exception writing to a string.\n      throw new Error(e);\n    }\n    return newHealthResponse(overallStatus, mediaType, healthJson);\n  }\n\n  static HttpResponse newHealthResponse(String status, MediaType mediaType, String healthJson) {\n    HttpStatus code = status.equals(STATUS_UP) ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;\n    return HttpResponse.of(code, mediaType, healthJson);\n  }\n\n  static String writeJsonError(String error) throws IOException {\n    StringWriter writer = new StringWriter();\n    try (JsonGenerator generator = JsonUtil.createGenerator(writer)) {\n      generator.writeStartObject();\n      generator.writeStringField(\"status\", STATUS_DOWN);\n      generator.writeObjectFieldStart(\"zipkin\");\n      generator.writeStringField(\"status\", STATUS_DOWN);\n      generator.writeObjectFieldStart(\"details\");\n      generator.writeStringField(\"error\", error);\n      generator.writeEndObject(); // .zipkin.details\n      generator.writeEndObject(); // .zipkin\n      generator.writeEndObject(); // .\n    }\n    return writer.toString();\n  }\n\n  static String writeJson(String overallStatus, List<ComponentHealth> healths) throws IOException {\n    StringWriter writer = new StringWriter();\n    try (JsonGenerator generator = JsonUtil.createGenerator(writer)) {\n      generator.writeStartObject();\n      generator.writeStringField(\"status\", overallStatus);\n      generator.writeObjectFieldStart(\"zipkin\");\n      generator.writeStringField(\"status\", overallStatus);\n      generator.writeObjectFieldStart(\"details\");\n\n      for (ComponentHealth health : healths) {\n        generator.writeObjectFieldStart(health.name);\n        generator.writeStringField(\"status\", health.status);\n\n        if (health.status.equals(STATUS_DOWN)) {\n          generator.writeObjectFieldStart(\"details\");\n          generator.writeStringField(\"error\", health.error);\n          generator.writeEndObject(); // .zipkin.details.healthName.details\n        }\n\n        generator.writeEndObject(); // .zipkin.details.healthName\n      }\n\n      generator.writeEndObject(); // .zipkin.details\n      generator.writeEndObject(); // .zipkin\n      generator.writeEndObject(); // .\n    }\n    return writer.toString();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.kafka;\n\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.collector.kafka.KafkaCollector;\nimport zipkin2.storage.StorageComponent;\n\n/**\n * This collector consumes a topic, decodes spans from thrift messages and stores them subject to\n * sampling policy.\n */\n@ConditionalOnClass(KafkaCollector.class)\n@Conditional(ZipkinKafkaCollectorConfiguration.KafkaBootstrapServersSet.class)\n@EnableConfigurationProperties(ZipkinKafkaCollectorProperties.class)\npublic class ZipkinKafkaCollectorConfiguration { // makes simple type name unique for /actuator/conditions\n\n  @Bean(initMethod = \"start\")\n  KafkaCollector kafka(\n      ZipkinKafkaCollectorProperties properties,\n      CollectorSampler sampler,\n      CollectorMetrics metrics,\n      StorageComponent storage) {\n    return properties.toBuilder().sampler(sampler).metrics(metrics).storage(storage).build();\n  }\n  /**\n   * This condition passes when {@link ZipkinKafkaCollectorProperties#getBootstrapServers()} is set\n   * to non-empty.\n   *\n   * <p>This is here because the yaml defaults this property to empty like this, and spring-boot\n   * doesn't have an option to treat empty properties as unset.\n   *\n   * <pre>{@code\n   * bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:}\n   * }</pre>\n   */\n  static final class KafkaBootstrapServersSet implements Condition {\n    @Override\n    public boolean matches(ConditionContext context, AnnotatedTypeMetadata a) {\n      return !isEmpty(\n        context.getEnvironment().getProperty(\"zipkin.collector.kafka.bootstrap-servers\")) &&\n        notFalse(context.getEnvironment().getProperty(\"zipkin.collector.kafka.enabled\"));\n    }\n\n    private static boolean isEmpty(String s) {\n      return s == null || s.isEmpty();\n    }\n\n    private static boolean notFalse(String s){\n      return s == null || !s.equals(\"false\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.kafka;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport zipkin2.collector.kafka.KafkaCollector;\n\n@ConfigurationProperties(\"zipkin.collector.kafka\")\nclass ZipkinKafkaCollectorProperties {\n  /** Comma-separated list of Kafka bootstrap servers in the form [host]:[port],... */\n  private String bootstrapServers;\n  /** Kafka consumer group id used by the collector. */\n  private String groupId;\n  /** Kafka topic span data will be retrieved from. */\n  private String topic;\n  /** Number of Kafka consumer threads to run. */\n  private Integer streams;\n  /** Additional Kafka consumer configuration. */\n  private Map<String, String> overrides = new LinkedHashMap<>();\n\n  public String getBootstrapServers() {\n    return bootstrapServers;\n  }\n\n  public void setBootstrapServers(String bootstrapServers) {\n    this.bootstrapServers = emptyToNull(bootstrapServers);\n  }\n\n  public String getGroupId() {\n    return groupId;\n  }\n\n  public void setGroupId(String groupId) {\n    this.groupId = emptyToNull(groupId);\n  }\n\n  public String getTopic() {\n    return topic;\n  }\n\n  public void setTopic(String topic) {\n    this.topic = emptyToNull(topic);\n  }\n\n  public Integer getStreams() {\n    return streams;\n  }\n\n  public void setStreams(Integer streams) {\n    this.streams = streams;\n  }\n\n  public Map<String, String> getOverrides() {\n    return overrides;\n  }\n\n  public void setOverrides(Map<String, String> overrides) {\n    this.overrides = overrides;\n  }\n\n  public KafkaCollector.Builder toBuilder() {\n    final KafkaCollector.Builder result = KafkaCollector.builder();\n    if (bootstrapServers != null) result.bootstrapServers(bootstrapServers);\n    if (groupId != null) result.groupId(groupId);\n    if (topic != null) result.topic(topic);\n    if (streams != null) result.streams(streams);\n    if (overrides != null) result.overrides(overrides);\n    return result;\n  }\n\n  private static String emptyToNull(String s) {\n    return \"\".equals(s) ? null : s;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/mysql/ZipkinMySQLStorageConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.mysql;\n\nimport java.util.List;\nimport java.util.concurrent.Executor;\nimport javax.sql.DataSource;\nimport org.jooq.ExecuteListenerProvider;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.mysql.v1.MySQLStorage;\n\n@EnableConfigurationProperties(ZipkinMySQLStorageProperties.class)\n@ConditionalOnClass(MySQLStorage.class)\n@ConditionalOnProperty(name = \"zipkin.storage.type\", havingValue = \"mysql\")\n@ConditionalOnMissingBean(StorageComponent.class)\n@Import(ZipkinSelfTracingMySQLStorageConfiguration.class)\npublic class ZipkinMySQLStorageConfiguration {\n  @Autowired(required = false) ZipkinMySQLStorageProperties mysql;\n  @Autowired(required = false) ExecuteListenerProvider mysqlListener;\n\n  @Bean @ConditionalOnMissingBean\n  Executor mysqlExecutor() {\n    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n    executor.setThreadNamePrefix(\"ZipkinMySQLStorage-\");\n    executor.initialize();\n    return executor;\n  }\n\n  @Bean @ConditionalOnMissingBean\n  DataSource mysqlDataSource() {\n    return mysql.toDataSource();\n  }\n\n  @Bean StorageComponent storage(\n    Executor mysqlExecutor,\n    DataSource mysqlDataSource,\n    @Value(\"${zipkin.storage.strict-trace-id:true}\") boolean strictTraceId,\n    @Value(\"${zipkin.storage.search-enabled:true}\") boolean searchEnabled,\n    @Value(\"${zipkin.storage.autocomplete-keys:}\") List<String> autocompleteKeys) {\n    return MySQLStorage.newBuilder()\n      .strictTraceId(strictTraceId)\n      .searchEnabled(searchEnabled)\n      .autocompleteKeys(autocompleteKeys)\n      .executor(mysqlExecutor)\n      .datasource(mysqlDataSource)\n      .listenerProvider(mysqlListener)\n      .build();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/mysql/ZipkinMySQLStorageProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.mysql;\n\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.io.Serializable;\nimport javax.sql.DataSource;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.util.StringUtils;\n\n@ConfigurationProperties(\"zipkin.storage.mysql\")\nclass ZipkinMySQLStorageProperties implements Serializable { // for Spark jobs\n  private static final long serialVersionUID = 0L;\n\n  private String jdbcUrl;\n  private String host = \"localhost\";\n  private int port = 3306;\n  private String username;\n  private String password;\n  private String db = \"zipkin\";\n  private int maxActive = 10;\n  private boolean useSsl;\n\n  public String getJdbcUrl() {\n    return jdbcUrl;\n  }\n\n  public void setJdbcUrl(String jdbcUrl) {\n    this.jdbcUrl = jdbcUrl;\n  }\n\n  public String getHost() {\n    return host;\n  }\n\n  public void setHost(String host) {\n    this.host = host;\n  }\n\n  public int getPort() {\n    return port;\n  }\n\n  public void setPort(int port) {\n    this.port = port;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = \"\".equals(username) ? null : username;\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = \"\".equals(password) ? null : password;\n  }\n\n  public String getDb() {\n    return db;\n  }\n\n  public void setDb(String db) {\n    this.db = db;\n  }\n\n  public int getMaxActive() {\n    return maxActive;\n  }\n\n  public void setMaxActive(int maxActive) {\n    this.maxActive = maxActive;\n  }\n\n  public boolean isUseSsl() {\n    return useSsl;\n  }\n\n  public void setUseSsl(boolean useSsl) {\n    this.useSsl = useSsl;\n  }\n\n  public DataSource toDataSource() {\n    HikariDataSource result = new HikariDataSource();\n    result.setDriverClassName(\"org.mariadb.jdbc.Driver\");\n    result.setJdbcUrl(determineJdbcUrl());\n    result.setMaximumPoolSize(getMaxActive());\n    result.setUsername(getUsername());\n    result.setPassword(getPassword());\n    return result;\n  }\n\n  private String determineJdbcUrl() {\n    if (StringUtils.hasText(getJdbcUrl())) {\n      return getJdbcUrl();\n    }\n\n    String url = \"jdbc:mariadb://\"\n        + getHost() + \":\" + getPort()\n        + \"/\" + getDb()\n        + \"?autoReconnect=true\"\n        + \"&useSSL=\" + isUseSsl()\n        + \"&useUnicode=yes&characterEncoding=UTF-8\";\n    return url;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/mysql/ZipkinSelfTracingMySQLStorageConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.mysql;\n\nimport brave.Span;\nimport brave.propagation.CurrentTraceContext;\nimport brave.propagation.ThreadLocalSpan;\nimport com.linecorp.armeria.common.RequestContext;\nimport java.util.concurrent.Executor;\nimport org.jooq.ExecuteContext;\nimport org.jooq.ExecuteListenerProvider;\nimport org.jooq.impl.DefaultExecuteListener;\nimport org.jooq.impl.DefaultExecuteListenerProvider;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport zipkin2.server.internal.ConditionalOnSelfTracing;\n\n/** Sets up the MySQL tracing in Brave as an initialization. */\n@ConditionalOnSelfTracing\n@ConditionalOnProperty(name = \"zipkin.storage.type\", havingValue = \"mysql\")\nclass ZipkinSelfTracingMySQLStorageConfiguration extends DefaultExecuteListener {\n\n  @Autowired ZipkinMySQLStorageProperties mysql;\n  @Autowired CurrentTraceContext currentTraceContext;\n  @Autowired ThreadLocalSpan threadLocalSpan;\n\n  @Bean ExecuteListenerProvider mysqlListener() {\n    return new DefaultExecuteListenerProvider(this);\n  }\n\n  @Bean Executor mysqlExecutor() {\n    return makeContextAware(\n      new ZipkinMySQLStorageConfiguration().mysqlExecutor(),\n      currentTraceContext\n    );\n  }\n\n  /**\n   * Decorates the input such that the {@link RequestContext#current() current request context} and\n   * the and the {@link CurrentTraceContext#get() current trace context} at assembly time is made\n   * current when task is executed.\n   */\n  static Executor makeContextAware(Executor delegate, CurrentTraceContext currentTraceContext) {\n    class TracingCurrentRequestContextExecutor implements Executor {\n      @Override public void execute(Runnable task) {\n        delegate.execute(RequestContext.current().makeContextAware(currentTraceContext.wrap(task)));\n      }\n    }\n    return new TracingCurrentRequestContextExecutor();\n  }\n\n  @Override public void renderEnd(ExecuteContext ctx) {\n    // don't start new traces (to prevent amplifying writes to local storage)\n    if (currentTraceContext.get() == null) return;\n\n    // Gets the next span (and places it in scope) so code between here and postProcess can read it\n    Span span = threadLocalSpan.next();\n    if (span == null || span.isNoop()) return;\n\n    String sql = ctx.sql();\n    int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT\n    span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));\n    span.tag(\"sql.query\", sql);\n    span.remoteServiceName(\"mysql\");\n    span.remoteIpAndPort(mysql.getHost(), mysql.getPort());\n    span.start();\n  }\n\n  @Override public void executeEnd(ExecuteContext ctx) {\n    Span span = ThreadLocalSpan.CURRENT_TRACER.remove();\n    if (span == null || span.isNoop()) return;\n    if (ctx.sqlException() != null) span.error(ctx.sqlException());\n    span.finish();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/package-info.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n/**\n * Classes in this package are considered internal details to Zipkin's server and are unsupported\n * unless integrated with our server build.\n */\npackage zipkin2.server.internal;\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/prometheus/ZipkinMetricsController.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.prometheus;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.server.annotation.Get;\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.Meter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.prometheus.client.CollectorRegistry;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport zipkin2.server.internal.JsonUtil;\n\npublic class ZipkinMetricsController {\n\n  final MeterRegistry meterRegistry;\n  final CollectorRegistry collectorRegistry;\n\n  ZipkinMetricsController(MeterRegistry meterRegistry, CollectorRegistry collectorRegistry) {\n    this.meterRegistry = meterRegistry;\n    this.collectorRegistry = collectorRegistry;\n  }\n\n  // Extracts Zipkin metrics to provide backward compatibility\n  @Get(\"/metrics\")\n  public HttpResponse fetchMetricsFromMicrometer() throws IOException {\n    StringWriter writer = new StringWriter();\n    JsonGenerator generator = JsonUtil.createGenerator(writer);\n    generator.writeStartObject();\n    // Get the Zipkin Custom meters for constructing the Metrics endpoint\n    for (Meter meter : meterRegistry.getMeters()) {\n      String name = meter.getId().getName();\n      if (!name.startsWith(\"zipkin_collector\")) continue;\n      String transport = meter.getId().getTag(\"transport\");\n      if (transport == null) continue;\n\n      Meter.Type type = meter.getId().getType();\n      if (type == Meter.Type.COUNTER) {\n        generator.writeNumberField(\"counter.\" + name + \".\" + transport, ((Counter) meter).count());\n      } else if (type == Meter.Type.GAUGE) {\n        generator.writeNumberField(\"gauge.\" + name + \".\" + transport, ((Gauge) meter).value());\n      } // We only use counters and gauges\n    }\n    generator.writeEndObject();\n    generator.flush(); // instead of using try/finally as extra indent causes lines to wrap\n    return HttpResponse.of(HttpStatus.OK, MediaType.JSON, writer.toString());\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.prometheus;\n\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.RequestContext;\nimport com.linecorp.armeria.common.logging.RequestLog;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.Route;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport com.linecorp.armeria.server.SimpleDecoratingHttpService;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport io.micrometer.core.instrument.Clock;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Tag;\nimport io.micrometer.core.instrument.Timer;\nimport io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;\nimport io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;\nimport io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;\nimport io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;\nimport io.micrometer.core.instrument.binder.system.ProcessorMetrics;\nimport io.micrometer.prometheus.PrometheusConfig;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport io.netty.util.AttributeKey;\nimport io.prometheus.client.CollectorRegistry;\nimport java.util.Arrays;\nimport java.util.concurrent.TimeUnit;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.util.StringUtils;\n\n@Configuration(proxyBeanMethods=false)\npublic class ZipkinPrometheusMetricsConfiguration {\n  // from io.micrometer.spring.web.servlet.WebMvcTags\n  private static final Tag URI_NOT_FOUND = Tag.of(\"uri\", \"NOT_FOUND\");\n  private static final Tag URI_REDIRECTION = Tag.of(\"uri\", \"REDIRECTION\");\n  private static final Tag URI_TRACE_V2 = Tag.of(\"uri\", \"/api/v2/trace/{traceId}\");\n  // single-page app requests are forwarded to index: ZipkinUiConfiguration.forwardUiEndpoints\n  private static final Tag URI_CROSSROADS = Tag.of(\"uri\", \"/zipkin/index.html\");\n\n  final String metricName;\n\n  // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-metrics-spring-mvc\n  ZipkinPrometheusMetricsConfiguration(\n    @Value(\"${management.metrics.web.server.requests-metric-name:http.server.requests}\")\n      String metricName\n  ) {\n    this.metricName = metricName;\n  }\n\n  @Bean @ConditionalOnMissingBean public Clock clock() {\n    return Clock.SYSTEM;\n  }\n\n  @Bean @ConditionalOnMissingBean public PrometheusConfig config() {\n    return PrometheusConfig.DEFAULT;\n  }\n\n  @Bean @ConditionalOnMissingBean public CollectorRegistry registry() {\n    return new CollectorRegistry(true);\n  }\n\n  @Bean @ConditionalOnMissingBean public PrometheusMeterRegistry prometheusMeterRegistry(\n    PrometheusConfig config, CollectorRegistry registry, Clock clock) {\n    PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(config, registry, clock);\n    new JvmMemoryMetrics().bindTo(meterRegistry);\n    new JvmGcMetrics().bindTo(meterRegistry);\n    new JvmThreadMetrics().bindTo(meterRegistry);\n    new ClassLoaderMetrics().bindTo(meterRegistry);\n    new ProcessorMetrics().bindTo(meterRegistry);\n    return meterRegistry;\n  }\n\n  // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-metrics-spring-mvc\n  @Bean ArmeriaServerConfigurator httpRequestDurationConfigurator(MeterRegistry registry) {\n    return serverBuilder -> serverBuilder.routeDecorator()\n      .pathPrefix(\"/zipkin/api\")\n      .pathPrefix(\"/api\")\n      .build(s -> new MetricCollectingService(s, registry, metricName));\n  }\n\n  // We need to make sure not-found requests are still handled by a service to be decorated for\n  // adding metrics. We add a lower precedence path mapping so anything not mapped by another\n  // service is handled by this.\n  @Bean\n  @Order(1)\n  ArmeriaServerConfigurator notFoundMetricCollector() {\n    // Use glob instead of catch-all to avoid adding it to the trie router.\n    return sb -> sb.service(Route.builder().glob(\"/**\").build(),\n      (ctx, req) -> HttpResponse.of(HttpStatus.NOT_FOUND));\n  }\n\n  static final class MetricCollectingService extends SimpleDecoratingHttpService {\n    final MeterRegistry registry;\n    final String metricName;\n\n    MetricCollectingService(HttpService delegate, MeterRegistry registry, String metricName) {\n      super(delegate);\n      this.registry = registry;\n      this.metricName = metricName;\n    }\n\n    @Override\n    public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {\n      setup(ctx, registry, metricName);\n      return unwrap().serve(ctx, req);\n    }\n  }\n\n  // A variable to make sure setup method is not called twice.\n  private static final AttributeKey<Boolean> PROMETHEUS_METRICS_SET =\n    AttributeKey.valueOf(Boolean.class, \"PROMETHEUS_METRICS_SET\");\n\n  @SuppressWarnings(\"FutureReturnValueIgnored\") // no known action to take following .thenAccept\n  public static void setup(RequestContext ctx, MeterRegistry registry, String metricName) {\n    if (ctx.hasAttr(PROMETHEUS_METRICS_SET)) {\n      return;\n    }\n    ctx.setAttr(PROMETHEUS_METRICS_SET, true);\n    ctx.log().whenComplete().thenAccept(log -> getTimeBuilder(log, metricName).register(registry)\n      .record(log.totalDurationNanos(), TimeUnit.NANOSECONDS));\n  }\n\n  private static Timer.Builder getTimeBuilder(RequestLog requestLog, String metricName) {\n    return Timer.builder(metricName)\n      .tags(getTags(requestLog))\n      .description(\"Response time histogram\")\n      .publishPercentileHistogram();\n  }\n\n  private static Iterable<Tag> getTags(RequestLog requestLog) {\n    return Arrays.asList(Tag.of(\"method\", requestLog.requestHeaders().method().toString())\n      , uri(requestLog)\n      , Tag.of(\"status\", Integer.toString(requestLog.responseHeaders().status().code()))\n    );\n  }\n\n  /** Ensure metrics cardinality doesn't blow up on variables */\n  private static Tag uri(RequestLog requestLog) {\n    int status = requestLog.responseHeaders().status().code();\n    if (status > 299 && status < 400) return URI_REDIRECTION;\n    if (status == 404) return URI_NOT_FOUND;\n\n    String uri = getPathInfo(requestLog);\n    if (uri.startsWith(\"/zipkin\")) {\n      if (uri.equals(\"/zipkin/\") || uri.equals(\"/zipkin\")\n        || uri.startsWith(\"/zipkin/traces/\")\n        || uri.equals(\"/zipkin/dependency\")\n        || uri.equals(\"/zipkin/traceViewer\")) {\n        return URI_CROSSROADS; // single-page app route\n      }\n\n      // un-map UI's api route\n      if (uri.startsWith(\"/zipkin/api\")) {\n        uri = uri.replaceFirst(\"/zipkin\", \"\");\n      }\n    }\n    // handle templated routes instead of exploding on trace ID cardinality\n    if (uri.startsWith(\"/api/v2/trace/\")) return URI_TRACE_V2;\n    return Tag.of(\"uri\", uri);\n  }\n\n  // from io.micrometer.spring.web.servlet.WebMvcTags\n  static String getPathInfo(RequestLog requestLog) {\n    String uri = requestLog.context().path();\n    if (!StringUtils.hasText(uri)) return \"/\";\n    return uri.replaceAll(\"//+\", \"/\")\n      .replaceAll(\"/$\", \"\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/pulsar/ZipkinPulsarCollectorConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.pulsar;\n\nimport org.springframework.boot.autoconfigure.condition.AllNestedConditions;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.collector.pulsar.PulsarCollector;\nimport zipkin2.storage.StorageComponent;\n\nimport static io.micrometer.common.util.StringUtils.isEmpty;\n\n/** Auto-configuration for {@link PulsarCollector}. */\n@ConditionalOnClass(PulsarCollector.class)\n@Conditional(ZipkinPulsarCollectorConfiguration.PulsarConditions.class)\n@EnableConfigurationProperties(ZipkinPulsarCollectorProperties.class)\npublic class ZipkinPulsarCollectorConfiguration {\n\n  @Bean(initMethod = \"start\")\n  PulsarCollector pulsar(\n      ZipkinPulsarCollectorProperties properties,\n      CollectorSampler sampler,\n      CollectorMetrics metrics,\n      StorageComponent storage\n  ) {\n    return properties.toBuilder().sampler(sampler).metrics(metrics).storage(storage).build();\n  }\n\n  /**\n   * This condition passes when {@link ZipkinPulsarCollectorProperties#getServiceUrl()} is set\n   * to non-empty.\n   *\n   * <p>This is here because the yaml defaults this property to empty like this, and spring-boot\n   * doesn't have an option to treat empty properties as unset.\n   *\n   * <pre>{@code\n   * service-url: ${PULSAR_SERVICE_URL:}\n   * }</pre>\n   */\n  static final class PulsarConditions extends AllNestedConditions {\n\n    PulsarConditions() {\n      super(ConfigurationPhase.REGISTER_BEAN);\n    }\n\n    @ConditionalOnProperty(prefix = \"zipkin.collector.pulsar\", name = \"enabled\",\n        havingValue = \"true\", matchIfMissing = true)\n    private static final class PulsarEnabledCondition {\n    }\n\n    @Conditional(PulsarServiceUrlCondition.class)\n    private static final class PulsarServiceUrlCondition implements Condition {\n      @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {\n        String serviceUrl = context.getEnvironment().getProperty(\"zipkin.collector.pulsar.service-url\");\n        return !isEmpty(serviceUrl);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/pulsar/ZipkinPulsarCollectorProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.pulsar;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport zipkin2.collector.pulsar.PulsarCollector;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/** Properties for configuring and building a {@link PulsarCollector}. */\n@ConfigurationProperties(\"zipkin.collector.pulsar\")\nclass ZipkinPulsarCollectorProperties {\n\n  /** The service URL for the Pulsar service. */\n  private String serviceUrl;\n  /** Pulsar topic span data will be retrieved from. */\n  private String topic;\n  /** Specify the subscription name for this consumer. */\n  private String subscriptionName;\n  /** Number of concurrent span consumers */\n  private Integer concurrency;\n  /** Additional Pulsar client configuration. */\n  private Map<String, Object> clientProps = new LinkedHashMap<>();\n  /** Additional Pulsar consumer configuration. */\n  private Map<String, Object> consumerProps = new LinkedHashMap<>();\n\n  public String getServiceUrl() {\n    return serviceUrl;\n  }\n\n  public void setServiceUrl(String serviceUrl) {\n    this.serviceUrl = serviceUrl;\n  }\n\n  public String getTopic() {\n    return topic;\n  }\n\n  public void setTopic(String topic) {\n    this.topic = topic;\n  }\n\n  public String getSubscriptionName() {\n    return subscriptionName;\n  }\n\n  public void setSubscriptionName(String subscriptionName) {\n    this.subscriptionName = subscriptionName;\n  }\n\n  public Integer getConcurrency() {\n    return concurrency;\n  }\n\n  public void setConcurrency(Integer concurrency) {\n    this.concurrency = concurrency;\n  }\n\n  public Map<String, Object> getClientProps() {\n    return clientProps;\n  }\n\n  public void setClientProps(Map<String, Object> clientProps) {\n    this.clientProps = clientProps;\n  }\n\n  public Map<String, Object> getConsumerProps() {\n    return consumerProps;\n  }\n\n  public void setConsumerProps(Map<String, Object> consumerProps) {\n    this.consumerProps = consumerProps;\n  }\n\n  public PulsarCollector.Builder toBuilder() {\n    final PulsarCollector.Builder result = PulsarCollector.builder();\n    if (serviceUrl != null) {\n      result.serviceUrl(serviceUrl);\n    }\n    if (topic != null) result.topic(topic);\n    if (concurrency != null) result.concurrency(concurrency);\n    if (subscriptionName != null) result.subscriptionName(subscriptionName);\n    if (!clientProps.isEmpty()) result.clientProps(clientProps);\n    if (!consumerProps.isEmpty()) result.consumerProps(consumerProps);\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.rabbitmq;\n\nimport java.net.URISyntaxException;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.collector.rabbitmq.RabbitMQCollector;\nimport zipkin2.storage.StorageComponent;\n\n/** Auto-configuration for {@link RabbitMQCollector}. */\n@ConditionalOnClass(RabbitMQCollector.class)\n@Conditional(ZipkinRabbitMQCollectorConfiguration.RabbitMQAddressesOrUriSet.class)\n@EnableConfigurationProperties(ZipkinRabbitMQCollectorProperties.class)\npublic class ZipkinRabbitMQCollectorConfiguration {\n\n  @Bean(initMethod = \"start\")\n  RabbitMQCollector rabbitMq(\n      ZipkinRabbitMQCollectorProperties properties,\n      CollectorSampler sampler,\n      CollectorMetrics metrics,\n      StorageComponent storage)\n      throws NoSuchAlgorithmException, KeyManagementException, URISyntaxException {\n    return properties.toBuilder().sampler(sampler).metrics(metrics).storage(storage).build();\n  }\n  /**\n   * This condition passes when {@link ZipkinRabbitMQCollectorProperties#getAddresses()} or {@link\n   * ZipkinRabbitMQCollectorProperties#getUri()} is set to a non-empty value.\n   *\n   * <p>This is here because the yaml defaults this property to empty like this, and Spring Boot\n   * doesn't have an option to treat empty properties as unset.\n   *\n   * <pre>{@code\n   * addresses: ${RABBIT_ADDRESSES:}\n   * uri: ${RABBIT_URI:}\n   * }</pre>\n   */\n  static final class RabbitMQAddressesOrUriSet implements Condition {\n    @Override\n    public boolean matches(ConditionContext context, AnnotatedTypeMetadata a) {\n      return (!isEmpty(context.getEnvironment().getProperty(\"zipkin.collector.rabbitmq.addresses\"))\n        || !isEmpty(context.getEnvironment().getProperty(\"zipkin.collector.rabbitmq.uri\"))) &&\n        notFalse(context.getEnvironment().getProperty(\"zipkin.collector.rabbitmq.enabled\"));\n    }\n\n    private static boolean isEmpty(String s) {\n      return s == null || s.isEmpty();\n    }\n\n    private static boolean notFalse(String s){\n      return s == null || !s.equals(\"false\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.rabbitmq;\n\nimport com.rabbitmq.client.ConnectionFactory;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.List;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport zipkin2.collector.rabbitmq.RabbitMQCollector;\n\n/** Properties for configuring and building a {@link RabbitMQCollector}. */\n@ConfigurationProperties(\"zipkin.collector.rabbitmq\")\nclass ZipkinRabbitMQCollectorProperties {\n  static final URI EMPTY_URI = URI.create(\"\");\n\n  /** RabbitMQ server addresses in the form of a (comma-separated) list of host:port pairs */\n  private List<String> addresses;\n  /** Number of concurrent consumers */\n  private Integer concurrency = 1;\n  /** TCP connection timeout in milliseconds */\n  private Integer connectionTimeout;\n  /** RabbitMQ user password */\n  private String password;\n  /** RabbitMQ queue from which to collect the Zipkin spans */\n  private String queue;\n  /** RabbitMQ username */\n  private String username;\n  /** RabbitMQ virtual host */\n  private String virtualHost;\n  /** Flag to use SSL */\n  private Boolean useSsl;\n  /**\n   * RabbitMQ URI spec-compliant URI to connect to the RabbitMQ server. When used, other connection\n   * properties will be ignored.\n   */\n  private URI uri;\n\n  public List<String> getAddresses() {\n    return addresses;\n  }\n\n  public void setAddresses(List<String> addresses) {\n    this.addresses = addresses;\n  }\n\n  public int getConcurrency() {\n    return concurrency;\n  }\n\n  public void setConcurrency(int concurrency) {\n    this.concurrency = concurrency;\n  }\n\n  public Integer getConnectionTimeout() {\n    return connectionTimeout;\n  }\n\n  public void setConnectionTimeout(Integer connectionTimeout) {\n    this.connectionTimeout = connectionTimeout;\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = password;\n  }\n\n  public String getQueue() {\n    return queue;\n  }\n\n  public void setQueue(String queue) {\n    this.queue = queue;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = username;\n  }\n\n  public String getVirtualHost() {\n    return virtualHost;\n  }\n\n  public void setVirtualHost(String virtualHost) {\n    this.virtualHost = virtualHost;\n  }\n\n  public Boolean getUseSsl() {\n    return useSsl;\n  }\n\n  public void setUseSsl(Boolean useSsl) {\n    this.useSsl = useSsl;\n  }\n\n  public URI getUri() {\n    return uri;\n  }\n\n  public void setUri(URI uri) {\n    if (EMPTY_URI.equals(uri)) return;\n    this.uri = uri;\n  }\n\n  public RabbitMQCollector.Builder toBuilder()\n      throws KeyManagementException, NoSuchAlgorithmException, URISyntaxException {\n    final RabbitMQCollector.Builder result = RabbitMQCollector.builder();\n    ConnectionFactory connectionFactory = new ConnectionFactory();\n    if (concurrency != null) result.concurrency(concurrency);\n    if (connectionTimeout != null) connectionFactory.setConnectionTimeout(connectionTimeout);\n    if (queue != null) result.queue(queue);\n\n    if (uri != null) {\n      connectionFactory.setUri(uri);\n    } else {\n      if (addresses != null) result.addresses(addresses);\n      if (password != null) connectionFactory.setPassword(password);\n      if (username != null) connectionFactory.setUsername(username);\n      if (virtualHost != null) connectionFactory.setVirtualHost(virtualHost);\n      if (useSsl != null && useSsl) connectionFactory.useSslProtocol();\n    }\n    result.connectionFactory(connectionFactory);\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/scribe/ZipkinScribeCollectorConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.scribe;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.collector.scribe.ScribeCollector;\nimport zipkin2.storage.StorageComponent;\n\n/**\n * This collector accepts Scribe logs in a specified category. Each log entry is expected to contain\n * a single span, which is TBinaryProtocol big-endian, then base64 encoded. Decoded spans are stored\n * asynchronously.\n */\n@ConditionalOnClass(ScribeCollector.class)\n@ConditionalOnProperty(value = \"zipkin.collector.scribe.enabled\", havingValue = \"true\")\npublic class ZipkinScribeCollectorConfiguration {\n  /** The init method will block until the scribe port is listening, or crash on port conflict */\n  @Bean(initMethod = \"start\")\n  ScribeCollector scribe(\n    @Value(\"${zipkin.collector.scribe.category:zipkin}\") String category,\n    @Value(\"${zipkin.collector.scribe.port:9410}\") int port,\n    CollectorSampler sampler,\n    CollectorMetrics metrics,\n    StorageComponent storage) {\n    return ScribeCollector.newBuilder()\n      .category(category)\n      .port(port)\n      .sampler(sampler)\n      .metrics(metrics)\n      .storage(storage)\n      .build();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/throttle/LimiterMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport zipkin2.collector.CollectorMetrics;\n\n/** Follows the same naming convention as {@link CollectorMetrics} */\nfinal class LimiterMetrics {\n  final Counter requests, requestsSucceeded, requestsIgnored, requestsDropped;\n\n  LimiterMetrics(MeterRegistry registry) {\n    requests = Counter.builder(\"zipkin_storage.throttle.requests\")\n      .description(\"cumulative amount of limiter requests acquired\")\n      .register(registry);\n    requestsSucceeded = Counter.builder(\"zipkin_storage.throttle.requests_succeeded\")\n      .description(\"cumulative amount of limiter requests acquired that later succeeded\")\n      .register(registry);\n    requestsDropped =\n      Counter.builder(\"zipkin_storage.throttle.requests_dropped\")\n        .description(\n          \"cumulative amount of limiter requests acquired that later dropped due to capacity\")\n        .register(registry);\n    requestsIgnored =\n      Counter.builder(\"zipkin_storage.throttle.requests_ignored\")\n        .description(\n          \"cumulative amount of limiter requests acquired that later dropped not due to capacity\")\n        .register(registry);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/throttle/MicrometerThrottleMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport com.netflix.concurrency.limits.limiter.AbstractLimiter;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport zipkin2.server.internal.MicrometerCollectorMetrics;\n\n/** Follows the same naming convention as {@link MicrometerCollectorMetrics} */\nfinal class MicrometerThrottleMetrics {\n  final MeterRegistry registryInstance;\n\n  MicrometerThrottleMetrics(MeterRegistry registryInstance) {\n    this.registryInstance = registryInstance;\n  }\n\n  void bind(ThreadPoolExecutor pool) {\n    Gauge.builder(\"zipkin_storage.throttle.concurrency\", pool::getCorePoolSize)\n      .description(\"number of threads running storage requests\")\n      .register(registryInstance);\n    Gauge.builder(\"zipkin_storage.throttle.queue_size\", pool.getQueue()::size)\n      .description(\"number of items queued waiting for access to storage\")\n      .register(registryInstance);\n  }\n\n  void bind(AbstractLimiter limiter) {\n    // This value should parallel (zipkin_storage.throttle.queue_size + zipkin_storage.throttle.concurrency)\n    // It is tracked to make sure it doesn't perpetually increase.  If it does then we're not resolving LimitListeners.\n    Gauge.builder(\"zipkin_storage.throttle.in_flight_requests\", limiter::getInflight)\n      .description(\"number of requests the limiter thinks are active\")\n      .register(registryInstance);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/throttle/ThrottledCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport com.linecorp.armeria.common.util.Exceptions;\nimport com.netflix.concurrency.limits.Limiter;\nimport com.netflix.concurrency.limits.Limiter.Listener;\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.function.Predicate;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nimport static com.linecorp.armeria.common.util.Exceptions.clearTrace;\n\n/**\n * {@link Call} implementation that is backed by an {@link ExecutorService}. The ExecutorService\n * serves two purposes:\n * <ol>\n * <li>Limits the number of requests that can run in parallel.</li>\n * <li>Depending on configuration, can queue up requests to make sure we don't aggressively drop\n * requests that would otherwise succeed if given a moment. Bounded queues are safest for this as\n * unbounded ones can lead to heap exhaustion and {@link OutOfMemoryError OOM errors}.</li>\n * </ol>\n *\n * @see ThrottledStorageComponent\n */\nfinal class ThrottledCall extends Call.Base<Void> {\n  /**\n   * <p>This reduces allocations when concurrency reached by always returning the same instance.\n   * This is only thrown in one location, and a stack trace starting from static initialization\n   * isn't useful. Hence, we {@link Exceptions#clearTrace clear the trace}.\n   */\n  static final RejectedExecutionException STORAGE_THROTTLE_MAX_CONCURRENCY =\n    clearTrace(new RejectedExecutionException(\"STORAGE_THROTTLE_MAX_CONCURRENCY reached\"));\n\n  static final Callback<Void> NOOP_CALLBACK = new Callback<Void>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n    }\n  };\n\n  final Call<Void> delegate;\n  final Executor executor;\n  final Limiter<Void> limiter;\n  final LimiterMetrics limiterMetrics;\n  final Predicate<Throwable> isOverCapacity;\n  final CountDownLatch latch = new CountDownLatch(1);\n  Throwable throwable; // thread visibility guaranteed by the countdown latch\n\n  ThrottledCall(Call<Void> delegate, Executor executor, Limiter<Void> limiter,\n    LimiterMetrics limiterMetrics, Predicate<Throwable> isOverCapacity) {\n    this.delegate = delegate;\n    this.executor = executor;\n    this.limiter = limiter;\n    this.limiterMetrics = limiterMetrics;\n    this.isOverCapacity = isOverCapacity;\n  }\n\n  /**\n   * To simplify code, this doesn't actually invoke the underlying {@link #execute()} method. This\n   * is ok because in almost all cases, doing so would imply invoking {@link #enqueue(Callback)}\n   * anyway.\n   */\n  @Override protected Void doExecute() throws IOException {\n    // Enqueue the call invocation on the executor and block until it completes.\n    doEnqueue(NOOP_CALLBACK);\n    if (!await(latch)) throw new InterruptedIOException();\n\n    // Check if the run resulted in an exception\n    Throwable t = this.throwable;\n    if (t == null) return null; // success\n\n    // Coerce the throwable to the signature of Call.execute()\n    if (t instanceof Error error) throw error;\n    if (t instanceof IOException exception) throw exception;\n    if (t instanceof RuntimeException exception) throw exception;\n    throw new RuntimeException(t);\n  }\n\n  // When handling enqueue, we don't block the calling thread. Any exception goes to the callback.\n  @Override protected void doEnqueue(Callback<Void> callback) {\n    Listener limiterListener =\n      limiter.acquire(null).orElseThrow(() -> STORAGE_THROTTLE_MAX_CONCURRENCY);\n\n    limiterMetrics.requests.increment();\n    EnqueueAndAwait enqueueAndAwait = new EnqueueAndAwait(callback, limiterListener);\n\n    try {\n      executor.execute(enqueueAndAwait);\n    } catch (RuntimeException | Error t) { // possibly rejected, but from the executor, not storage!\n      propagateIfFatal(t);\n      callback.onError(t);\n      // Ignoring in all cases here because storage itself isn't saying we need to throttle. Though\n      // we may still be write bound, but a drop in concurrency won't necessarily help.\n      limiterListener.onIgnore();\n      throw t; // allows blocking calls to see the exception\n    }\n  }\n\n  @Override public Call<Void> clone() {\n    return new ThrottledCall(delegate.clone(), executor, limiter, limiterMetrics, isOverCapacity);\n  }\n\n  @Override public String toString() {\n    return \"Throttled(\" + delegate + \")\";\n  }\n\n  /** When run, this enqueues a call with a given callback, and awaits its completion. */\n  final class EnqueueAndAwait implements Runnable, Callback<Void> {\n    final Callback<Void> callback;\n    final Listener limiterListener;\n\n    EnqueueAndAwait(Callback<Void> callback, Listener limiterListener) {\n      this.callback = callback;\n      this.limiterListener = limiterListener;\n    }\n\n    /**\n     * This waits until completion to ensure the number of executing calls doesn't surpass the\n     * concurrency limit of the executor.\n     *\n     * <h3>The {@link Listener} isn't affected during run</h3>\n     * There could be an error enqueuing the call or an interruption during shutdown of the\n     * executor. We do not affect the {@link Listener} here because it would be redundant to\n     * handling already done in callbacks. For example, if shutting down, the storage layer would\n     * also invoke {@link #onError(Throwable)}.\n     */\n    @Override public void run() {\n      if (delegate.isCanceled()) return;\n      try {\n        delegate.enqueue(this);\n\n        // Need to wait here since the callback call will run asynchronously also.\n        // This ensures we don't exceed our throttle/queue limits.\n        await(latch);\n      } catch (Throwable t) { // edge case: error during enqueue!\n        propagateIfFatal(t);\n        callback.onError(t);\n      }\n    }\n\n    @Override public void onSuccess(Void value) {\n      try {\n        // usually we don't add metrics like this,\n        // but for now it is helpful to sanity check acquired vs erred.\n        limiterMetrics.requestsSucceeded.increment();\n        limiterListener.onSuccess(); // NOTE: limiter could block and delay the caller's callback\n        callback.onSuccess(value);\n      } finally {\n        latch.countDown();\n      }\n    }\n\n    @Override public void onError(Throwable t) {\n      try {\n        throwable = t; // catch the throwable in case the invocation is blocking (Call.execute())\n        if (isOverCapacity.test(t)) {\n          limiterMetrics.requestsDropped.increment();\n          limiterListener.onDropped();\n        } else {\n          limiterMetrics.requestsIgnored.increment();\n          limiterListener.onIgnore();\n        }\n\n        // NOTE: the above limiter could block and delay the caller's callback\n        callback.onError(t);\n      } finally {\n        latch.countDown();\n      }\n    }\n\n    @Override public String toString() {\n      return \"EnqueueAndAwait{call=\" + delegate + \", callback=\" + callback + \"}\";\n    }\n  }\n\n  /** Returns true if uninterrupted waiting for the latch */\n  static boolean await(CountDownLatch latch) {\n    try {\n      latch.await();\n      return true;\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/throttle/ThrottledStorageComponent.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport brave.Tracer;\nimport brave.Tracing;\nimport brave.propagation.CurrentTraceContext;\nimport com.linecorp.armeria.common.brave.RequestContextCurrentTraceContext;\nimport com.netflix.concurrency.limits.Limit;\nimport com.netflix.concurrency.limits.Limiter;\nimport com.netflix.concurrency.limits.limit.Gradient2Limit;\nimport com.netflix.concurrency.limits.limiter.AbstractLimiter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.util.NamedThreadFactory;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.server.internal.brave.TracedCall;\nimport zipkin2.storage.ForwardingStorageComponent;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.linecorp.armeria.common.util.Exceptions.clearTrace;\n\n/**\n * Delegating implementation that limits requests to the {@link #spanConsumer()} of another {@link\n * StorageComponent}.  The theory here is that this class can be used to:\n * <ul>\n * <li>Prevent spamming the storage engine with excessive, spike requests when they come in; thus\n * preserving it's life.</li>\n * <li>Optionally act as a buffer so that a fixed number requests can be queued for execution when\n * the throttle allows for it.  This optional queue must be bounded in order to avoid running out of\n * memory from infinitely queueing.</li>\n * </ul>\n *\n * @see ThrottledSpanConsumer\n */\npublic final class ThrottledStorageComponent extends ForwardingStorageComponent {\n  /**\n   * See {@link ThrottledCall#STORAGE_THROTTLE_MAX_CONCURRENCY} if unfamiliar with clearing trace on\n   * exceptions only thrown from one spot.\n   */\n  static final RejectedExecutionException STORAGE_THROTTLE_MAX_QUEUE_SIZE =\n    clearTrace(new RejectedExecutionException(\"STORAGE_THROTTLE_MAX_QUEUE_SIZE reached\"));\n\n  final StorageComponent delegate;\n  final @Nullable Tracer tracer;\n  final @Nullable CurrentTraceContext currentTraceContext;\n  final AbstractLimiter<Void> limiter;\n  final ThreadPoolExecutor executor;\n  final LimiterMetrics limiterMetrics;\n\n  public ThrottledStorageComponent(StorageComponent delegate, MeterRegistry registry,\n    @Nullable Tracing tracing, int minConcurrency, int maxConcurrency, int maxQueueSize) {\n    this.delegate = Objects.requireNonNull(delegate);\n    this.tracer = tracing != null ? tracing.tracer() : null;\n    this.currentTraceContext = tracing != null ? tracing.currentTraceContext() : null;\n\n    Limit limit = Gradient2Limit.newBuilder()\n      .minLimit(minConcurrency)\n      // Limiter will trend towards min until otherwise necessary so may as well start there\n      .initialLimit(minConcurrency)\n      .maxConcurrency(maxConcurrency)\n      .queueSize(0)\n      .build();\n    this.limiter = new Builder().limit(limit).build();\n\n    // The size of the thread pool is managed by the limiter, so we initialize it with the lower\n    // bound (current limit), and later use change notification to resize it.\n    executor = new ThreadPoolExecutor(\n      limit.getLimit(),\n      limit.getLimit(),\n      0,\n      TimeUnit.DAYS,\n      createQueue(maxQueueSize),\n      new NamedThreadFactory(\"zipkin-throttle-pool\") {\n        @Override public Thread newThread(Runnable runnable) {\n          return super.newThread(new Runnable() {\n            @Override public void run() {\n              RequestContextCurrentTraceContext.setCurrentThreadNotRequestThread(true);\n              runnable.run();\n            }\n\n            @Override public String toString() {\n              return runnable.toString();\n            }\n          });\n        }\n      },\n      (r, e) -> {\n        throw STORAGE_THROTTLE_MAX_QUEUE_SIZE;\n      });\n    limit.notifyOnChange(new ThreadPoolExecutorResizer(executor));\n\n    MicrometerThrottleMetrics metrics = new MicrometerThrottleMetrics(registry);\n    metrics.bind(executor);\n    metrics.bind(limiter);\n\n    limiterMetrics = new LimiterMetrics(registry);\n  }\n\n  @Override protected StorageComponent delegate() {\n    return delegate;\n  }\n\n  @Override public SpanConsumer spanConsumer() {\n    return new ThrottledSpanConsumer(this);\n  }\n\n  @Override public void close() throws IOException {\n    executor.shutdownNow();\n    delegate.close();\n  }\n\n  @Override public String toString() {\n    return \"Throttled{\" + delegate.toString() + \"}\";\n  }\n\n  static final class ThrottledSpanConsumer implements SpanConsumer {\n    final SpanConsumer delegate;\n    final Executor executor;\n    final Limiter<Void> limiter;\n    final LimiterMetrics limiterMetrics;\n    final Predicate<Throwable> isOverCapacity;\n    @Nullable final Tracer tracer;\n\n    ThrottledSpanConsumer(ThrottledStorageComponent throttledStorage) {\n      this.delegate = throttledStorage.delegate.spanConsumer();\n      this.executor = throttledStorage.currentTraceContext != null\n        ? throttledStorage.currentTraceContext.executor(throttledStorage.executor)\n        : throttledStorage.executor;\n      this.limiter = throttledStorage.limiter;\n      this.limiterMetrics = throttledStorage.limiterMetrics;\n      this.isOverCapacity = throttledStorage::isOverCapacity;\n      this.tracer = throttledStorage.tracer;\n    }\n\n    @Override public Call<Void> accept(List<Span> spans) {\n      Call<Void> result = new ThrottledCall(\n        delegate.accept(spans), executor, limiter, limiterMetrics, isOverCapacity);\n\n      return tracer != null ? new TracedCall<>(tracer, result, \"throttled-accept-spans\") : result;\n    }\n\n    @Override public String toString() {\n      return \"Throttled(\" + delegate + \")\";\n    }\n  }\n\n  static BlockingQueue<Runnable> createQueue(int maxSize) {\n    if (maxSize < 0) throw new IllegalArgumentException(\"maxSize < 0\");\n\n    if (maxSize == 0) {\n      // 0 means we should be bounded but we can't create a queue with that size so use 1 instead.\n      maxSize = 1;\n    }\n\n    return new LinkedBlockingQueue<>(maxSize);\n  }\n\n  static final class ThreadPoolExecutorResizer implements Consumer<Integer> {\n    final ThreadPoolExecutor executor;\n\n    ThreadPoolExecutorResizer(ThreadPoolExecutor executor) {\n      this.executor = executor;\n    }\n\n    /**\n     * This is {@code synchronized} to ensure that we don't let the core/max pool sizes get out of\n     * sync; even for an instant.  The two need to be tightly coupled together to ensure that when\n     * our queue fills up we don't spin up extra Threads beyond our calculated limit.\n     *\n     * <p>There is also an unfortunate aspect where the {@code max} has to always be greater than\n     * {@code core} or an exception will be thrown.  So they have to be adjust appropriately\n     * relative to the direction the size is going.\n     */\n    @Override public synchronized void accept(Integer newValue) {\n      int previousValue = executor.getCorePoolSize();\n\n      int newValueInt = newValue;\n      if (previousValue < newValueInt) {\n        executor.setMaximumPoolSize(newValueInt);\n        executor.setCorePoolSize(newValueInt);\n      } else if (previousValue > newValueInt) {\n        executor.setCorePoolSize(newValueInt);\n        executor.setMaximumPoolSize(newValueInt);\n      }\n      // Note: no case for equals.  Why modify something that doesn't need modified?\n    }\n  }\n\n  static final class Builder extends AbstractLimiter.Builder<Builder> {\n    NonLimitingLimiter build() {\n      return new NonLimitingLimiter(this);\n    }\n\n    @Override protected Builder self() {\n      return this;\n    }\n  }\n\n  /**\n   * Unlike a normal Limiter, this will actually not prevent the creation of a {@link Listener} in\n   * {@link #acquire(java.lang.Void)}.  The point of this is to ensure that we can always derive an\n   * appropriate {@link Limit#getLimit() Limit} while the {@link #executor} handles actually\n   * limiting running requests.\n   */\n  static final class NonLimitingLimiter extends AbstractLimiter<Void> {\n    NonLimitingLimiter(AbstractLimiter.Builder<?> builder) {\n      super(builder);\n    }\n\n    @Override public Optional<Listener> acquire(Void context) {\n      return Optional.of(createListener());\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/throttle/ZipkinStorageThrottleProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n@ConfigurationProperties(\"zipkin.storage.throttle\")\npublic final class ZipkinStorageThrottleProperties {\n  /** Should we throttle at all? */\n  private boolean enabled;\n  /** Minimum number of storage requests to allow through at a given time. */\n  private int minConcurrency;\n  /**\n   * Maximum number of storage requests to allow through at a given time. Should be tuned to\n   * (bulk_index_pool_size / num_servers_in_cluster). e.g. 200 (default pool size in Elasticsearch)\n   * / 2 (number of load balanced zipkin-server instances) = 100.\n   */\n  private int maxConcurrency;\n  /**\n   * Maximum number of storage requests to buffer while waiting for open Thread. 0 = no buffering.\n   */\n  private int maxQueueSize;\n\n  public boolean isEnabled() {\n    return enabled;\n  }\n\n  public void setEnabled(boolean enabled) {\n    this.enabled = enabled;\n  }\n\n  public int getMinConcurrency() {\n    return minConcurrency;\n  }\n\n  public void setMinConcurrency(int minConcurrency) {\n    this.minConcurrency = minConcurrency;\n  }\n\n  public int getMaxConcurrency() {\n    return maxConcurrency;\n  }\n\n  public void setMaxConcurrency(int maxConcurrency) {\n    this.maxConcurrency = maxConcurrency;\n  }\n\n  public int getMaxQueueSize() {\n    return maxQueueSize;\n  }\n\n  public void setMaxQueueSize(int maxQueueSize) {\n    this.maxQueueSize = maxQueueSize;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ui/CompressionProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.ui;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.web.server.Compression;\n\n@ConfigurationProperties(\"server\")\nclass CompressionProperties {\n  public Compression getCompression() {\n    return compression;\n  }\n\n  public void setCompression(Compression compression) {\n    this.compression = compression;\n  }\n\n  private Compression compression;\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.ui;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ServerCacheControl;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.RedirectService;\nimport com.linecorp.armeria.server.file.FileService;\nimport com.linecorp.armeria.server.file.HttpFile;\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.config.MeterFilter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.StringWriter;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.io.Resource;\nimport org.springframework.util.StreamUtils;\nimport zipkin2.server.internal.JsonUtil;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.server.internal.ui.ZipkinUiProperties.DEFAULT_BASEPATH;\n\n/**\n * Zipkin-UI is a single-page application mounted at /zipkin. For simplicity, assume paths mentioned\n * below are relative to that. For example, the UI reads config.json, from the absolute path\n * /zipkin/config.json\n *\n * <p>When looking at a trace, the browser is sent to the path \"/traces/{id}\". For the single-page\n * app to serve that route, the server needs to forward the request to \"/index.html\". The same\n * forwarding applies to \"/dependencies\" and any other routes the UI controls.\n *\n * <p>Under the scenes the JavaScript code looks at {@code window.location} to figure out what the\n * UI should do. This is handled by a route api defined in the crossroads library.\n *\n * <h3>Caching</h3>\n * <p>This includes a hard-coded cache policy, consistent with zipkin-scala.\n * <ul>\n *   <li>1 minute for index.html</li>\n *   <li>10 minute for /config.json</li>\n *   <li>365 days for hashed resources (ex /app-e12b3bbb7e5a572f270d.min.js)</li>\n * </ul>\n * Since index.html links to hashed resource names, any change to it will orphan old resources.\n * That's why hashed resource age can be 365 days.\n */\n@EnableConfigurationProperties({ZipkinUiProperties.class, CompressionProperties.class})\n@ConditionalOnProperty(name = \"zipkin.ui.enabled\", matchIfMissing = true)\npublic class ZipkinUiConfiguration {\n  @Autowired ZipkinUiProperties ui;\n  @Value(\"classpath:zipkin-lens/index.html\") Resource lensIndexHtml;\n\n  @Bean HttpService indexService() throws Exception {\n    HttpService lensIndex = maybeIndexService(ui.getBasepath(), lensIndexHtml);\n    if (lensIndex != null) return lensIndex;\n    throw new BeanCreationException(\"Could not load Lens UI from \" + lensIndexHtml);\n  }\n\n  @Bean ArmeriaServerConfigurator uiServerConfigurator(\n    HttpService indexService,\n    Optional<MeterRegistry> meterRegistry\n  ) throws IOException {\n    ServerCacheControl maxAgeYear =\n      ServerCacheControl.builder().maxAgeSeconds(TimeUnit.DAYS.toSeconds(365)).build();\n\n    HttpService uiFileService = FileService.builder(getClass().getClassLoader(), \"zipkin-lens\")\n      .cacheControl(maxAgeYear)\n      .build();\n\n    String config = writeConfig(ui);\n    return sb -> {\n      sb.service(\"/zipkin/config.json\", HttpFile.builder(HttpData.ofUtf8(config))\n        .cacheControl(ServerCacheControl.builder().maxAgeSeconds(600).build())\n        .contentType(MediaType.JSON_UTF_8)\n        .build()\n        .asService());\n\n      sb.serviceUnder(\"/zipkin/\", uiFileService);\n\n      // TODO This approach requires maintenance when new UI routes are added. Change to the following:\n      // If the path is a a file w/an extension, treat normally.\n      // Otherwise instead of returning 404, forward to the index.\n      // See https://github.com/twitter/finatra/blob/458c6b639c3afb4e29873d123125eeeb2b02e2cd/http/src/main/scala/com/twitter/finatra/http/response/ResponseBuilder.scala#L321\n      sb.service(\"/zipkin/\", indexService)\n        .service(\"/zipkin/index.html\", indexService)\n        .service(\"/zipkin/traces/{id}\", indexService)\n        .service(\"/zipkin/dependency\", indexService)\n        .service(\"/zipkin/traceViewer\", indexService);\n\n      sb.service(\"/favicon.ico\", new RedirectService(HttpStatus.FOUND, \"/zipkin/favicon.ico\"))\n        .service(\"/\", new RedirectService(HttpStatus.FOUND, \"/zipkin/\"))\n        .service(\"/zipkin\", new RedirectService(HttpStatus.FOUND, \"/zipkin/\"));\n\n      // don't add metrics for favicon\n      meterRegistry.ifPresent(m -> m.config().meterFilter(MeterFilter.deny(id -> {\n        String uri = id.getTag(\"uri\");\n        return uri != null && uri.startsWith(\"/favicon.ico\");\n      })));\n    };\n  }\n\n  //\n  // environment: '',\n  // queryLimit: 10,\n  // defaultLookback: 15 * 60 * 1000, // 15 minutes\n  // searchEnabled: true,\n  // dependency: {\n  //   enabled: true,\n  //   lowErrorRate: 0.5, // 50% of calls in error turns line yellow\n  //   highErrorRate: 0.75 // 75% of calls in error turns line red\n  // }\n  static String writeConfig(ZipkinUiProperties ui) throws IOException {\n    StringWriter writer = new StringWriter();\n    try (JsonGenerator generator = JsonUtil.createGenerator(writer)) {\n      generator.useDefaultPrettyPrinter();\n      generator.writeStartObject();\n      generator.writeStringField(\"environment\", ui.getEnvironment());\n      generator.writeNumberField(\"queryLimit\", ui.getQueryLimit());\n      generator.writeNumberField(\"defaultLookback\", ui.getDefaultLookback());\n      generator.writeBooleanField(\"searchEnabled\", ui.isSearchEnabled());\n      generator.writeStringField(\"logsUrl\", ui.getLogsUrl());\n      generator.writeStringField(\"supportUrl\", ui.getSupportUrl());\n      generator.writeStringField(\"archivePostUrl\", ui.getArchivePostUrl());\n      generator.writeStringField(\"archiveUrl\", ui.getArchiveUrl());\n      generator.writeObjectFieldStart(\"dependency\");\n      generator.writeBooleanField(\"enabled\", ui.getDependency().isEnabled());\n      generator.writeNumberField(\"lowErrorRate\", ui.getDependency().getLowErrorRate());\n      generator.writeNumberField(\"highErrorRate\", ui.getDependency().getHighErrorRate());\n      generator.writeEndObject(); // .dependency\n      generator.writeEndObject(); // .\n    }\n    return writer.toString();\n  }\n\n  static HttpService maybeIndexService(String basePath, Resource resource) throws IOException {\n    String maybeContent = maybeResource(basePath, resource);\n    if (maybeContent == null) return null;\n\n    ServerCacheControl maxAgeMinute = ServerCacheControl.builder().maxAgeSeconds(60).build();\n\n    return HttpFile.builder(HttpData.ofUtf8(maybeContent))\n      .contentType(MediaType.HTML_UTF_8).cacheControl(maxAgeMinute)\n      .build().asService();\n  }\n\n  static String maybeResource(String basePath, Resource resource) throws IOException {\n    if (!resource.isReadable()) return null;\n\n    try (InputStream stream = resource.getInputStream()) {\n      String content = StreamUtils.copyToString(stream, UTF_8);\n      if (DEFAULT_BASEPATH.equals(basePath)) return content;\n\n      if (basePath.equals(\"/\")) basePath = \"\";\n\n      // relativize any href or src in index.html\n      // TODO: see if vite config can make these relative by default!\n      return content.replace(\"=\\\"\" + DEFAULT_BASEPATH, \"=\\\".\")\n        // set the base href, used in js, to absolute\n        .replace(\"<base href=\\\".\", \"<base href=\\\"\" + basePath);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiProperties.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.ui;\n\nimport java.util.concurrent.TimeUnit;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.util.StringUtils;\n\n@ConfigurationProperties(\"zipkin.ui\")\nclass ZipkinUiProperties {\n  // TODO: this isn't honored in lens https://github.com/openzipkin/zipkin/issues/2519\n  static final String DEFAULT_BASEPATH = \"/zipkin\";\n\n  private String environment = \"\";\n  private int queryLimit = 10;\n  private int defaultLookback = (int) TimeUnit.DAYS.toMillis(7);\n  private String instrumented = \".*\";\n  private String logsUrl = null;\n  private String supportUrl = null;\n  private String archivePostUrl = null;\n  private String archiveUrl = null;\n  private String basepath = DEFAULT_BASEPATH;\n  private boolean searchEnabled = true;\n  private Dependency dependency = new Dependency();\n\n  public int getDefaultLookback() {\n    return defaultLookback;\n  }\n\n  public void setDefaultLookback(int defaultLookback) {\n    this.defaultLookback = defaultLookback;\n  }\n\n  public String getEnvironment() {\n    return environment;\n  }\n\n  public void setEnvironment(String environment) {\n    this.environment = environment;\n  }\n\n  public int getQueryLimit() {\n    return queryLimit;\n  }\n\n  public void setQueryLimit(int queryLimit) {\n    this.queryLimit = queryLimit;\n  }\n\n  public String getInstrumented() {\n    return instrumented;\n  }\n\n  public void setInstrumented(String instrumented) {\n    this.instrumented = instrumented;\n  }\n\n  public String getLogsUrl() {\n    return logsUrl;\n  }\n\n  public String getArchivePostUrl() {\n    return archivePostUrl;\n  }\n\n\n  public String getArchiveUrl() {\n    return archiveUrl;\n  }\n\n  public void setLogsUrl(String logsUrl) {\n    if (!StringUtils.isEmpty(logsUrl)) {\n      this.logsUrl = logsUrl;\n    }\n  }\n\n  public String getSupportUrl() {\n    return supportUrl;\n  }\n\n  public void setSupportUrl(String supportUrl) {\n    if (!StringUtils.isEmpty(supportUrl)) {\n      this.supportUrl = supportUrl;\n    }\n\n  }\n\n  public void setArchivePostUrl(String archivePostUrl) {\n    if (!StringUtils.isEmpty(archivePostUrl)) {\n      this.archivePostUrl = archivePostUrl;\n    }\n  }\n\n  public void setArchiveUrl(String archiveUrl) {\n    if (!StringUtils.isEmpty(archiveUrl)) {\n      this.archiveUrl = archiveUrl;\n    }\n  }\n\n  public boolean isSearchEnabled() {\n    return searchEnabled;\n  }\n\n  public void setSearchEnabled(boolean searchEnabled) {\n    this.searchEnabled = searchEnabled;\n  }\n\n  public Dependency getDependency() {\n    return dependency;\n  }\n\n  public void setDependency(Dependency dependency) {\n    this.dependency = dependency;\n  }\n\n  public String getBasepath() {\n    return basepath;\n  }\n\n  public void setBasepath(String basepath) {\n    this.basepath = basepath;\n  }\n\n  public static class Dependency {\n    private boolean enabled = true;\n    private float lowErrorRate = 0.5f; // 50% of calls in error turns line yellow\n    private float highErrorRate = 0.75f; // 75% of calls in error turns line red\n\n    public boolean isEnabled() {\n      return enabled;\n    }\n\n    public void setEnabled(boolean enabled) {\n      this.enabled = enabled;\n    }\n\n    public float getLowErrorRate() {\n      return lowErrorRate;\n    }\n\n    public void setLowErrorRate(float lowErrorRate) {\n      this.lowErrorRate = lowErrorRate;\n    }\n\n    public float getHighErrorRate() {\n      return highErrorRate;\n    }\n\n    public void setHighErrorRate(float highErrorRate) {\n      this.highErrorRate = highErrorRate;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/main/resources/info.json",
    "content": "{\"zipkin\":{\"version\":\"@project.version@\",\"commit\":\"@git.commit.id.abbrev@\"}}\n"
  },
  {
    "path": "zipkin-server/src/main/resources/simplelogger.properties",
    "content": "# SLF4J's SimpleLogger configuration file\n# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.defaultLogLevel=info\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\norg.slf4j.simpleLogger.showShortLogName=true\n\n# this mirrors the logging configuration applied in zipkin-server-shared.yml , logging.level\n# This only includes Armeria as for example Kafka and Cassandra are not in the slim dist\n\n# Unless it's serious we don't want to know.\norg.slf4j.simpleLogger.log.com.linecorp.armeria=WARN\n# But allow to say it's ready to serve requests\norg.slf4j.simpleLogger.log.com.linecorp.armeria.server.Server=INFO\n# # and when registered in Eureka\norg.slf4j.simpleLogger.log.com.linecorp.armeria.server.eureka.EurekaUpdatingListener=INFO\n# # and when http-logging is enabled\norg.slf4j.simpleLogger.log.com.linecorp.armeria.client.logging.LoggingClient=INFO\n"
  },
  {
    "path": "zipkin-server/src/main/resources/zipkin-server-shared.yml",
    "content": "zipkin:\n  self-tracing:\n    # Set to true to enable self-tracing.\n    enabled: ${SELF_TRACING_ENABLED:false}\n    # percentage of self-traces to retain. If set to a value other than 1.0, traces-per-second will\n    # not be used.\n    sample-rate: ${SELF_TRACING_SAMPLE_RATE:1.0}\n    # Number of traces per second to retain. sample-rate must be set to 1.0 to use this value. If\n    # set to 0, an unlimited number of traces per second will be retained.\n    traces-per-second: ${SELF_TRACING_TRACES_PER_SECOND:1}\n    # Timeout in seconds to flush self-tracing data to storage.\n    message-timeout: ${SELF_TRACING_FLUSH_INTERVAL:1}\n  collector:\n    # percentage to traces to retain\n    sample-rate: ${COLLECTOR_SAMPLE_RATE:1.0}\n    activemq:\n      enabled: ${COLLECTOR_ACTIVEMQ_ENABLED:true}\n      # ActiveMQ broker url. Ex. tcp://localhost:61616 or failover:(tcp://localhost:61616,tcp://remotehost:61616)\n      url: ${ACTIVEMQ_URL:}\n      # Queue from which to collect span messages.\n      queue: ${ACTIVEMQ_QUEUE:zipkin}\n      # Number of concurrent span consumers.\n      concurrency: ${ACTIVEMQ_CONCURRENCY:1}\n      # Optional username to connect to the broker\n      username: ${ACTIVEMQ_USERNAME:}\n      # Optional password to connect to the broker\n      password: ${ACTIVEMQ_PASSWORD:}\n    http:\n      # Set false to disable creation of spans via HTTP collector API\n      enabled: ${COLLECTOR_HTTP_ENABLED:${HTTP_COLLECTOR_ENABLED:true}}\n    grpc:\n      # Set false to disable the GRPC collector\n      enabled: ${COLLECTOR_GRPC_ENABLED:true}\n    kafka:\n      enabled: ${COLLECTOR_KAFKA_ENABLED:true}\n      # Kafka bootstrap broker list, comma-separated host:port values. Setting this activates the\n      # Kafka 0.10+ collector.\n      bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:}\n      # Name of topic to poll for spans\n      topic: ${KAFKA_TOPIC:zipkin}\n      # Consumer group this process is consuming on behalf of.\n      group-id: ${KAFKA_GROUP_ID:zipkin}\n      # Count of consumer threads consuming the topic\n      streams: ${KAFKA_STREAMS:1}\n    rabbitmq:\n      enabled: ${COLLECTOR_RABBITMQ_ENABLED:true}\n      # RabbitMQ server address list (comma-separated list of host:port)\n      addresses: ${RABBIT_ADDRESSES:}\n      concurrency: ${RABBIT_CONCURRENCY:1}\n      # TCP connection timeout in milliseconds\n      connection-timeout: ${RABBIT_CONNECTION_TIMEOUT:60000}\n      password: ${RABBIT_PASSWORD:guest}\n      queue: ${RABBIT_QUEUE:zipkin}\n      username: ${RABBIT_USER:guest}\n      virtual-host: ${RABBIT_VIRTUAL_HOST:/}\n      useSsl: ${RABBIT_USE_SSL:false}\n      uri: ${RABBIT_URI:}\n    scribe:\n      enabled: ${COLLECTOR_SCRIBE_ENABLED:${SCRIBE_ENABLED:false}}\n      category: ${SCRIBE_CATEGORY:zipkin}\n      port: ${COLLECTOR_PORT:9410}\n    pulsar:\n      enabled: ${COLLECTOR_PULSAR_ENABLED:true}\n      # The service URL for the Pulsar client ex. pulsar://my-broker:6650.\n      service-url: ${PULSAR_SERVICE_URL:}\n      # Queue zipkin spans will be consumed from.\n      topic: ${PULSAR_TOPIC:zipkin}\n      # Specify the subscription name for this consumer.\n      subscription-name: ${PULSAR_SUBSCRIPTION_NAME:zipkin}\n      # Number of concurrent span consumers.\n      concurrency: ${PULSAR_CONCURRENCY:1}\n\n  discovery:\n    eureka:\n      enabled: ${DISCOVERY_EUREKA_ENABLED:true}\n      # URL of the Eureka v2 endpoint. e.g. http://eureka:8761/eureka/v2\n      service-url: ${EUREKA_SERVICE_URL:}\n      # The appName property of the instance\n      app-name: ${EUREKA_APP_NAME:zipkin}\n      # The instanceId property of the instance\n      instance-id: ${EUREKA_INSTANCE_ID:}\n      # The hostName property of the instance\n      hostname: ${EUREKA_HOSTNAME:}\n\n  query:\n    enabled: ${QUERY_ENABLED:true}\n    # Timeout for requests to the query API\n    timeout: ${QUERY_TIMEOUT:11s}\n    # 1 day in millis\n    lookback: ${QUERY_LOOKBACK:86400000}\n    # The Cache-Control max-age (seconds) for /api/v2/services, /api/v2/remoteServices and /api/v2/spans\n    names-max-age: 300\n    # CORS allowed-origins.\n    allowed-origins: \"*\"\n\n  # Internal properties that end users should never try to use\n  internal:\n    actuator:\n      enabled: true\n      # auto-configuration to include when ArmeriaSpringActuatorAutoConfiguration is present.\n      # Note: These are still subject to endpoint conditions. The list must be checked for drift\n      #       upgrading Spring Boot.\n      include:\n        - org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration\n        - org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration\n\n  storage:\n    strict-trace-id: ${STRICT_TRACE_ID:true}\n    search-enabled: ${SEARCH_ENABLED:true}\n    autocomplete-keys: ${AUTOCOMPLETE_KEYS:}\n    autocomplete-ttl: ${AUTOCOMPLETE_TTL:3600000}\n    autocomplete-cardinality: 20000\n    type: ${STORAGE_TYPE:mem}\n    throttle:\n      enabled: ${STORAGE_THROTTLE_ENABLED:false}\n      min-concurrency: ${STORAGE_THROTTLE_MIN_CONCURRENCY:10}\n      max-concurrency: ${STORAGE_THROTTLE_MAX_CONCURRENCY:200}\n      max-queue-size: ${STORAGE_THROTTLE_MAX_QUEUE_SIZE:1000}\n    mem:\n      # Maximum number of spans to keep in memory.  When exceeded, oldest traces (and their spans) will be purged.\n      max-spans: ${MEM_MAX_SPANS:500000}\n    cassandra3:\n      # Comma separated list of host addresses part of Cassandra cluster. Ports default to 9042 but you can also specify a custom port with 'host:port'.\n      contact-points: ${CASSANDRA_CONTACT_POINTS:localhost}\n      # Name of the datacenter that will be considered \"local\" for load balancing.\n      local-dc: ${CASSANDRA_LOCAL_DC:datacenter1}\n      # Will throw an exception on startup if authentication fails.\n      username: ${CASSANDRA_USERNAME:}\n      password: ${CASSANDRA_PASSWORD:}\n      keyspace: ${CASSANDRA_KEYSPACE:zipkin2}\n      # Max pooled connections per datacenter-local host.\n      max-connections: ${CASSANDRA_MAX_CONNECTIONS:8}\n      # Ensuring that schema exists, if enabled tries to execute script /zipkin2-schema.cql\n      ensure-schema: ${CASSANDRA_ENSURE_SCHEMA:true}\n      # how many more index rows to fetch than the user-supplied query limit\n      index-fetch-multiplier: ${CASSANDRA_INDEX_FETCH_MULTIPLIER:3}\n      # Using ssl for connection, rely on Keystore\n      use-ssl: ${CASSANDRA_USE_SSL:false}\n      # Controls validation of Cassandra server hostname\n      ssl-hostname-validation: ${CASSANDRA_SSL_HOSTNAME_VALIDATION:true}\n    elasticsearch:\n      # host is left unset intentionally, to defer the decision\n      hosts: ${ES_HOSTS:}\n      pipeline: ${ES_PIPELINE:}\n      timeout: ${ES_TIMEOUT:10000}\n      index: ${ES_INDEX:zipkin}\n      ensure-templates: ${ES_ENSURE_TEMPLATES:true}\n      date-separator: ${ES_DATE_SEPARATOR:-}\n      index-shards: ${ES_INDEX_SHARDS:5}\n      index-replicas: ${ES_INDEX_REPLICAS:1}\n      username: ${ES_USERNAME:}\n      password: ${ES_PASSWORD:}\n      credentials-file: ${ES_CREDENTIALS_FILE:}\n      credentials-refresh-interval: ${ES_CREDENTIALS_REFRESH_INTERVAL:5}\n      http-logging: ${ES_HTTP_LOGGING:}\n      ssl:\n        no-verify: ${ES_SSL_NO_VERIFY:false}\n      health-check:\n        enabled: ${ES_HEALTH_CHECK_ENABLED:true}\n        interval: ${ES_HEALTH_CHECK_INTERVAL:3s}\n      template-priority: ${ES_TEMPLATE_PRIORITY:}\n    mysql:\n      jdbc-url: ${MYSQL_JDBC_URL:}\n      host: ${MYSQL_HOST:localhost}\n      port: ${MYSQL_TCP_PORT:3306}\n      username: ${MYSQL_USER:}\n      password: ${MYSQL_PASS:}\n      db: ${MYSQL_DB:zipkin}\n      max-active: ${MYSQL_MAX_CONNECTIONS:10}\n      use-ssl: ${MYSQL_USE_SSL:false}\n  ui:\n    enabled: ${UI_ENABLED:${QUERY_ENABLED:true}}\n    ## Values below here are mapped to ZipkinUiProperties, served as /config.json\n    # Default limit for Find Traces\n    query-limit: 10\n    # The value here becomes a label in the top-right corner\n    environment:\n    # Default duration to look back when finding traces.\n    # Affects the \"Start time\" element in the UI. 15 minutes in millis\n    default-lookback: 900000\n    # When false, disables the \"Discover\" screen\n    search-enabled: ${SEARCH_ENABLED:true}\n    # Which sites this Zipkin UI covers. Regex syntax. (e.g. http:\\/\\/example.com\\/.*)\n    # Multiple sites can be specified, e.g.\n    # - .*example1.com\n    # - .*example2.com\n    # Default is \"match all websites\"\n    instrumented: .*\n    # URL placed into the <base> tag in the HTML\n    base-path: /zipkin\n\n# We are using Armeria instead of Tomcat. Have it inherit the default configuration from Spring\nspring.main.web-application-type: none\n# These defaults are not used directly. They are used via armeria namespacing\nserver:\n  port: ${QUERY_PORT:9411}\n  use-forward-headers: true\n  compression:\n    enabled: true\n    # compresses any response over min-response-size (default is 2KiB)\n    # Includes dynamic json content and large static assets from zipkin-ui\n    mime-types: application/json,application/javascript,text/css,image/svg\n    min-response-size: 2048\n\narmeria:\n  ports:\n    - port: ${server.port}\n      protocols:\n        - http\n  compression:\n    enabled: ${server.compression.enabled}\n    mime-types: ${server.compression.mime-types}\n    min-response-size: ${server.compression.min-response-size}\n  gracefulShutdownQuietPeriodMillis: -1\n  gracefulShutdownTimeoutMillis: -1\n\nspring:\n  jmx:\n     # reduce startup time by excluding unexposed JMX service\n     enabled: false\n  mvc:\n    favicon:\n      # zipkin has its own favicon\n      enabled: false\n  autoconfigure:\n    # NOTE: These exclusions can drift between Spring Boot minor versions. Audit accordingly.\n    # Ex. curl -s localhost:9411/actuator/beans|jq '.contexts.application.beans|keys_unsorted[]'|sort\n    exclude:\n      - org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration\n      # JMX is disabled\n      - org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration\n      # /health and /actuator/health served directly by Armeria\n      - org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration\n      - org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration\n      # /info and /actuator/info served directly by Armeria (content is /info.json)\n      - org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration\n      - org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration\n      # /prometheus and /actuator/prometheus are served directly by Armeria\n      - org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration\n      # Remove unused auto-configuration\n      - org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration\n      - org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration\n      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration\n      - org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration\n      - org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration\n      - org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration\n      - org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration\nlogging:\n  pattern:\n    level: \"%clr{%5p} %clr{[%X{traceId}/%X{spanId}]}{yellow}\"\n  level:\n    # Hush MySQL related logs\n    org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor: 'WARN'\n    com.zaxxer.hikari.HikariDataSource: 'WARN'\n    # Don't print driver version in console output\n    com.datastax.oss.driver.internal.core.DefaultMavenCoordinates: 'WARN'\n    # We exclude Geo codec and Graph extensions to keep size down\n    com.datastax.oss.driver.internal.core.context.InternalDriverContext: 'WARN'\n    # Avoid logs about adding handlers\n    com.datastax.oss.driver.internal.core.cql.CqlPrepareAsyncProcessor: 'WARN'\n    # Use of native clocks in Cassandra is not insightful\n    com.datastax.oss.driver.internal.core.time.Clock: 'WARN'\n    # Unless it's serious we don't want to know\n    com.linecorp.armeria: 'WARN'\n    # # But log when ready to serve requests\n    com.linecorp.armeria.server.Server: 'INFO'\n    # # and when registered in Eureka\n    com.linecorp.armeria.server.eureka.EurekaUpdatingListener: 'INFO'\n    # # and when http-logging is enabled\n    com.linecorp.armeria.client.logging.LoggingClient: 'INFO'\n    # kafka is quite chatty, so we switch everything off by default\n    org.apache.kafka: 'OFF'\n#     # investigate /api/v2/dependencies\n#     zipkin2.internal.DependencyLinker: 'DEBUG'\n#     # log reason behind http collector dropped messages\n#     zipkin2.server.ZipkinHttpCollector: 'DEBUG'\n#     zipkin2.collector.kafka.KafkaCollector: 'DEBUG'\n#     zipkin2.collector.rabbitmq.RabbitMQCollector: 'DEBUG'\n#     zipkin2.collector.scribe.ScribeCollector: 'DEBUG'\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: '*'\n  # Below are served directly without actuator.\n  endpoint:\n    health:\n      enabled: false\n    prometheus:\n      enabled: false\n    info:\n      enabled: false\n# Disabling auto time http requests since it is added in ZipkinPrometheusMetricsConfiguration\n# In Zipkin we use different naming for the http requests duration\n  metrics:\n    web:\n      server:\n        auto-time-requests: false\n"
  },
  {
    "path": "zipkin-server/src/main/resources/zipkin-server.yml",
    "content": "spring.profiles.include: shared\narmeria.enableMetrics: false\n"
  },
  {
    "path": "zipkin-server/src/main/resources/zipkin.txt",
    "content": "${AnsiOrange}\n                  oo\n                 oooo\n                oooooo\n               oooooooo\n              oooooooooo\n             oooooooooooo\n           ooooooo  ooooooo\n          oooooo     ooooooo\n         oooooo       ooooooo\n        oooooo   o  o   oooooo\n       oooooo   oo  oo   oooooo\n     ooooooo  oooo  oooo  ooooooo\n    oooooo   ooooo  ooooo  ooooooo\n   oooooo   oooooo  oooooo  ooooooo\n  oooooooo      oo  oo      oooooooo\n  ooooooooooooo oo  oo ooooooooooooo\n      oooooooooooo  oooooooooooo\n          oooooooo  oooooooo\n              oooo  oooo\n${AnsiNormal}\n     ________ ____  _  _____ _   _\n    |__  /_ _|  _ \\| |/ /_ _| \\ | |\n      / / | || |_) | ' / | ||  \\| |\n     / /_ | ||  __/| . \\ | || |\\  |\n    |____|___|_|   |_|\\_\\___|_| \\_|\n\n:: version @project.version@ :: commit @git.commit.id.abbrev@ ::\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/collector/activemq/ZipkinActiveMQCollectorPropertiesOverrideTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.activemq;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.activemq.Access;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class ZipkinActiveMQCollectorPropertiesOverrideTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  public String property;\n  public Object value;\n  public Function<ActiveMQCollector.Builder, Object> builderExtractor;\n\n  public static Iterable<Object[]> data() {\n    return List.of(\n      parameters(\"url\", \"failover:(tcp://localhost:61616,tcp://remotehost:61616)\",\n        b -> b.connectionFactory.getBrokerURL()),\n      parameters(\"client-id-prefix\", \"zipkin-prod\", b -> b.connectionFactory.getClientIDPrefix()),\n      parameters(\"queue\", \"zapkin\", b -> b.queue),\n      parameters(\"concurrency\", 2, b -> b.concurrency),\n      parameters(\"username\", \"u\", b -> b.connectionFactory.getUserName()),\n      parameters(\"password\", \"p\", b -> b.connectionFactory.getPassword())\n    );\n  }\n\n  /** to allow us to define with a lambda */\n  static <T> Object[] parameters(\n    String propertySuffix, T value, Function<ActiveMQCollector.Builder, T> builderExtractor) {\n    return new Object[] {\"zipkin.collector.activemq.\" + propertySuffix, value, builderExtractor};\n  }\n\n  @MethodSource(\"data\")\n  @ParameterizedTest(name = \"{0}\")\n  void propertyTransferredToCollectorBuilder(String property, Object value,\n    Function<ActiveMQCollector.Builder, Object> builderExtractor) {\n    initZipkinActiveMQCollectorPropertiesOverrideTest(property, value, builderExtractor);\n    if (!property.endsWith(\"url\")) {\n      TestPropertyValues.of(\"zipkin.collector.activemq.url:tcp://localhost:61616\").applyTo(context);\n    }\n\n    TestPropertyValues.of(\"zipkin.collector.activemq.$property:$value\").applyTo(context);\n\n    if (property.endsWith(\"username\")) {\n      TestPropertyValues.of(\"zipkin.collector.activemq.password:p\").applyTo(context);\n    }\n\n    if (property.endsWith(\"password\")) {\n      TestPropertyValues.of(\"zipkin.collector.activemq.username:u\").applyTo(context);\n    }\n\n    TestPropertyValues.of(property + \":\" + value).applyTo(context);\n    Access.registerActiveMQProperties(context);\n    context.refresh();\n\n    assertThat(Access.collectorBuilder(context))\n      .extracting(builderExtractor)\n      .isEqualTo(value);\n  }\n\n  void initZipkinActiveMQCollectorPropertiesOverrideTest(String property, Object value,\n    Function<ActiveMQCollector.Builder, Object> builderExtractor) {\n    this.property = property;\n    this.value = value;\n    this.builderExtractor = builderExtractor;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.kafka;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.kafka.Access;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class ZipkinKafkaCollectorPropertiesOverrideTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    if (context != null) context.close();\n  }\n\n  public String property;\n  public Object value;\n  public Function<KafkaCollector.Builder, Object> builderExtractor;\n\n  public static Iterable<Object[]> data() {\n    return List.of(\n      parameters(\n        \"bootstrap-servers\",\n        \"127.0.0.1:9092\",\n        b -> b.properties.getProperty(\"bootstrap.servers\")),\n      parameters(\"group-id\", \"zapkin\", b -> b.properties.getProperty(\"group.id\")),\n      parameters(\"topic\", \"zapkin\", b -> b.topic),\n      parameters(\"streams\", 2, b -> b.streams),\n      parameters(\n        \"overrides.auto.offset.reset\",\n        \"latest\",\n        b -> b.properties.getProperty(\"auto.offset.reset\")));\n  }\n\n  /** to allow us to define with a lambda */\n  static <T> Object[] parameters(\n    String propertySuffix, T value, Function<KafkaCollector.Builder, T> builderExtractor) {\n    return new Object[] {\"zipkin.collector.kafka.\" + propertySuffix, value, builderExtractor};\n  }\n\n  @MethodSource(\"data\")\n  @ParameterizedTest(name = \"{0}\")\n  void propertyTransferredToCollectorBuilder(String property, Object value,\n    Function<KafkaCollector.Builder, Object> builderExtractor) {\n    initZipkinKafkaCollectorPropertiesOverrideTest(property, value, builderExtractor);\n    TestPropertyValues.of(property + \":\" + value).applyTo(context);\n    Access.registerKafkaProperties(context);\n    context.refresh();\n\n    assertThat(Access.collectorBuilder(context))\n      .extracting(builderExtractor)\n      .isEqualTo(value);\n  }\n\n  void initZipkinKafkaCollectorPropertiesOverrideTest(String property, Object value,\n    Function<KafkaCollector.Builder, Object> builderExtractor) {\n    this.property = property;\n    this.value = value;\n    this.builderExtractor = builderExtractor;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/collector/pulsar/ZipkinPulsarCollectorPropertiesOverrideTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.pulsar;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.pulsar.Access;\n\nimport java.util.List;\nimport java.util.function.Function;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class ZipkinPulsarCollectorPropertiesOverrideTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    if (context != null) context.close();\n  }\n\n  public String property;\n  public Object value;\n  public Function<PulsarCollector.Builder, Object> builderExtractor;\n\n  public static Iterable<Object[]> data() {\n    return List.of(\n        // intentionally punting on comma-separated form of a list of addresses as it doesn't fit\n        // this unit test. Better to make a separate one than force-fit!\n        parameters(\"service-url\", \"pulsar://127.0.0.1:6650\", b -> b.clientProps.get(\"serviceUrl\")),\n        parameters(\"topic\", \"zipkin\", b -> b.topic),\n        parameters(\"concurrency\", 2, b -> b.concurrency),\n        parameters(\"clientProps.serviceUrl\", \"pulsar://127.0.0.1:6650\", b -> b.clientProps.get(\"serviceUrl\")),\n        parameters(\"consumerProps.subscriptionName\", \"zipkin-subscription\", b -> b.consumerProps.get(\"subscriptionName\"))\n    );\n  }\n\n  /** to allow us to define with a lambda */\n  static <T> Object[] parameters(\n      String propertySuffix, T value, Function<PulsarCollector.Builder, T> builderExtractor) {\n    return new Object[]{\"zipkin.collector.pulsar.\" + propertySuffix, value, builderExtractor};\n  }\n\n  @MethodSource(\"data\")\n  @ParameterizedTest(name = \"{0}\")\n  void propertyTransferredToCollectorBuilder(String property, Object value,\n                                             Function<PulsarCollector.Builder, Object> builderExtractor) throws Exception {\n    initZipkinPulsarCollectorPropertiesOverrideTest(property, value, builderExtractor);\n    TestPropertyValues.of(property + \":\" + value).applyTo(context);\n    Access.registerPulsarProperties(context);\n    context.refresh();\n\n    assertThat(Access.collectorBuilder(context))\n        .extracting(builderExtractor)\n        .isEqualTo(value);\n  }\n\n  void initZipkinPulsarCollectorPropertiesOverrideTest(String property, Object value,\n                                                       Function<PulsarCollector.Builder, Object> builderExtractor) {\n    this.property = property;\n    this.value = value;\n    this.builderExtractor = builderExtractor;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.rabbitmq;\n\nimport java.net.URI;\nimport java.util.List;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.rabbitmq.Access;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class ZipkinRabbitMQCollectorPropertiesOverrideTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    if (context != null) context.close();\n  }\n\n  public String property;\n  public Object value;\n  public Function<RabbitMQCollector.Builder, Object> builderExtractor;\n\n  public static Iterable<Object[]> data() {\n    return List.of(\n        // intentionally punting on comma-separated form of a list of addresses as it doesn't fit\n        // this unit test. Better to make a separate one than force-fit!\n        parameters(\"addresses\", \"localhost:5671\", builder -> builder.addresses[0].toString()),\n        parameters(\"concurrency\", 2, builder -> builder.concurrency),\n        parameters(\n            \"connectionTimeout\",\n            30_000,\n            builder -> builder.connectionFactory.getConnectionTimeout()),\n        parameters(\"password\", \"admin\", builder -> builder.connectionFactory.getPassword()),\n        parameters(\"queue\", \"zapkin\", builder -> builder.queue),\n        parameters(\"username\", \"admin\", builder -> builder.connectionFactory.getUsername()),\n        parameters(\"virtualHost\", \"/hello\", builder -> builder.connectionFactory.getVirtualHost()),\n        parameters(\"useSsl\", true, builder -> builder.connectionFactory.isSSL()),\n        parameters(\n            \"uri\",\n            URI.create(\"amqp://localhost\"),\n            builder -> URI.create(\"amqp://\" + builder.connectionFactory.getHost())));\n  }\n\n  /** to allow us to define with a lambda */\n  static <T> Object[] parameters(\n      String propertySuffix, T value, Function<RabbitMQCollector.Builder, T> builderExtractor) {\n    return new Object[] {\"zipkin.collector.rabbitmq.\" + propertySuffix, value, builderExtractor};\n  }\n\n  @MethodSource(\"data\")\n  @ParameterizedTest(name = \"{0}\")\n  void propertyTransferredToCollectorBuilder(String property, Object value,\n    Function<RabbitMQCollector.Builder, Object> builderExtractor) throws Exception {\n    initZipkinRabbitMQCollectorPropertiesOverrideTest(property, value, builderExtractor);\n    TestPropertyValues.of(property + \":\" + value).applyTo(context);\n    Access.registerRabbitMQProperties(context);\n    context.refresh();\n\n    assertThat(Access.collectorBuilder(context))\n        .extracting(builderExtractor)\n        .isEqualTo(value);\n  }\n\n  void initZipkinRabbitMQCollectorPropertiesOverrideTest(String property, Object value,\n    Function<RabbitMQCollector.Builder, Object> builderExtractor) {\n    this.property = property;\n    this.value = value;\n    this.builderExtractor = builderExtractor;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/collector/scribe/ZipkinScribeCollectorConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.collector.scribe;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.InMemoryConfiguration;\nimport zipkin2.server.internal.scribe.ZipkinScribeCollectorConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\npublic class ZipkinScribeCollectorConfigurationTest {\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesntProvidesCollectorComponent_byDefault() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      refreshContext();\n\n      context.getBean(ScribeCollector.class);\n    });\n  }\n\n  /**\n   * Note: this will flake if you happen to be running a server on port 9410!\n   */\n  @Test void providesCollectorComponent_whenEnabled() {\n    TestPropertyValues.of(\"zipkin.collector.scribe.enabled:true\").applyTo(context);\n    refreshContext();\n\n    assertThat(context.getBean(ScribeCollector.class)).isNotNull();\n  }\n\n  @Test void canOverrideProperty_port() {\n    TestPropertyValues.of(\n      \"zipkin.collector.scribe.enabled:true\",\n      \"zipkin.collector.scribe.port:9999\")\n      .applyTo(context);\n    refreshContext();\n\n    assertThat(context.getBean(ScribeCollector.class).server.port)\n      .isEqualTo(9999);\n  }\n\n  public void refreshContext() {\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinScribeCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n    context.refresh();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITActuatorMappings.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assumptions.assumeThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\nclass ITActuatorMappings {\n  @Autowired PrometheusMeterRegistry registry;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @Test void actuatorIsOK() throws Exception {\n    assumeThat(get(\"/actuator\").isSuccessful()) // actuator is optional\n      .isTrue();\n\n    // ensure we don't track actuator in prometheus\n    assertThat(scrape())\n      .doesNotContain(\"actuator\");\n  }\n\n  @Test void actuatorInfoEndpointHasDifferentContentType() throws IOException {\n    Response info = get(\"/info\");\n    Response actuatorInfo = get(\"/actuator/info\");\n\n    // Different content type\n    assertThat(actuatorInfo.isSuccessful()).isTrue();\n    assertThat(actuatorInfo.body().contentType())\n      .isNotEqualTo(info.body().contentType())\n      .hasToString(\"application/vnd.spring-boot.actuator.v2+json; charset=utf-8\");\n\n    // Same content\n    assertThat(actuatorInfo.body().string())\n      .isEqualTo(info.body().string());\n\n    // ensure we don't track info in prometheus\n    assertThat(scrape())\n      .doesNotContain(\"/info\");\n  }\n\n  @Test void actuatorHealthEndpointHasDifferentContentType() throws IOException {\n    Response health = get(\"/health\");\n    Response actuatorHealth = get(\"/actuator/health\");\n\n    // Different content type\n    assertThat(actuatorHealth.isSuccessful()).isTrue();\n    assertThat(actuatorHealth.body().contentType())\n      .isNotEqualTo(health.body().contentType())\n      .hasToString(\"application/vnd.spring-boot.actuator.v2+json; charset=utf-8\");\n\n    // Same content\n    assertThat(actuatorHealth.body().string())\n      .isEqualTo(health.body().string());\n\n    // ensure we don't track health in prometheus\n    assertThat(scrape())\n      .doesNotContain(\"/health\");\n  }\n\n  Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .build()).execute();\n  }\n\n  String scrape() throws IOException {\n    try {\n      Thread.sleep(100);\n    } catch (InterruptedException e) {\n      throw new InterruptedIOException(e.getMessage());\n    }\n    return registry.scrape();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinGrpcCollector.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.util.List;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport okio.Buffer;\nimport okio.BufferedSource;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.TestObjects;\nimport zipkin2.codec.SpanBytesDecoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.proto3.ListOfSpans;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static okhttp3.Protocol.H2_PRIOR_KNOWLEDGE;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/** This tests that we accept messages constructed by other clients. */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\nclass ITZipkinGrpcCollector {\n  @Autowired InMemoryStorage storage;\n  @Autowired Server server;\n\n  @BeforeEach void init() {\n    storage.clear();\n  }\n\n  OkHttpClient client = new OkHttpClient.Builder().protocols(List.of(H2_PRIOR_KNOWLEDGE)).build();\n\n  ListOfSpans grpcRequest;\n\n  @BeforeEach void sanityCheckCodecCompatible() throws IOException {\n    grpcRequest = ListOfSpans.ADAPTER.decode(SpanBytesEncoder.PROTO3.encodeList(TestObjects.TRACE));\n\n    assertThat(SpanBytesDecoder.PROTO3.decodeList(grpcRequest.encode()))\n      .containsExactlyElementsOf(TestObjects.TRACE); // sanity check codec compatible\n  }\n\n  @Test void report_trace() throws IOException {\n    callReport(grpcRequest); // Result is effectively void\n\n    awaitSpans();\n\n    assertThat(storage.getTraces())\n      .containsExactly(TestObjects.TRACE);\n  }\n\n  @Test void report_emptyIsOk() throws IOException {\n    callReport(new ListOfSpans.Builder().build());\n  }\n\n  void callReport(ListOfSpans spans) throws IOException {\n    try (Buffer requestBody = new Buffer(); Buffer encodedMessage = new Buffer()) {\n      requestBody.writeByte(0 /* compressedFlag */);\n\n      ListOfSpans.ADAPTER.encode(encodedMessage, spans);\n      requestBody.writeInt((int) encodedMessage.size());\n      requestBody.writeAll(encodedMessage);\n\n      Request request = new Request.Builder()\n        .url(url(server, \"/zipkin.proto3.SpanService/Report\"))\n        .addHeader(\"te\", \"trailers\")\n        .post(RequestBody.create(requestBody.snapshot(), MediaType.get(\"application/grpc\")))\n        .build();\n      try (Response response = client.newCall(request).execute();\n           BufferedSource responseBody = response.body().source()) {\n\n        // We expect this is a valid gRPC over HTTP2 response (Length-Prefixed-Message).\n        // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses\n        byte compressedFlag = responseBody.readByte();\n        long messageLength = responseBody.readInt() & 0xffffffffL;\n        assertThat(responseBody.exhausted()).isTrue(); // We expect a single response\n\n        // Now, verify the Length-Prefixed-Message\n        assertThat(compressedFlag).isZero(); // server didn't compress\n        assertThat(messageLength).isZero(); // there are no fields in ReportResponse\n      }\n    }\n  }\n\n  void awaitSpans() {\n    await().untilAsserted(// wait for spans\n      () -> assertThat(storage.acceptedSpanCount()).isGreaterThanOrEqualTo(1));\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.util.Arrays;\nimport java.util.List;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okio.Okio;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.UTF_8;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\npublic class ITZipkinServer {\n  static final List<Span> TRACE = List.of(TestObjects.CLIENT_SPAN);\n\n  @Autowired InMemoryStorage storage;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @BeforeEach void init() {\n    storage.clear();\n  }\n\n  @Test void getTrace() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/trace/\" + TRACE.get(0).traceId());\n    assertThat(response.isSuccessful()).isTrue();\n\n    assertThat(response.body().bytes())\n      .containsExactly(SpanBytesEncoder.JSON_V2.encodeList(TRACE));\n  }\n\n  @Test void getTrace_notFound() throws Exception {\n    Response response = get(\"/api/v2/trace/\" + TRACE.get(0).traceId());\n    assertThat(response.code()).isEqualTo(404);\n\n    assertThat(response.body().string())\n      .isEqualTo(TRACE.get(0).traceId() + \" not found\");\n  }\n\n  @Test void getTrace_malformed() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/trace/0e8b46e1-81b\");\n    assertThat(response.code()).isEqualTo(400);\n\n    assertThat(response.body().string())\n      .isEqualTo(\"0e8b46e1-81b should be lower-hex encoded with no prefix\");\n  }\n\n  @Test void getTraces() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/traceMany?traceIds=abcd,\" + TRACE.get(0).traceId());\n    assertThat(response.isSuccessful()).isTrue();\n\n    assertThat(response.body().string())\n      .isEqualTo(\"[\" + new String(SpanBytesEncoder.JSON_V2.encodeList(TRACE), UTF_8) + \"]\");\n  }\n\n  @Test void getTraces_emptyNotOk() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/traceMany?traceIds=\");\n    assertThat(response.code()).isEqualTo(400);\n\n    assertThat(response.body().string())\n      .isEqualTo(\"traceIds parameter is empty\");\n  }\n\n  @Test void getTraces_singleNotOk() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/traceMany?traceIds=\" + TRACE.get(0).traceId());\n    assertThat(response.code()).isEqualTo(400);\n\n    assertThat(response.body().string())\n      .isEqualTo(\"Use /api/v2/trace/{traceId} endpoint to retrieve a single trace\");\n  }\n\n  @Test void getTraces_malformed() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/traceMany?traceIds=abcd,0e8b46e1-81b\");\n    assertThat(response.code()).isEqualTo(400);\n\n    assertThat(response.body().string())\n      .isEqualTo(\"0e8b46e1-81b should be lower-hex encoded with no prefix\");\n  }\n\n  @Test void tracesQueryRequiresNoParameters() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/traces\");\n    assertThat(response.isSuccessful()).isTrue();\n    assertThat(response.body().string())\n      .isEqualTo(\"[\" + new String(SpanBytesEncoder.JSON_V2.encodeList(TRACE), UTF_8) + \"]\");\n  }\n\n  @Test void v2WiresUp() throws Exception {\n    assertThat(get(\"/api/v2/services\").isSuccessful())\n      .isTrue();\n  }\n\n  @Test void doesntSetCacheControlOnNameEndpointsWhenLessThan4Services() throws Exception {\n    storage.accept(TRACE).execute();\n\n    assertThat(get(\"/api/v2/services\").header(\"Cache-Control\"))\n      .isNull();\n\n    assertThat(get(\"/api/v2/spans?serviceName=web\").header(\"Cache-Control\"))\n      .isNull();\n\n    assertThat(get(\"/api/v2/remoteServices?serviceName=web\").header(\"Cache-Control\"))\n      .isNull();\n  }\n\n  @Test void spanNameQueryWorksWithNonAsciiServiceName() throws Exception {\n    assertThat(get(\"/api/v2/spans?serviceName=个人信息服务\").code())\n      .isEqualTo(200);\n  }\n\n  @Test void remoteServiceNameQueryWorksWithNonAsciiServiceName() throws Exception {\n    assertThat(get(\"/api/v2/remoteServices?serviceName=个人信息服务\").code())\n      .isEqualTo(200);\n  }\n\n  @Test void remoteServiceNameReturnsCorrectJsonForEscapedWhitespaceInName()\n    throws Exception {\n    storage.accept(List.of(CLIENT_SPAN.toBuilder()\n      .localEndpoint(FRONTEND.toBuilder().serviceName(\"foo\\tbar\").build())\n      .build()))\n      .execute();\n    Response response = get(\"/api/v2/services\");\n    assertThat(response.isSuccessful()).isTrue();\n    assertThat(response.body().string()).isEqualTo(\"[\\\"foo\\\\tbar\\\"]\");\n  }\n\n  @Test void setsCacheControlOnNameEndpointsWhenMoreThan3Services() throws Exception {\n    List<String> services = List.of(\"foo\", \"bar\", \"baz\", \"quz\");\n    for (int i = 0; i < services.size(); i++) {\n      storage.accept(List.of(\n        Span.newBuilder().traceId(\"a\").id(i + 1).timestamp(TODAY).name(\"whopper\")\n          .localEndpoint(Endpoint.newBuilder().serviceName(services.get(i)).build())\n          .remoteEndpoint(Endpoint.newBuilder().serviceName(services.get(i) + 1).build())\n          .build()\n      )).execute();\n    }\n\n    assertThat(get(\"/api/v2/services\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=300, must-revalidate\");\n\n    assertThat(get(\"/api/v2/spans?serviceName=web\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=300, must-revalidate\");\n\n    assertThat(get(\"/api/v2/remoteServices?serviceName=web\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=300, must-revalidate\");\n\n    // Check that the response is alphabetically sorted.\n    assertThat(get(\"/api/v2/services\").body().string())\n      .isEqualTo(\"[\\\"bar\\\",\\\"baz\\\",\\\"foo\\\",\\\"quz\\\"]\");\n  }\n\n  @Test void shouldAllowAnyOriginByDefault() throws Exception {\n    Response response = client.newCall(new Request.Builder()\n      .url(url(server, \"/api/v2/traces\"))\n      .header(\"Origin\", \"http://foo.example.com\")\n      .build()).execute();\n\n    assertThat(response.isSuccessful()).isTrue();\n    assertThat(response.header(\"vary\")).isNull();\n    assertThat(response.header(\"access-control-allow-credentials\")).isNull();\n    assertThat(response.header(\"access-control-allow-origin\")).contains(\"*\");\n  }\n\n  @Test void forwardsApiForUi() throws Exception {\n    assertThat(get(\"/zipkin/api/v2/traces\").isSuccessful()).isTrue();\n    assertThat(get(\"/zipkin/api/v2/traces\").isSuccessful()).isTrue();\n  }\n\n  /** Simulate a proxy which forwards / to zipkin as opposed to resolving / -> /zipkin first */\n  @Test void redirectedHeaderUsesOriginalHostAndPort() throws Exception {\n    Request forwarded = new Request.Builder()\n      .url(url(server, \"/\"))\n      .addHeader(\"Host\", \"zipkin.com\")\n      .addHeader(\"X-Forwarded-Proto\", \"https\")\n      .addHeader(\"X-Forwarded-Port\", \"444\")\n      .build();\n\n    Response response = client.newBuilder().followRedirects(false).build()\n      .newCall(forwarded).execute();\n\n    // Redirect header should be the proxy, not the backed IP/port\n    assertThat(response.header(\"Location\"))\n      .isEqualTo(\"/zipkin/\");\n  }\n\n  @Test void infoEndpointIsAvailable() throws IOException {\n    Response info = get(\"/info\");\n    assertThat(info.isSuccessful()).isTrue();\n    assertThat(info.body().contentType().toString())\n      .isEqualTo(\"application/json; charset=utf-8\");\n    assertThat(info.body().string())\n      .isEqualToIgnoringWhitespace(stringFromClasspath(getClass(), \"info.json\"));\n  }\n\n  @Test void getTrace_spaceAfterTraceId() throws Exception {\n    storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/trace/\" + TRACE.get(0).traceId() + \" \");\n    assertThat(response.isSuccessful()).isTrue();\n\n    assertThat(response.body().bytes())\n      .containsExactly(SpanBytesEncoder.JSON_V2.encodeList(TRACE));\n  }\n\n  @Test void traceMethodDisallowed() {\n    // trace method is disallowed for any route but we just test couple of paths here, can't test them all\n    Arrays.stream(new String[]{\"/\", \"/api/v2/traces\", \"/whatever/and/not\"}).forEach(path -> {\n      final Response response;\n      try {\n        response = client.newCall(new Request.Builder().url(url(server, path))\n          .method(\"TRACE\", null).build())\n          .execute();\n        assertThat(response.isSuccessful()).isFalse();\n        assertThat(response.code()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.code());\n      } catch (IOException e) {\n        throw new RuntimeException(e.getMessage(), e);\n      }\n    });\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .build()).execute();\n  }\n\n  public static String url(Server server, String path) {\n    return \"http://localhost:\" + server.activeLocalPort() + path;\n  }\n\n  public static String stringFromClasspath(Class<?> thisClass, String path) throws IOException {\n    URL url = thisClass.getClassLoader().getResource(path);\n    assertThat(url).isNotNull();\n\n    try (InputStream fromClasspath = url.openStream()) {\n      return Okio.buffer(Okio.source(fromClasspath)).readUtf8();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerAutocomplete.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.util.List;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Integration test suite for autocomplete tags.\n * <p>\n * Verifies that the whitelist of key can be configured via \"zipkin.storage.autocomplete-keys\".\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.storage.autocomplete-keys=environment,clnt/finagle.version\"\n  }\n)\nclass ITZipkinServerAutocomplete {\n\n  @Autowired Server server;\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @Test void setsCacheControlOnAutocompleteKeysEndpoint() throws Exception {\n    assertThat(get(\"/api/v2/autocompleteKeys\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=300, must-revalidate\");\n  }\n\n  @Test void setsCacheControlOnAutocompleteEndpointWhenMoreThan3Values() throws Exception {\n    assertThat(get(\"/api/v2/autocompleteValues?key=environment\").header(\"Cache-Control\"))\n      .isNull();\n    assertThat(get(\"/api/v2/autocompleteValues?key=clnt/finagle.version\").header(\"Cache-Control\"))\n      .isNull();\n\n    for (int i = 0; i < 4; i++) {\n      post(\"/api/v2/spans\", SpanBytesEncoder.JSON_V2.encodeList(List.of(\n        Span.newBuilder().traceId(\"a\").id(i + 1).timestamp(TODAY).name(\"whopper\")\n          .putTag(\"clnt/finagle.version\", \"6.45.\" + i).build()\n      )));\n    }\n\n    assertThat(get(\"/api/v2/autocompleteValues?key=environment\").header(\"Cache-Control\"))\n      .isNull();\n    assertThat(get(\"/api/v2/autocompleteValues?key=clnt/finagle.version\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=300, must-revalidate\");\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .build()).execute();\n  }\n\n  private Response post(String path, byte[] body) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .post(RequestBody.create(body))\n      .build()).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerCORS.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Integration test suite for CORS configuration.\n * <p>\n * Verifies that allowed-origins can be configured via properties (zipkin.query.allowed-origins).\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.query.allowed-origins=\" + ITZipkinServerCORS.ALLOWED_ORIGIN\n  }\n)\nclass ITZipkinServerCORS {\n  static final String ALLOWED_ORIGIN = \"http://foo.example.com\";\n  static final String DISALLOWED_ORIGIN = \"http://bar.example.com\";\n\n  @Autowired Server server;\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  /** Notably, javascript makes pre-flight requests, and won't POST spans if disallowed! */\n  @Test void shouldAllowConfiguredOrigin_preflight() throws Exception {\n    shouldPermitPreflight(optionsForOrigin(\"GET\", \"/api/v2/traces\", ALLOWED_ORIGIN));\n    shouldPermitPreflight(optionsForOrigin(\"POST\", \"/api/v2/spans\", ALLOWED_ORIGIN));\n  }\n\n  static void shouldPermitPreflight(Response response) {\n    assertThat(response.isSuccessful())\n      .withFailMessage(response.toString())\n      .isTrue();\n    assertThat(response.header(\"vary\")).contains(\"origin\");\n    assertThat(response.header(\"access-control-allow-origin\")).contains(ALLOWED_ORIGIN);\n    assertThat(response.header(\"access-control-allow-methods\"))\n      .contains(response.request().header(\"access-control-request-method\"));\n    assertThat(response.header(\"access-control-allow-credentials\")).isNull();\n    assertThat(response.header(\"access-control-allow-headers\")).contains(\"content-type\");\n  }\n\n  @Test void shouldAllowConfiguredOrigin() throws Exception {\n    shouldAllowConfiguredOrigin(getTracesFromOrigin(ALLOWED_ORIGIN));\n    shouldAllowConfiguredOrigin(postSpansFromOrigin(ALLOWED_ORIGIN));\n  }\n\n  static void shouldAllowConfiguredOrigin(Response response) {\n    assertThat(response.header(\"vary\")).contains(\"origin\");\n    assertThat(response.header(\"access-control-allow-origin\"))\n      .contains(response.request().header(\"origin\"));\n    assertThat(response.header(\"access-control-allow-credentials\")).isNull();\n    assertThat(response.header(\"access-control-allow-headers\")).contains(\"content-type\");\n  }\n\n  @Test void shouldDisallowOrigin() throws Exception {\n    shouldDisallowOrigin(getTracesFromOrigin(DISALLOWED_ORIGIN));\n    shouldDisallowOrigin(postSpansFromOrigin(DISALLOWED_ORIGIN));\n  }\n\n  static void shouldDisallowOrigin(Response response) {\n    assertThat(response.header(\"vary\")).isNull();\n    assertThat(response.header(\"access-control-allow-credentials\")).isNull();\n    assertThat(response.header(\"access-control-allow-origin\")).isNull();\n    assertThat(response.header(\"access-control-allow-headers\")).isNull();\n  }\n\n  private Response optionsForOrigin(String method, String path, String origin) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .header(\"Origin\", origin)\n      .header(\"access-control-request-method\", method)\n      .header(\"access-control-request-headers\", \"content-type\")\n      .method(\"OPTIONS\", null)\n      .build()).execute();\n  }\n\n  private Response getTracesFromOrigin(String origin) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, \"/api/v2/traces\"))\n      .header(\"Origin\", origin)\n      .build()).execute();\n  }\n\n  private Response postSpansFromOrigin(String origin) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, \"/api/v2/spans\"))\n      .header(\"Origin\", origin)\n      .post(RequestBody.create(\"[]\", MediaType.parse(\"application/json\")))\n      .build()).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Query-only builds should be able to disable the HTTP collector, so that associated assets 404\n * instead of allowing creation of spans.\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.storage.type=\", // cheat and test empty storage type\n    \"zipkin.collector.http.enabled=false\"\n  })\nclass ITZipkinServerHttpCollectorDisabled {\n\n  @Autowired Server server;\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @Test void httpCollectorEndpointReturns404() throws Exception {\n    Response response = client.newCall(new Request.Builder()\n      .url(url(server, \"/api/v2/spans\"))\n      .post(RequestBody.create(\"[]\", MediaType.parse(\"application/json\")))\n      .build()).execute();\n\n    assertThat(response.code()).isEqualTo(404);\n  }\n\n  /** Shows the same http path still works for GET */\n  @Test void getOnSpansEndpointReturnsOK() throws Exception {\n    Response response = client.newCall(new Request.Builder()\n      .url(url(server, \"/api/v2/spans?serviceName=unknown\"))\n      .build()).execute();\n\n    assertThat(response.isSuccessful()).isTrue();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerQueryDisabled.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Collector-only builds should be able to disable the query (and indirectly the UI), so that\n * associated assets 404 vs throw exceptions.\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.query.enabled=false\",\n    \"zipkin.ui.enabled=false\"\n  }\n)\nclass ITZipkinServerQueryDisabled {\n  @Autowired Server server;\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @Test void queryRelatedEndpoints404() throws Exception {\n    assertThat(get(\"/api/v2/traces\").code()).isEqualTo(404);\n    assertThat(get(\"/index.html\").code()).isEqualTo(404);\n\n    // but other endpoints are ok\n    assertThat(get(\"/health\").isSuccessful()).isTrue();\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerSsl.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.client.ClientFactory;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport com.linecorp.armeria.server.Server;\nimport com.linecorp.armeria.spring.ArmeriaSettings;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.elasticsearch.Access.configureSsl;\n\n/**\n * This code ensures you can setup SSL. Look at {@link ArmeriaSettings} for property names.\n *\n * <p>This is inspired by com.linecorp.armeria.spring.ArmeriaSslConfigurationTest\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    // TODO: use normal spring.server properties after https://github.com/line/armeria/issues/1834\n    \"armeria.ssl.enabled=true\",\n    \"armeria.ssl.key-store=classpath:keystore.p12\",\n    \"armeria.ssl.key-store-password=password\",\n    \"armeria.ssl.key-store-type=PKCS12\",\n    \"armeria.ssl.trust-store=classpath:keystore.p12\",\n    \"armeria.ssl.trust-store-password=password\",\n    \"armeria.ssl.trust-store-type=PKCS12\",\n    \"armeria.ports[1].port=0\",\n    \"armeria.ports[1].protocols[0]=https\",\n    // redundant in zipkin-server-shared https://github.com/spring-projects/spring-boot/issues/16394\n    \"armeria.ports[0].port=${server.port}\",\n    \"armeria.ports[0].protocols[0]=http\",\n  })\nclass ITZipkinServerSsl {\n  @Autowired Server server;\n  @Autowired ArmeriaSettings armeriaSettings;\n\n  ClientFactory clientFactory;\n\n  @BeforeEach void configureClientFactory() {\n    clientFactory = configureSsl(ClientFactory.builder(), armeriaSettings.getSsl()).build();\n  }\n\n  @Test void callHealthEndpoint_HTTP() {\n    callHealthEndpoint(SessionProtocol.HTTP);\n  }\n\n  @Test void callHealthEndpoint_HTTPS() {\n    callHealthEndpoint(SessionProtocol.HTTPS);\n  }\n\n  void callHealthEndpoint(SessionProtocol http) {\n    AggregatedHttpResponse response =\n      WebClient.builder(baseUrl(server, http)).factory(clientFactory).build()\n        .get(\"/health\")\n        .aggregate().join();\n\n    assertThat(response.status()).isEqualTo(HttpStatus.OK);\n  }\n\n  static String baseUrl(Server server, SessionProtocol protocol) {\n    return server.activePorts().values().stream()\n      .filter(p -> p.hasProtocol(protocol)).findAny()\n      .map(p -> protocol.uriText() + \"://localhost:\" + p.localAddress().getPort())\n      .orElseThrow(() -> new AssertionError(protocol + \" port not open\"));\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerTimeout.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.util.List;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.mock.mockito.MockBean;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.internal.TracesAdapter;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.query.timeout=1ms\"\n  }\n)\nclass ITZipkinServerTimeout {\n  static final List<Span> TRACE = List.of(TestObjects.CLIENT_SPAN);\n\n  SlowSpanStore spanStore;\n\n  @MockBean StorageComponent storage;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @BeforeEach void init() {\n    spanStore = new SlowSpanStore();\n    when(storage.spanStore()).thenReturn(spanStore);\n    when(storage.traces()).thenReturn(new TracesAdapter(spanStore));\n  }\n\n  @Test void getTrace() throws Exception {\n    spanStore.storage.accept(TRACE).execute();\n\n    Response response = get(\"/api/v2/trace/\" + TRACE.get(0).traceId());\n    assertThat(response.isSuccessful()).isFalse();\n\n    assertThat(response.code()).isEqualTo(500);\n  }\n\n  Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .build()).execute();\n  }\n\n  static String url(Server server, String path) {\n    return \"http://localhost:\" + server.activeLocalPort() + path;\n  }\n\n  static class SlowSpanStore implements SpanStore {\n    final InMemoryStorage storage = InMemoryStorage.newBuilder().build();\n\n    @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {\n      sleep();\n      return storage.spanStore().getTraces(request);\n    }\n\n    @Override public Call<List<Span>> getTrace(String traceId) {\n      sleep();\n      return storage.spanStore().getTrace(traceId);\n    }\n\n    @Override public Call<List<String>> getServiceNames() {\n      sleep();\n      return storage.spanStore().getServiceNames();\n    }\n\n    @Override public Call<List<String>> getSpanNames(String serviceName) {\n      sleep();\n      return storage.spanStore().getSpanNames(serviceName);\n    }\n\n    @Override public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n      sleep();\n      return storage.spanStore().getDependencies(endTs, lookback);\n    }\n\n    static void sleep() {\n      try {\n        Thread.sleep(500);\n      } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n        throw new Error(e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/InMemoryConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.collector.CollectorMetrics;\nimport zipkin2.collector.CollectorSampler;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.StorageComponent;\n\n@Configuration\npublic class InMemoryConfiguration {\n  @Bean public CollectorSampler sampler() {\n    return CollectorSampler.ALWAYS_SAMPLE;\n  }\n\n  @Bean public CollectorMetrics metrics() {\n    return CollectorMetrics.NOOP_METRICS;\n  }\n\n  @Bean public StorageComponent storage() {\n    return InMemoryStorage.newBuilder().build();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/NoOpMeterRegistryConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport com.linecorp.armeria.common.metric.NoopMeterRegistry;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class NoOpMeterRegistryConfiguration {\n  @Bean public MeterRegistry noOpMeterRegistry() {\n    return NoopMeterRegistry.get();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ZipkinActuatorImporterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.support.GenericApplicationContext;\n\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.server.internal.ZipkinActuatorImporter.PROPERTY_NAME_ACTUATOR_ENABLED;\n\n// This tests actuator integration without actually requiring a compile dep on actuator\nclass ZipkinActuatorImporterTest {\n  ZipkinActuatorImporter zipkinActuatorImporter =\n    new ZipkinActuatorImporter(ActuatorImpl.class.getName());\n  GenericApplicationContext context = new GenericApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesntCrashWhenNoIncludes() {\n    zipkinActuatorImporter.initialize(context);\n\n    context.refresh();\n  }\n\n  @Test void configuresInclude() {\n    TestPropertyValues.of(\n      \"zipkin.internal.actuator.include[0]=\" + Include1.class.getName()\n    ).applyTo(context);\n\n    zipkinActuatorImporter.initialize(context);\n\n    context.refresh();\n    context.getBean(Include1.class);\n  }\n\n  @Test void doesntCrashOnBadActuatorImpl() {\n    TestPropertyValues.of(\n      \"zipkin.internal.actuator.include[0]=\" + Include1.class.getName()\n    ).applyTo(context);\n\n    new ZipkinActuatorImporter(\"tomatoes\").initialize(context);\n\n    context.refresh();\n    assertThatThrownBy(() -> context.getBean(Include1.class))\n      .isInstanceOf(NoSuchBeanDefinitionException.class);\n  }\n\n  @Test void skipsWhenDisabled() {\n    TestPropertyValues.of(\n      PROPERTY_NAME_ACTUATOR_ENABLED + \"=false\",\n      \"zipkin.internal.actuator.include[1]=\" + Include2.class.getName()\n    ).applyTo(context);\n\n    zipkinActuatorImporter.initialize(context);\n\n    context.refresh();\n\n    assertThatThrownBy(() -> context.getBean(Include1.class))\n      .isInstanceOf(NoSuchBeanDefinitionException.class);\n  }\n\n  @Test void doesntCrashWhenBadInclude() {\n    TestPropertyValues.of(\n      \"zipkin.internal.actuator.include[0]=tomatoes\"\n    ).applyTo(context);\n\n    zipkinActuatorImporter.initialize(context);\n\n    context.refresh();\n  }\n\n  @Test void configuresIncludes() {\n    TestPropertyValues.of(\n      \"zipkin.internal.actuator.include[0]=\" + Include1.class.getName(),\n      \"zipkin.internal.actuator.include[1]=\" + Include2.class.getName()\n    ).applyTo(context);\n\n    zipkinActuatorImporter.initialize(context);\n\n    context.refresh();\n    context.getBean(Include1.class);\n    context.getBean(Include2.class);\n  }\n\n  @Configuration\n  static class ActuatorImpl {\n  }\n\n  @Configuration\n  static class Include1 {\n  }\n\n  @Configuration\n  static class Include2 {\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ZipkinHttpConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport brave.Tracing;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.prometheus.PrometheusConfig;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.convert.ApplicationConversionService;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.convert.ConversionService;\nimport zipkin2.server.internal.brave.ZipkinSelfTracingConfiguration;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinHttpConfigurationTest {\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void httpCollector_enabledByDefault() {\n    registerBaseConfig(context);\n    context.register(ZipkinHttpCollector.class);\n    context.refresh();\n\n    assertThat(context.getBean(ZipkinHttpCollector.class)).isNotNull();\n  }\n\n  @Test void httpCollector_canDisable() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.collector.http.enabled:false\").applyTo(context);\n      registerBaseConfig(context);\n      context.register(ZipkinHttpCollector.class);\n      context.refresh();\n\n      context.getBean(ZipkinHttpCollector.class);\n    });\n  }\n\n  @Test void query_enabledByDefault() {\n    registerBaseConfig(context);\n    context.register(ZipkinQueryApiV2.class);\n    context.refresh();\n\n    assertThat(context.getBean(ZipkinQueryApiV2.class)).isNotNull();\n  }\n\n  @Test void query_canDisable() {\n    TestPropertyValues.of(\"zipkin.query.enabled:false\").applyTo(context);\n    registerBaseConfig(context);\n    context.register(ZipkinQueryApiV2.class);\n    context.refresh();\n\n    assertThatThrownBy(() -> context.getBean(ZipkinQueryApiV2.class))\n      .isInstanceOf(NoSuchBeanDefinitionException.class);\n  }\n\n  @Test void selfTracing_canEnable() {\n    TestPropertyValues.of(\"zipkin.self-tracing.enabled:true\").applyTo(context);\n    registerBaseConfig(context);\n    context.register(ZipkinSelfTracingConfiguration.class);\n    context.refresh();\n\n    context.getBean(Tracing.class).close();\n  }\n\n  @Test void search_canDisable() {\n    TestPropertyValues.of(\"zipkin.storage.search-enabled:false\").applyTo(context);\n    registerBaseConfig(context);\n    context.refresh();\n\n    StorageComponent v2Storage = context.getBean(StorageComponent.class);\n    assertThat(v2Storage)\n      .extracting(\"searchEnabled\")\n      .isEqualTo(false);\n  }\n\n  @Configuration\n  public static class Config {\n    @Bean MeterRegistry registry() {\n      return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);\n    }\n\n    @Bean ConversionService conversionService() {\n      return ApplicationConversionService.getSharedInstance();\n    }\n  }\n\n  static void registerBaseConfig(AnnotationConfigApplicationContext context) {\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      Config.class,\n      ZipkinConfiguration.class,\n      ZipkinHttpConfiguration.class\n    );\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ZipkinModuleImporterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.support.GenericApplicationContext;\n\nclass ZipkinModuleImporterTest {\n  ZipkinModuleImporter zipkinModuleImporter = new ZipkinModuleImporter();\n  GenericApplicationContext context = new GenericApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesntCrashWhenNoModules() {\n    zipkinModuleImporter.initialize(context);\n\n    context.refresh();\n  }\n\n  @Test void configuresModule() {\n    TestPropertyValues.of(\n      \"zipkin.internal.module.module1=\" + Module1.class.getName()\n    ).applyTo(context);\n\n    zipkinModuleImporter.initialize(context);\n\n    context.refresh();\n    context.getBean(Module1.class);\n  }\n\n  @Test void doesntCrashWhenBadModule() {\n    TestPropertyValues.of(\n      \"zipkin.internal.module.module1=tomatoes\"\n    ).applyTo(context);\n\n    zipkinModuleImporter.initialize(context);\n\n    context.refresh();\n  }\n\n  @Test void configuresModules() {\n    TestPropertyValues.of(\n      \"zipkin.internal.module.module1=\" + Module1.class.getName(),\n      \"zipkin.internal.module.module2=\" + Module2.class.getName()\n    ).applyTo(context);\n\n    zipkinModuleImporter.initialize(context);\n\n    context.refresh();\n    context.getBean(Module1.class);\n    context.getBean(Module2.class);\n  }\n\n  @Configuration\n  static class Module1 {\n  }\n\n  @Configuration\n  static class Module2 {\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/activemq/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.activemq;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.collector.activemq.ActiveMQCollector;\n\n/** opens package access for testing */\npublic final class Access {\n\n  /** Just registering properties to avoid automatically connecting to a ActiveMQ server */\n  public static void registerActiveMQProperties(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, EnableActiveMQCollectorProperties.class);\n  }\n\n  @Configuration\n  @EnableConfigurationProperties(ZipkinActiveMQCollectorProperties.class)\n  static class EnableActiveMQCollectorProperties {}\n\n  public static ActiveMQCollector.Builder collectorBuilder(\n      AnnotationConfigApplicationContext context) {\n    return context.getBean(ZipkinActiveMQCollectorProperties.class).toBuilder();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/activemq/ZipkinActiveMQCollectorConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.activemq;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.collector.activemq.ActiveMQCollector;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinActiveMQCollectorConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesNotProvideCollectorComponent_whenAddressAndUriNotSet() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinActiveMQCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(ActiveMQCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenUrlSet() {\n    TestPropertyValues.of(\"zipkin.collector.activemq.url=vm://localhost\")\n      .applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinActiveMQCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n\n    try {\n      context.refresh();\n      failBecauseExceptionWasNotThrown(BeanCreationException.class);\n    } catch (BeanCreationException e) {\n      assertThat(e.getCause()).hasMessage(\n        \"Unable to establish connection to ActiveMQ broker: Transport scheme NOT recognized: [vm]\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/activemq/ZipkinActiveMQCollectorPropertiesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.activemq;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.collector.activemq.ActiveMQCollector;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass ZipkinActiveMQCollectorPropertiesTest {\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  /** This prevents an empty ACTIVEMQ_URL variable from being mistaken as a real one */\n  @Test void ignoresEmptyURL() {\n    ZipkinActiveMQCollectorProperties properties = new ZipkinActiveMQCollectorProperties();\n    properties.setUrl(\"\");\n\n    assertThat(properties.getUrl()).isNull();\n  }\n\n  @Test void providesCollectorComponent_whenUrlSet() {\n    TestPropertyValues.of(\"zipkin.collector.activemq.url:tcp://localhost:61611\") // wrong port\n      .applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinActiveMQCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n\n    assertThatThrownBy(context::refresh)\n      .isInstanceOf(BeanCreationException.class)\n      .hasMessageContaining(\"Unable to establish connection to ActiveMQ broker\");\n  }\n\n  @Test void doesNotProvidesCollectorComponent_whenUrlSetAndDisabled() {\n    TestPropertyValues.of(\"zipkin.collector.activemq.url:tcp://localhost:61616\")\n      .applyTo(context);\n    TestPropertyValues.of(\"zipkin.collector.activemq.enabled:false\").applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinActiveMQCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n    context.refresh();\n\n    assertThatThrownBy(() -> context.getBean(ActiveMQCollector.class))\n      .isInstanceOf(NoSuchBeanDefinitionException.class);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/banner/ZipkinBannerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.banner;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.PrintStream;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.ansi.AnsiOutput;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ZipkinBannerTest {\n  @AfterEach void tearDown() {\n    AnsiOutput.setEnabled(AnsiOutput.Enabled.DETECT);\n  }\n\n  @Test void shouldReplaceWhenAnsiEnabled() {\n    AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);\n\n    ZipkinBanner banner = new ZipkinBanner();\n    ByteArrayOutputStream out = new ByteArrayOutputStream();\n    banner.printBanner(null, null, new PrintStream(out));\n\n    assertThat(out.toString(UTF_8))\n      .doesNotContain(\"${\")\n      .contains(\"\\033\"); // ansi codes\n  }\n\n  @Test void shouldReplaceWhenAnsiDisabled() {\n    AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER);\n\n    ZipkinBanner banner = new ZipkinBanner();\n    ByteArrayOutputStream out = new ByteArrayOutputStream();\n    banner.printBanner(null, null, new PrintStream(out));\n\n    assertThat(out.toString(UTF_8))\n      .doesNotContain(\"${\")\n      .doesNotContain(\"\\033\"); // ansi codes\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/brave/ITZipkinSelfTracing.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.brave;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Component;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.reporter.brave.AsyncZipkinSpanHandler;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.QueryRequest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * This class is flaky for as yet unknown reasons. For example, in CI, sometimes assertions fail\n * due to incomplete traces. Hence, it includes more assertion customization than normal.\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.self-tracing.enabled=true\",\n    \"zipkin.self-tracing.message-timeout=100ms\",\n    \"zipkin.self-tracing.traces-per-second=100\"\n  })\nclass ITZipkinSelfTracing {\n  @Autowired TracingStorageComponent storage;\n  @Autowired AsyncZipkinSpanHandler zipkinSpanHandler;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @BeforeEach void clear() {\n    inMemoryStorage().clear();\n  }\n\n  InMemoryStorage inMemoryStorage() {\n    return (InMemoryStorage) storage.delegate;\n  }\n\n  @Test void getIsTraced_v2() throws Exception {\n    assertThat(getServices(\"v2\").body().string()).isEqualTo(\"[]\");\n\n    List<List<Span>> traces = awaitSpans(2);\n\n    assertQueryReturnsResults(QueryRequest.newBuilder()\n      .annotationQuery(Map.of(\"http.path\", \"/api/v2/services\")), traces);\n\n    assertQueryReturnsResults(QueryRequest.newBuilder().spanName(\"get-service-names\"), traces);\n  }\n\n  @Test\n  @Disabled(\"https://github.com/openzipkin/zipkin/issues/2781\")\n  void postIsTraced_v1() throws Exception {\n    postSpan(\"v1\");\n\n    List<List<Span>> traces = awaitSpans(3); // test span + POST + accept-spans\n\n    assertQueryReturnsResults(QueryRequest.newBuilder()\n      .annotationQuery(Map.of(\"http.path\", \"/api/v1/spans\")), traces);\n\n    assertQueryReturnsResults(QueryRequest.newBuilder().spanName(\"accept-spans\"), traces);\n  }\n\n  @Test\n  @Disabled(\"https://github.com/openzipkin/zipkin/issues/2781\")\n  void postIsTraced_v2() throws Exception {\n    postSpan(\"v2\");\n\n    List<List<Span>> traces = awaitSpans(3); // test span + POST + accept-spans\n\n    assertQueryReturnsResults(QueryRequest.newBuilder()\n      .annotationQuery(Map.of(\"http.path\", \"/api/v2/spans\")), traces);\n\n    assertQueryReturnsResults(QueryRequest.newBuilder().spanName(\"accept-spans\"), traces);\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    assertThat(storage).hasToString(\"Traced{InMemoryStorage{}}\");\n    assertThat(zipkinSpanHandler).hasToString(\"AsyncReporter{StorageComponent}\");\n  }\n\n  List<List<Span>> awaitSpans(int count) {\n    await().untilAsserted(() -> { // wait for spans\n      List<List<Span>> traces = inMemoryStorage().getTraces();\n      long received = traces.stream().mapToLong(List::size).sum();\n      assertThat(inMemoryStorage().acceptedSpanCount())\n        .withFailMessage(\"Wanted %s spans: got %s. Current traces: %s\", count, received, traces)\n        .isGreaterThanOrEqualTo(count);\n    });\n    return inMemoryStorage().getTraces();\n  }\n\n  void assertQueryReturnsResults(QueryRequest.Builder builder, List<List<Span>> traces)\n    throws IOException {\n    QueryRequest query = builder.endTs(System.currentTimeMillis()).lookback(DAY).limit(2).build();\n    assertThat(inMemoryStorage().getTraces(query).execute())\n      .withFailMessage(\"Expected results from %s. Current traces: %s\", query, traces)\n      .isNotEmpty();\n  }\n\n  /**\n   * This POSTs a single span. Afterwards, we expect this trace in storage, and also the self-trace\n   * of POSTing it.\n   */\n  void postSpan(String version) throws IOException {\n    SpanBytesEncoder encoder =\n      \"v1\".equals(version) ? SpanBytesEncoder.JSON_V1 : SpanBytesEncoder.JSON_V2;\n\n    List<Span> testTrace = List.of(\n      Span.newBuilder().timestamp(TODAY).traceId(\"1\").id(\"2\").name(\"test-trace\").build()\n    );\n\n    Response response = client.newCall(new Request.Builder()\n      .url(url(server, \"/api/\" + version + \"/spans\"))\n      .post(RequestBody.create(encoder.encodeList(testTrace)))\n      .build())\n      .execute();\n    assertSuccessful(response);\n  }\n\n  Response getServices(String version) throws IOException {\n    Response response = client.newCall(new Request.Builder()\n      .url(url(server, \"/api/\" + version + \"/services\"))\n      .build())\n      .execute();\n    assertSuccessful(response);\n    return response;\n  }\n\n  static void assertSuccessful(Response response) throws IOException {\n    assertThat(response.isSuccessful())\n      .withFailMessage(\"unsuccessful %s: %s\", response.request(),\n        response.peekBody(Long.MAX_VALUE).string())\n      .isTrue();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/cassandra3/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.cassandra3;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\n\n/** opens package access for testing */\npublic final class Access {\n\n  public static void registerCassandra3(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, ZipkinCassandra3StorageConfiguration.class);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.ClientFactoryBuilder;\nimport com.linecorp.armeria.spring.Ssl;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.NoOpMeterRegistryConfiguration;\n\n/** opens package access for testing */\npublic final class Access {\n\n  public static void registerElasticsearch(AnnotationConfigApplicationContext context) {\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      NoOpMeterRegistryConfiguration.class,\n      ZipkinElasticsearchStorageConfiguration.class);\n  }\n\n  public static ClientFactoryBuilder configureSsl(ClientFactoryBuilder builder, Ssl ssl) {\n    ZipkinElasticsearchStorageProperties.Ssl eSsl = new ZipkinElasticsearchStorageProperties.Ssl();\n    eSsl.setKeyStore(ssl.getKeyStore());\n    eSsl.setKeyStorePassword(ssl.getKeyStorePassword());\n    eSsl.setKeyStoreType(ssl.getKeyStoreType());\n    eSsl.setTrustStore(ssl.getTrustStore());\n    eSsl.setTrustStorePassword(ssl.getTrustStorePassword());\n    eSsl.setTrustStoreType(ssl.getTrustStoreType());\n    try {\n      return ZipkinElasticsearchStorageConfiguration.configureSsl(builder, eSsl);\n    } catch (Exception e) {\n      throw new AssertionError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchAuth.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.server.ServerBuilder;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.TrustManagerFactory;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.VERSION_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.YELLOW_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageProperties.Ssl;\n\nclass ITElasticsearchAuth {\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension() {\n    @Override protected void configureServer(ServerBuilder sb) throws Exception {\n      sb.https(0);\n      Ssl ssl = new Ssl();\n      ssl.setKeyStore(\"classpath:keystore.jks\");\n      ssl.setKeyStorePassword(\"password\");\n      ssl.setTrustStore(\"classpath:keystore.jks\");\n      ssl.setTrustStorePassword(\"password\");\n\n      final KeyManagerFactory keyManagerFactory = SslUtil.getKeyManagerFactory(ssl);\n      final TrustManagerFactory trustManagerFactory = SslUtil.getTrustManagerFactory(ssl);\n      sb.tls(keyManagerFactory)\n        .tlsCustomizer(sslContextBuilder -> {\n          sslContextBuilder.keyManager(keyManagerFactory);\n          sslContextBuilder.trustManager(trustManagerFactory);\n        });\n    }\n  };\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n  ElasticsearchStorage storage;\n\n  @BeforeEach void init() {\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type=elasticsearch\",\n      \"zipkin.storage.elasticsearch.ensure-templates=false\",\n      \"zipkin.storage.elasticsearch.username=Aladdin\",\n      \"zipkin.storage.elasticsearch.password=OpenSesame\",\n      \"zipkin.storage.elasticsearch.hosts=https://localhost:\" + server.httpsPort(),\n      \"zipkin.storage.elasticsearch.ssl.key-store=classpath:keystore.jks\",\n      \"zipkin.storage.elasticsearch.ssl.key-store-password=password\",\n      \"zipkin.storage.elasticsearch.ssl.trust-store=classpath:keystore.jks\",\n      \"zipkin.storage.elasticsearch.ssl.trust-store-password=password\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n    storage = context.getBean(ElasticsearchStorage.class);\n  }\n\n  @AfterEach void close() {\n    storage.close();\n  }\n\n  @Test void healthcheck_usesAuthAndTls() {\n    server.enqueue(VERSION_RESPONSE.toHttpResponse());\n    server.enqueue(YELLOW_RESPONSE.toHttpResponse());\n\n    assertThat(storage.check().ok()).isTrue();\n\n    AggregatedHttpRequest next = server.takeRequest().request();\n    // hard coded for sanity taken from https://en.wikipedia.org/wiki/Basic_access_authentication\n    assertThat(next.headers().get(\"Authorization\"))\n      .isEqualTo(\"Basic QWxhZGRpbjpPcGVuU2VzYW1l\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchClientInitialization.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.CheckResult;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ITElasticsearchClientInitialization {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  /**\n   * This blocks for less than the timeout of 2 second to prove we defer i/o until first use of the\n   * storage component.\n   */\n  @Test @Timeout(1900L) void defersIOUntilFirstUse() {\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.timeout:2000\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234,127.0.0.1:5678\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    context.getBean(ElasticsearchStorage.class).close();\n  }\n\n  /** blocking a little is ok, but blocking forever is not. */\n  @Test @Timeout(3000L) void doesntHangWhenAllDown() {\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.timeout:1000\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234,127.0.0.1:5678\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      CheckResult result = storage.check();\n      assertThat(result.ok()).isFalse();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchDynamicCredentials.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.server.ServerBuilder;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport java.io.File;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.TrustManagerFactory;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.VERSION_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.YELLOW_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.ZipkinElasticsearchStorageProperties.Ssl;\n\nclass ITElasticsearchDynamicCredentials {\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension() {\n    @Override protected void configureServer(ServerBuilder sb) throws Exception {\n      sb.https(0);\n      Ssl ssl = new Ssl();\n      ssl.setKeyStore(\"classpath:keystore.jks\");\n      ssl.setKeyStorePassword(\"password\");\n      ssl.setTrustStore(\"classpath:keystore.jks\");\n      ssl.setTrustStorePassword(\"password\");\n\n      final KeyManagerFactory keyManagerFactory = SslUtil.getKeyManagerFactory(ssl);\n      final TrustManagerFactory trustManagerFactory = SslUtil.getTrustManagerFactory(ssl);\n      sb.tls(keyManagerFactory)\n        .tlsCustomizer(sslContextBuilder -> {\n          sslContextBuilder.keyManager(keyManagerFactory);\n          sslContextBuilder.trustManager(trustManagerFactory);\n        });\n    }\n  };\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n  ElasticsearchStorage storage;\n  String credentialsFile;\n\n  @BeforeEach void init() {\n    credentialsFile = pathOfResource(\"es-credentials\");\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type=elasticsearch\",\n      \"zipkin.storage.elasticsearch.ensure-templates=false\",\n      \"zipkin.storage.elasticsearch.hosts=https://localhost:\" + server.httpsPort(),\n      \"zipkin.storage.elasticsearch.credentials-file=\" + credentialsFile,\n      \"zipkin.storage.elasticsearch.credentials-refresh-interval=3\",\n      \"zipkin.storage.elasticsearch.ssl.key-store=classpath:keystore.jks\",\n      \"zipkin.storage.elasticsearch.ssl.key-store-password=password\",\n      \"zipkin.storage.elasticsearch.ssl.trust-store=classpath:keystore.jks\",\n      \"zipkin.storage.elasticsearch.ssl.trust-store-password=password\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n    storage = context.getBean(ElasticsearchStorage.class);\n  }\n\n  @AfterEach void close() {\n    storage.close();\n  }\n\n  @Test void healthcheck_usesDynamicCredentialsAndTls() {\n    server.enqueue(VERSION_RESPONSE.toHttpResponse());\n    server.enqueue(YELLOW_RESPONSE.toHttpResponse());\n    assertThat(storage.check().ok()).isTrue();\n    AggregatedHttpRequest next = server.takeRequest().request();\n    assertThat(next.headers().get(\"Authorization\")).isEqualTo(\"Basic Zm9vOmJhcg==\");\n  }\n\n  static String pathOfResource(String resource) {\n    File file = new File(\n      ITElasticsearchDynamicCredentials.class.getClassLoader().getResource(resource).getFile());\n    return file.getAbsolutePath();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchHealthCheck.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.endpoint.EndpointSelectionTimeoutException;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.server.ServerBuilder;\nimport com.linecorp.armeria.server.healthcheck.HealthCheckService;\nimport com.linecorp.armeria.server.healthcheck.SettableHealthChecker;\nimport com.linecorp.armeria.testing.junit5.server.ServerExtension;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.TimeUnit;\nimport javax.net.ssl.SSLException;\nimport org.awaitility.core.ConditionFactory;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.CheckResult;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.GREEN_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.VERSION_RESPONSE;\n\n/**\n * These tests focus on http client health checks not currently in zipkin-storage-elasticsearch.\n */\nclass ITElasticsearchHealthCheck {\n  static final Logger logger = LoggerFactory.getLogger(ITElasticsearchHealthCheck.class.getName());\n  // Health check interval is 100ms, but in-flight requests in CI might take a few hundred ms\n  static final ConditionFactory awaitTimeout = await().timeout(1, TimeUnit.SECONDS);\n\n  static final SettableHealthChecker server1Health = new SettableHealthChecker(true);\n\n  @RegisterExtension\n  static ServerExtension server1 = new ServerExtension() {\n    @Override protected void configure(ServerBuilder sb) {\n      sb.service(\"/\", (ctx, req) -> sendResponseAfterAggregate(req, VERSION_RESPONSE));\n      sb.service(\"/_cluster/health\", HealthCheckService.of(server1Health));\n      sb.serviceUnder(\"/_cluster/health/\", (ctx, req) -> GREEN_RESPONSE.toHttpResponse());\n    }\n  };\n\n  /** This ensures the response is sent after the request is fully read. */\n  private static HttpResponse sendResponseAfterAggregate(HttpRequest req,\n    AggregatedHttpResponse response) {\n    final CompletableFuture<HttpResponse> future = new CompletableFuture<>();\n    req.aggregate().whenComplete((aggregatedReq, cause) -> {\n      if (cause != null) {\n        future.completeExceptionally(cause);\n      } else {\n        future.complete(response.toHttpResponse());\n      }\n    });\n    return HttpResponse.from(future);\n  }\n\n  static final SettableHealthChecker server2Health = new SettableHealthChecker(true);\n\n  @RegisterExtension\n  static ServerExtension server2 = new ServerExtension() {\n    @Override protected void configure(ServerBuilder sb) {\n      sb.service(\"/\", (ctx, req) -> sendResponseAfterAggregate(req, VERSION_RESPONSE));\n      sb.service(\"/_cluster/health\", HealthCheckService.of(server2Health));\n      sb.serviceUnder(\"/_cluster/health/\",\n        (ctx, req) -> sendResponseAfterAggregate(req, GREEN_RESPONSE));\n    }\n  };\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @BeforeEach void setUp() {\n    server1Health.setHealthy(true);\n    server2Health.setHealthy(true);\n\n    logger.info(\"server 1: {}, server 2: {}\", server1.httpUri(), server2.httpUri());\n\n    initWithHosts(\"127.0.0.1:\" + server1.httpPort() + \",127.0.0.1:\" + server2.httpPort());\n  }\n\n  private void initWithHosts(String hosts) {\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type=elasticsearch\",\n      \"zipkin.storage.elasticsearch.ensure-templates=false\",\n      \"zipkin.storage.elasticsearch.timeout=200\",\n      \"zipkin.storage.elasticsearch.health-check.enabled=true\",\n      // uncomment (and also change log4j2.properties) to see health-checks requests in the console\n      //\"zipkin.storage.elasticsearch.health-check.http-logging=headers\",\n      \"zipkin.storage.elasticsearch.health-check.interval=100ms\",\n      \"zipkin.storage.elasticsearch.hosts=\" + hosts)\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n  }\n\n  @Test void allHealthy() {\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n\n      // There's an initialization delay, so await instead of expect everything up now.\n      awaitTimeout.untilAsserted(() -> assertThat(storage.check().ok()).isTrue());\n    }\n  }\n\n  @Test void oneHealthy() {\n    server1Health.setHealthy(false);\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      assertOk(storage.check());\n    }\n  }\n\n  @Test void wrongScheme() {\n    context.close();\n    context = new AnnotationConfigApplicationContext();\n    initWithHosts(\"https://localhost:\" + server1.httpPort());\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      CheckResult result = storage.check();\n      assertThat(result.ok()).isFalse();\n      // Test this is not wrapped in a rejection exception, as health check is not throttled\n      // Depending on JDK this is SSLHandshakeException or NotSslRecordException\n      assertThat(result.error()).isInstanceOf(SSLException.class);\n    }\n  }\n\n  @Test void noneHealthy() {\n    server1Health.setHealthy(false);\n    server2Health.setHealthy(false);\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      CheckResult result = storage.check();\n      assertThat(result.ok()).isFalse();\n      assertThat(result.error())\n        .isInstanceOf(EndpointSelectionTimeoutException.class);\n    }\n  }\n\n  // If this flakes, uncomment in initWithHosts and log4j2.properties\n  @Test void healthyThenNotHealthyThenHealthy() {\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      assertOk(storage.check());\n\n      logger.info(\"setting server 1 and 2 unhealthy\");\n      server1Health.setHealthy(false);\n      server2Health.setHealthy(false);\n\n      awaitTimeout.untilAsserted(() -> assertThat(storage.check().ok()).isFalse());\n\n      logger.info(\"setting server 1 healthy\");\n      server1Health.setHealthy(true);\n\n      awaitTimeout.untilAsserted(() -> assertThat(storage.check().ok()).isTrue());\n    }\n  }\n\n  @Test void notHealthyThenHealthyThenNotHealthy() {\n    server1Health.setHealthy(false);\n    server2Health.setHealthy(false);\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      CheckResult result = storage.check();\n      assertThat(result.ok()).isFalse();\n\n      server2Health.setHealthy(true);\n\n      awaitTimeout.untilAsserted(() -> assertThat(storage.check().ok()).isTrue());\n\n      server2Health.setHealthy(false);\n\n      awaitTimeout.untilAsserted(() -> assertThat(storage.check().ok()).isFalse());\n    }\n  }\n\n  @Test void healthCheckDisabled() {\n    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.ensure-templates=false\",\n      \"zipkin.storage.elasticsearch.timeout=200\",\n      \"zipkin.storage.elasticsearch.health-check.enabled=false\",\n      \"zipkin.storage.elasticsearch.health-check.interval=100ms\",\n      \"zipkin.storage.elasticsearch.hosts=127.0.0.1:\" +\n        server1.httpPort() + \",127.0.0.1:\" + server2.httpPort())\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    server1Health.setHealthy(false);\n    server2Health.setHealthy(false);\n\n    try (ElasticsearchStorage storage = context.getBean(ElasticsearchStorage.class)) {\n      // Even though cluster health is false, we ignore that and continue to check index health,\n      // which is correctly returned by our mock server.\n      assertOk(storage.check());\n    }\n  }\n\n  static void assertOk(CheckResult result) {\n    if (!result.ok()) {\n      Throwable error = result.error();\n      throw new AssertionError(\"Health check failed with message: \" + error.getMessage(), error);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchNoVerify.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.VERSION_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.YELLOW_RESPONSE;\n\nclass ITElasticsearchNoVerify {\n  @RegisterExtension\n  static MockWebServerExtension server = new MockWebServerExtension();\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n  ElasticsearchStorage storage;\n\n  @BeforeEach void init() {\n    TestPropertyValues.of(\n        \"spring.config.name=zipkin-server\",\n        \"zipkin.storage.type=elasticsearch\",\n        \"zipkin.storage.elasticsearch.ensure-templates=false\",\n        \"zipkin.storage.elasticsearch.hosts=https://localhost:\" + server.httpsPort(),\n        \"zipkin.storage.elasticsearch.ssl.no-verify=true\")\n        .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n    storage = context.getBean(ElasticsearchStorage.class);\n  }\n\n  @AfterEach void close() {\n    storage.close();\n  }\n\n  @Test void healthcheck_no_tls_verify() {\n    server.enqueue(VERSION_RESPONSE.toHttpResponse());\n    server.enqueue(YELLOW_RESPONSE.toHttpResponse());\n\n    assertThat(storage.check().ok()).isTrue();\n  }\n\n  @Test void service_no_tls_verify() throws Exception {\n    server.enqueue(\n        AggregatedHttpResponse.of(ResponseHeaders.of(HttpStatus.OK), HttpData.ofUtf8(\"{}\")));\n\n    assertThat(storage.serviceAndSpanNames().getServiceNames().execute()).isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ITElasticsearchSelfTracing.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.server.internal.brave.ZipkinSelfTracingConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.VERSION_RESPONSE;\nimport static zipkin2.server.internal.elasticsearch.TestResponses.YELLOW_RESPONSE;\n\nclass ITElasticsearchSelfTracing {\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n  ElasticsearchStorage storage;\n\n  @BeforeEach void init() {\n    TestPropertyValues.of(\n      \"spring.config.name=zipkin-server\",\n      \"zipkin.self-tracing.enabled=true\",\n      \"zipkin.self-tracing.message-timeout=1ms\",\n      \"zipkin.self-tracing.traces-per-second=10\",\n      \"zipkin.storage.type=elasticsearch\",\n      \"zipkin.storage.elasticsearch.ensure-templates=false\",\n      \"zipkin.storage.elasticsearch.hosts=\" + server.httpUri()).applyTo(context);\n    Access.registerElasticsearch(context);\n    context.register(ZipkinSelfTracingConfiguration.class);\n    context.refresh();\n    storage = context.getBean(ElasticsearchStorage.class);\n  }\n\n  @AfterEach void close() {\n    storage.close();\n  }\n\n  /**\n   * We currently don't have a nice way to mute outbound propagation in Brave. This just makes sure\n   * we are nicer.\n   */\n  @Test void healthcheck_usesB3Single() {\n    server.enqueue(VERSION_RESPONSE.toHttpResponse());\n    server.enqueue(YELLOW_RESPONSE.toHttpResponse());\n\n    assertThat(storage.check().ok()).isTrue();\n\n    assertThat(server.takeRequest().request().headers())\n      .extracting(e -> e.getKey().toString())\n      .contains(\"b3\")\n      .doesNotContain(\"x-b3-traceid\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/InitialEndpointSupplierTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.Endpoint;\nimport org.junit.jupiter.api.Test;\n\nimport static com.linecorp.armeria.common.SessionProtocol.HTTP;\nimport static com.linecorp.armeria.common.SessionProtocol.HTTPS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass InitialEndpointSupplierTest {\n\n  @Test void defaultIsLocalhost9200RegardlessOfSessionProtocol() {\n    assertThat(new InitialEndpointSupplier(HTTP, null).get())\n      .isEqualTo(Endpoint.of(\"localhost\", 9200))\n      .isEqualTo(new InitialEndpointSupplier(HTTPS, null).get());\n  }\n\n  @Test void usesNaturalHttpPortsWhenUrls() {\n    assertThat(new InitialEndpointSupplier(HTTP, \"http://localhost\").get())\n      .isEqualTo(Endpoint.of(\"localhost\", 80));\n    assertThat(new InitialEndpointSupplier(HTTPS, \"https://localhost\").get())\n      .isEqualTo(Endpoint.of(\"localhost\", 443));\n  }\n\n  @Test void defaultsPlainHostsToPort9200() {\n    assertThat(new InitialEndpointSupplier(HTTP, \"localhost\").get())\n      .isEqualTo(Endpoint.of(\"localhost\", 9200));\n    assertThat(new InitialEndpointSupplier(HTTPS, \"localhost\").get())\n      .isEqualTo(Endpoint.of(\"localhost\", 443));\n  }\n\n  /** This helps ensure old setups don't break (provided they have http port 9200 open) */\n  @Test void coersesPort9300To9200() {\n    assertThat(new InitialEndpointSupplier(HTTP, \"localhost:9300\").get())\n      .isEqualTo(Endpoint.of(\"localhost\", 9200));\n  }\n\n  @Test void parsesListOfLocalhosts() {\n    String hostList = \"localhost:9201,localhost:9202\";\n    assertThat(new InitialEndpointSupplier(HTTP, hostList).get().endpoints())\n      .containsExactly(Endpoint.of(\"localhost\", 9201), Endpoint.of(\"localhost\", 9202))\n      .containsExactlyElementsOf(new InitialEndpointSupplier(HTTPS, hostList).get().endpoints());\n  }\n\n  @Test void parsesListOfLocalhosts_skipsBlankEntry() {\n    String hostList = \"localhost:9201,,localhost:9202\";\n    assertThat(new InitialEndpointSupplier(HTTP, hostList).get().endpoints())\n      .containsExactly(Endpoint.of(\"localhost\", 9201), Endpoint.of(\"localhost\", 9202))\n      .containsExactlyElementsOf(new InitialEndpointSupplier(HTTPS, hostList).get().endpoints());\n  }\n\n  @Test void parsesEmptyListOfHosts_toDefault() {\n    assertThat(new InitialEndpointSupplier(HTTP, \"\").get().endpoints())\n      .containsExactly(Endpoint.of(\"localhost\", 9200));\n  }\n\n  @Test void parsesListOfLocalhosts_failsWhenAllInvalid() {\n    InitialEndpointSupplier supplier = new InitialEndpointSupplier(HTTP, \",\");\n    assertThatThrownBy(supplier::get)\n      .isInstanceOf(IllegalArgumentException.class)\n      .hasMessage(\"No valid endpoints found in ES hosts: ,\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/TestResponses.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\n\nimport static com.linecorp.armeria.common.HttpStatus.OK;\nimport static com.linecorp.armeria.common.MediaType.JSON;\n\nfinal class TestResponses {\n  static final AggregatedHttpResponse VERSION_RESPONSE = AggregatedHttpResponse.of(OK, JSON, \"\"\"\n    {\n      \"name\" : \"PV-NhJd\",\n      \"cluster_name\" : \"CollectorDBCluster\",\n      \"cluster_uuid\" : \"UjZaM0fQRC6tkHINCg9y8w\",\n      \"version\" : {\n        \"number\" : \"6.7.0\",\n        \"build_flavor\" : \"oss\",\n        \"build_type\" : \"tar\",\n        \"build_hash\" : \"8453f77\",\n        \"build_date\" : \"2019-03-21T15:32:29.844721Z\",\n        \"build_snapshot\" : false,\n        \"lucene_version\" : \"7.7.0\",\n        \"minimum_wire_compatibility_version\" : \"5.6.0\",\n        \"minimum_index_compatibility_version\" : \"5.0.0\"\n      },\n      \"tagline\" : \"You Know, for Search\"\n    }\n    \"\"\");\n  static final AggregatedHttpResponse YELLOW_RESPONSE = AggregatedHttpResponse.of(OK, JSON, \"\"\"\n    {\n      \"cluster_name\": \"CollectorDBCluster\",\n      \"status\": \"yellow\",\n      \"timed_out\": false,\n      \"number_of_nodes\": 1,\n      \"number_of_data_nodes\": 1,\n      \"active_primary_shards\": 5,\n      \"active_shards\": 5,\n      \"relocating_shards\": 0,\n      \"initializing_shards\": 0,\n      \"unassigned_shards\": 5,\n      \"delayed_unassigned_shards\": 0,\n      \"number_of_pending_tasks\": 0,\n      \"number_of_in_flight_fetch\": 0,\n      \"task_max_waiting_in_queue_millis\": 0,\n      \"active_shards_percent_as_number\": 50\n    }\n    \"\"\");\n  static final AggregatedHttpResponse GREEN_RESPONSE = AggregatedHttpResponse.of(OK, JSON,\n    \"\"\"\n    {\n      \"cluster_name\": \"CollectorDBCluster\",\n      \"status\": \"green\",\n      \"timed_out\": false,\n      \"number_of_nodes\": 1,\n      \"number_of_data_nodes\": 1,\n      \"active_primary_shards\": 5,\n      \"active_shards\": 5,\n      \"relocating_shards\": 0,\n      \"initializing_shards\": 0,\n      \"unassigned_shards\": 5,\n      \"delayed_unassigned_shards\": 0,\n      \"number_of_pending_tasks\": 0,\n      \"number_of_in_flight_fetch\": 0,\n      \"task_max_waiting_in_queue_millis\": 0,\n      \"active_shards_percent_as_number\": 50\n    }\n    \"\"\");\n\n  private TestResponses() {\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/ZipkinElasticsearchStorageConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.elasticsearch;\n\nimport com.linecorp.armeria.client.ClientOptions;\nimport com.linecorp.armeria.client.ClientOptionsBuilder;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.beans.factory.UnsatisfiedDependencyException;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static zipkin2.server.internal.elasticsearch.ITElasticsearchDynamicCredentials.pathOfResource;\n\nclass ZipkinElasticsearchStorageConfigurationTest {\n  final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesntProvideStorageComponent_whenStorageTypeNotElasticsearch() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.storage.type:cassandra\").applyTo(context);\n      Access.registerElasticsearch(context);\n      context.refresh();\n\n      es();\n    });\n  }\n\n  @Test void providesStorageComponent_whenStorageTypeElasticsearchAndHostsAreUrls() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).isNotNull();\n  }\n\n  @Test void canOverridesProperty_hostsWithList() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200,http://host2:9200\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(context.getBean(ZipkinElasticsearchStorageProperties.class).getHosts())\n      .isEqualTo(\"http://host1:9200,http://host2:9200\");\n  }\n\n  @Test void decentToString_whenUnresolvedOrUnhealthy() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://127.0.0.1:9200,http://127.0.0.1:9201\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).hasToString(\n      \"ElasticsearchStorage{initialEndpoints=http://127.0.0.1:9200,http://127.0.0.1:9201, index=zipkin}\");\n  }\n\n  @Test void configuresPipeline() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.storage.elasticsearch.pipeline:zipkin\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().pipeline()).isEqualTo(\"zipkin\");\n  }\n\n  @Test void httpPrefixOptional() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:host1:9200\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(context.getBean(SessionProtocol.class))\n      .isEqualTo(SessionProtocol.HTTP);\n  }\n\n  @Test void https() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:https://localhost\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(context.getBean(SessionProtocol.class))\n      .isEqualTo(SessionProtocol.HTTPS);\n    assertThat(context.getBean(InitialEndpointSupplier.class).get().endpoints().get(0).port())\n      .isEqualTo(443);\n  }\n\n  @Configuration\n  static class CustomizerConfiguration {\n\n    @Bean @Qualifier(\"zipkinElasticsearch\") public Consumer<ClientOptionsBuilder> one() {\n      return one;\n    }\n\n    @Bean @Qualifier(\"zipkinElasticsearch\") public Consumer<ClientOptionsBuilder> two() {\n      return two;\n    }\n\n    Consumer<ClientOptionsBuilder> one = client -> client.maxResponseLength(12345L);\n    Consumer<ClientOptionsBuilder> two =\n      client -> client.addHeader(\"test\", \"bar\");\n  }\n\n  /** Ensures we can wire up network interceptors, such as for logging or authentication */\n  @Test void usesInterceptorsQualifiedWith_zipkinElasticsearchHttp() {\n    TestPropertyValues.of(\"zipkin.storage.type:elasticsearch\").applyTo(context);\n    Access.registerElasticsearch(context);\n    context.register(CustomizerConfiguration.class);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n    assertThat(factory.options.maxResponseLength()).isEqualTo(12345L);\n    assertThat(factory.options.headers().get(\"test\")).isEqualTo(\"bar\");\n  }\n\n  @Test void timeout_defaultsTo10Seconds() {\n    TestPropertyValues.of(\"zipkin.storage.type:elasticsearch\").applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n    // TODO(anuraaga): Verify connect timeout after https://github.com/line/armeria/issues/1890\n    assertThat(factory.options.responseTimeoutMillis()).isEqualTo(10000L);\n    assertThat(factory.options.writeTimeoutMillis()).isEqualTo(10000L);\n  }\n\n  @Test void timeout_override() {\n    long timeout = 30000L;\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\",\n      \"zipkin.storage.elasticsearch.timeout:\" + timeout)\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n    // TODO(anuraaga): Verify connect timeout after https://github.com/line/armeria/issues/1890\n    assertThat(factory.options.responseTimeoutMillis()).isEqualTo(timeout);\n    assertThat(factory.options.writeTimeoutMillis()).isEqualTo(timeout);\n  }\n\n  @Test void strictTraceId_defaultsToTrue() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n    assertThat(es().strictTraceId()).isTrue();\n  }\n\n  @Test void strictTraceId_canSetToFalse() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.storage.strict-trace-id:false\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().strictTraceId()).isFalse();\n  }\n\n  @Test void dailyIndexFormat() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().indexNameFormatter().formatTypeAndTimestamp(\"span\", 0))\n      .isEqualTo(\"zipkin*span-1970-01-01\");\n  }\n\n  @Test void dailyIndexFormat_overridingPrefix() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.storage.elasticsearch.index:zipkin_prod\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().indexNameFormatter().formatTypeAndTimestamp(\"span\", 0))\n      .isEqualTo(\"zipkin_prod*span-1970-01-01\");\n  }\n\n  @Test void dailyIndexFormat_overridingDateSeparator() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.storage.elasticsearch.date-separator:.\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().indexNameFormatter().formatTypeAndTimestamp(\"span\", 0))\n      .isEqualTo(\"zipkin*span-1970.01.01\");\n  }\n\n  @Test void dailyIndexFormat_overridingDateSeparator_empty() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.storage.elasticsearch.date-separator:\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().indexNameFormatter().formatTypeAndTimestamp(\"span\", 0))\n      .isEqualTo(\"zipkin*span-19700101\");\n  }\n\n  @Test void dailyIndexFormat_overridingDateSeparator_invalidToBeMultiChar() {\n    assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\n        \"zipkin.storage.type:elasticsearch\",\n        \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n        \"zipkin.storage.elasticsearch.date-separator:blagho\")\n        .applyTo(context);\n      Access.registerElasticsearch(context);\n\n      context.refresh();\n    });\n  }\n\n  @Test void namesLookbackAssignedFromQueryLookback() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:http://host1:9200\",\n      \"zipkin.query.lookback:\" + TimeUnit.DAYS.toMillis(2))\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es().namesLookback()).isEqualTo((int) TimeUnit.DAYS.toMillis(2));\n  }\n\n  @Test void doesntProvideBasicAuthInterceptor_whenBasicAuthUserNameandPasswordNotConfigured() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n    WebClient client = WebClient.builder(\"http://127.0.0.1:1234\")\n      .option(ClientOptions.DECORATION, factory.options.decoration())\n      .build();\n    assertThat(client.as(BasicAuthInterceptor.class)).isNull();\n  }\n\n  @Test void providesBasicAuthInterceptor_whenBasicAuthUserNameAndPasswordConfigured() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\",\n      \"zipkin.storage.elasticsearch.username:somename\",\n      \"zipkin.storage.elasticsearch.password:pass\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n\n    WebClient client = WebClient.builder(\"http://127.0.0.1:1234\")\n      .option(ClientOptions.DECORATION, factory.options.decoration())\n      .build();\n    assertThat(client.as(BasicAuthInterceptor.class)).isNotNull();\n  }\n\n  @Test void providesBasicAuthInterceptor_whenDynamicCredentialsConfigured() {\n    String credentialsFile = pathOfResource(\"es-credentials\");\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\",\n      \"zipkin.storage.elasticsearch.credentials-file:\" + credentialsFile,\n      \"zipkin.storage.elasticsearch.credentials-refresh-interval:2\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    HttpClientFactory factory = context.getBean(HttpClientFactory.class);\n\n    WebClient client = WebClient.builder(\"http://127.0.0.1:1234\")\n      .option(ClientOptions.DECORATION, factory.options.decoration())\n      .build();\n    assertThat(client.as(BasicAuthInterceptor.class)).isNotNull();\n    BasicCredentials basicCredentials =\n      Objects.requireNonNull(client.as(BasicAuthInterceptor.class)).basicCredentials;\n    String credentials = basicCredentials.getCredentials();\n    assertThat(credentials).isEqualTo(\"Basic Zm9vOmJhcg==\");\n  }\n\n  @Test void providesBasicAuthInterceptor_whenInvalidDynamicCredentialsConfigured() {\n    assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> {\n      String credentialsFile = pathOfResource(\"es-credentials-invalid\");\n      TestPropertyValues.of(\n        \"zipkin.storage.type:elasticsearch\",\n        \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\",\n        \"zipkin.storage.elasticsearch.credentials-file:\" + credentialsFile,\n        \"zipkin.storage.elasticsearch.credentials-refresh-interval:2\")\n        .applyTo(context);\n      Access.registerElasticsearch(context);\n      context.refresh();\n    });\n  }\n\n  @Test void providesBasicAuthInterceptor_whenDynamicCredentialsConfiguredButFileAbsent() {\n    assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\n        \"zipkin.storage.type:elasticsearch\",\n        \"zipkin.storage.elasticsearch.hosts:127.0.0.1:1234\",\n        \"zipkin.storage.elasticsearch.credentials-file:no-this-file\",\n        \"zipkin.storage.elasticsearch.credentials-refresh-interval:2\")\n        .applyTo(context);\n      Access.registerElasticsearch(context);\n      context.refresh();\n    });\n  }\n\n  @Test void searchEnabled_false() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.search-enabled:false\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"searchEnabled\")\n      .isEqualTo(false);\n  }\n\n  @Test void autocompleteKeys_list() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.autocomplete-keys:environment\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"autocompleteKeys\")\n      .isEqualTo(List.of(\"environment\"));\n  }\n\n  @Test void autocompleteTtl() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.autocomplete-ttl:60000\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"autocompleteTtl\")\n      .isEqualTo(60000);\n  }\n\n  @Test void autocompleteCardinality() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.autocomplete-cardinality:5000\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"autocompleteCardinality\")\n      .isEqualTo(5000);\n  }\n\n  @Test void templatePriority_valid() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.template-priority:0\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"templatePriority\")\n      .isEqualTo(0);\n  }\n\n  @Test void templatePriority_null() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:elasticsearch\",\n      \"zipkin.storage.elasticsearch.template-priority:\")\n      .applyTo(context);\n    Access.registerElasticsearch(context);\n    context.refresh();\n\n    assertThat(es()).extracting(\"templatePriority\")\n      .isNull();\n  }\n\n  @Test void templatePriority_Invalid() {\n    assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\n        \"zipkin.storage.type:elasticsearch\",\n        \"zipkin.storage.elasticsearch.template-priority:string\")\n        .applyTo(context);\n      Access.registerElasticsearch(context);\n      context.refresh();\n\n      es();\n    });\n  }\n\n  ElasticsearchStorage es() {\n    return context.getBean(ElasticsearchStorage.class);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/eureka/BaseITZipkinEureka.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport com.jayway.jsonpath.JsonPath;\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.Map;\nimport okhttp3.HttpUrl;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport org.junit.jupiter.api.MethodOrderer.OrderAnnotation;\nimport org.junit.jupiter.api.Order;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestMethodOrder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.junit.jupiter.Testcontainers;\nimport zipkin.server.ZipkinServer;\n\nimport static okhttp3.Credentials.basic;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static org.testcontainers.utility.DockerImageName.parse;\n\n/**\n * Integration test for Eureka, which validates with <a href=\"https://github.com/Netflix/eureka/wiki/Eureka-REST-operations\">Eureka REST operations</a>\n *\n * <p>Note: We only validate that authentication works, as it is cheaper than also testing without\n * it.\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.storage.type=\", // cheat and test empty storage type\n    \"zipkin.collector.http.enabled=false\",\n    \"zipkin.query.enabled=false\",\n    \"zipkin.ui.enabled=false\",\n    \"zipkin.discovery.eureka.hostname=localhost\"\n  })\n@Tag(\"docker\")\n@Testcontainers(disabledWithoutDocker = true)\n@TestMethodOrder(OrderAnnotation.class) // so that deregistration tests don't flake the others.\nabstract class BaseITZipkinEureka {\n  /**\n   * Path under the Eureka v2 endpoint for the app named \"zipkin\".\n   * Note that Eureka always coerces app names to uppercase.\n   */\n  private static final String APPS_ZIPKIN = \"apps/ZIPKIN\";\n  private final OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @Autowired Server zipkin;\n\n  final HttpUrl serviceUrl;\n\n  BaseITZipkinEureka(HttpUrl serviceUrl) {\n    this.serviceUrl = serviceUrl;\n  }\n\n  @Test @Order(1) void registersInEureka() throws IOException {\n    // The zipkin server may start before Eureka processes the registration\n    await().untilAsserted( // wait for registration\n      () -> {\n        try (Response response = getEurekaZipkinApp()) {\n          assertThat(response.code()).isEqualTo(200);\n        }\n      });\n\n    String json = getEurekaZipkinAppAsString();\n\n    // Make sure the health status is OK\n    assertThat(readString(json, \"$.application.instance[0].status\"))\n      .isEqualTo(\"UP\");\n\n    int zipkinPort = zipkin.activePort().localAddress().getPort();\n\n    // Note: Netflix/Eureka says use hostname, which can conflict on laptops.\n    // Armeria adopts the spring-cloud-netflix convention shown here.\n    assertThat(readString(json, \"$.application.instance[0].instanceId\"))\n      .isEqualTo(\"localhost:zipkin:\" + zipkinPort);\n\n    // Make sure the vip address does not include the port!\n    assertThat(readString(json, \"$.application.instance[0].vipAddress\"))\n      .isEqualTo(\"localhost\");\n\n    // Make sure URLs are fully resolved. Notably, the status page URL defaults to the /info\n    // endpoint, as that's the one chosen in spring-cloud-netflix.\n    assertThat(readString(json, \"$.application.instance[0].homePageUrl\"))\n      .isEqualTo(\"http://localhost:\" + zipkinPort + \"/zipkin\");\n    assertThat(readString(json, \"$.application.instance[0].statusPageUrl\"))\n      .isEqualTo(\"http://localhost:\" + zipkinPort + \"/info\");\n    assertThat(readString(json, \"$.application.instance[0].healthCheckUrl\"))\n      .isEqualTo(\"http://localhost:\" + zipkinPort + \"/health\");\n  }\n\n  @Test @Order(2) void deregistersOnClose() {\n    zipkin.close();\n    await().untilAsserted( // wait for deregistration\n      () -> {\n        try (Response response = getEurekaZipkinApp()) {\n          assertThat(response.code()).isEqualTo(404);\n        }\n      });\n  }\n\n  private String getEurekaZipkinAppAsString() throws IOException {\n    try (Response response = getEurekaZipkinApp(); ResponseBody body = response.body()) {\n      assertThat(response.isSuccessful()).withFailMessage(response.toString()).isTrue();\n      return body != null ? body.string() : \"\";\n    }\n  }\n\n  private Response getEurekaZipkinApp() throws IOException {\n    HttpUrl url = serviceUrl.newBuilder().addEncodedPathSegments(APPS_ZIPKIN).build();\n    Request.Builder rb = new Request.Builder().url(url)\n      .header(\"Accept\", \"application/json\"); // XML is default\n    if (!url.username().isEmpty()) {\n      rb.header(\"Authorization\", basic(url.username(), url.password()));\n    }\n    return client.newCall(rb.build()).execute();\n  }\n\n  static final class EurekaContainer extends GenericContainer<EurekaContainer> {\n    static final Logger LOGGER = LoggerFactory.getLogger(EurekaContainer.class);\n    static final int EUREKA_PORT = 8761;\n\n    EurekaContainer(Map<String, String> env) {\n      super(parse(\"ghcr.io/openzipkin/zipkin-eureka:3.4.3\"));\n      withEnv(env);\n      withExposedPorts(EUREKA_PORT);\n      waitStrategy = Wait.forHealthcheck();\n      withStartupTimeout(Duration.ofSeconds(60));\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n\n    HttpUrl serviceUrl() {\n      return new HttpUrl.Builder()\n        .scheme(\"http\")\n        .host(getHost()).port(getMappedPort(EUREKA_PORT))\n        .addEncodedPathSegments(\"eureka/v2\").build();\n    }\n  }\n\n  static String readString(String json, String jsonPath) {\n    return JsonPath.compile(jsonPath).read(json);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/eureka/ITZipkinEureka.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport java.util.Map;\nimport okhttp3.HttpUrl;\nimport org.springframework.test.context.DynamicPropertyRegistry;\nimport org.springframework.test.context.DynamicPropertySource;\nimport org.testcontainers.junit.jupiter.Container;\n\nclass ITZipkinEureka extends BaseITZipkinEureka {\n  @Container static EurekaContainer eureka = new EurekaContainer(Map.of());\n\n  static HttpUrl serviceUrl() {\n    return eureka.serviceUrl();\n  }\n\n  /** Get the serviceUrl of the Eureka container prior to booting Zipkin. */\n  @DynamicPropertySource static void propertyOverride(DynamicPropertyRegistry registry) {\n    registry.add(\"zipkin.discovery.eureka.serviceUrl\", () -> serviceUrl().url());\n  }\n\n  ITZipkinEureka() {\n    super(serviceUrl());\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/eureka/ITZipkinEurekaAuthenticated.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport java.util.Map;\nimport okhttp3.HttpUrl;\nimport org.springframework.test.context.DynamicPropertyRegistry;\nimport org.springframework.test.context.DynamicPropertySource;\nimport org.testcontainers.junit.jupiter.Container;\n\nclass ITZipkinEurekaAuthenticated extends BaseITZipkinEureka {\n  static final String username = \"user\", password = \"pass\";\n  @Container static EurekaContainer eureka =\n    new EurekaContainer(Map.of(\"EUREKA_USERNAME\", username, \"EUREKA_PASSWORD\", password));\n\n  static HttpUrl serviceUrl() {\n    return eureka.serviceUrl().newBuilder().username(username).password(password).build();\n  }\n\n  /** Get the serviceUrl of the Eureka container prior to booting Zipkin. */\n  @DynamicPropertySource static void propertyOverride(DynamicPropertyRegistry registry) {\n    registry.add(\"zipkin.discovery.eureka.serviceUrl\", () -> serviceUrl().url());\n  }\n\n  ITZipkinEurekaAuthenticated() {\n    super(serviceUrl());\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport com.linecorp.armeria.server.eureka.EurekaUpdatingListener;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinEurekaDiscoveryConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesNotProvideDiscoveryComponent_whenServiceUrlUnset() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinEurekaDiscoveryConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(EurekaUpdatingListener.class);\n    });\n  }\n\n  @Test void doesNotProvidesEurekaUpdatingListener_whenServiceUrlEmptyString() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.discovery.eureka.service-url:\").applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinEurekaDiscoveryConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(EurekaUpdatingListener.class);\n    });\n  }\n\n  @Test void providesDiscoveryComponent_whenServiceUrlSet() {\n    TestPropertyValues.of(\"zipkin.discovery.eureka.service-url:http://localhost:8761/eureka/v2\")\n      .applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinEurekaDiscoveryConfiguration.class,\n      InMemoryConfiguration.class);\n    context.refresh();\n\n    assertThat(context.getBean(EurekaUpdatingListener.class)).isNotNull();\n  }\n\n  @Test void providesDiscoveryComponent_whenServiceUrlAuthenticates() {\n    TestPropertyValues.of(\n        \"zipkin.discovery.eureka.service-url:http://myuser:mypassword@localhost:8761/eureka/v2\")\n      .applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinEurekaDiscoveryConfiguration.class,\n      InMemoryConfiguration.class);\n    context.refresh();\n\n    assertThat(context.getBean(EurekaUpdatingListener.class)).isNotNull();\n  }\n\n  @Test void doesNotProvidesEurekaUpdatingListener_whenServiceUrlSetAndDisabled() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.discovery.eureka.service-url:http://localhost:8761/eureka/v2\")\n        .applyTo(context);\n      TestPropertyValues.of(\"zipkin.discovery.eureka.enabled:false\").applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinEurekaDiscoveryConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(EurekaUpdatingListener.class);\n    });\n  }\n\n  @Test void canOverrideProperty_appName() {\n    context = createContextWithOverridenProperty(\"zipkin.discovery.eureka.appName:zipkin-demo\");\n\n    assertThat(context.getBean(ZipkinEurekaDiscoveryProperties.class).getAppName())\n      .isEqualTo(\"zipkin-demo\");\n  }\n\n  @Test void canOverrideProperty_instanceId() {\n    context = createContextWithOverridenProperty(\"zipkin.discovery.eureka.instanceId:zipkin-demo\");\n\n    assertThat(context.getBean(ZipkinEurekaDiscoveryProperties.class).getInstanceId())\n      .isEqualTo(\"zipkin-demo\");\n  }\n\n  @Test void canOverrideProperty_hostname() {\n    context = createContextWithOverridenProperty(\"zipkin.discovery.eureka.hostname:zipkin-demo\");\n\n    assertThat(context.getBean(ZipkinEurekaDiscoveryProperties.class).getHostname())\n      .isEqualTo(\"zipkin-demo\");\n  }\n\n  private static AnnotationConfigApplicationContext createContextWithOverridenProperty(\n    String pair) {\n    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of( // Add a service-url so that the pair is read.\n      \"zipkin.discovery.eureka.service-url:http://localhost:8761/eureka/v2\",\n      pair\n    ).applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinEurekaDiscoveryConfiguration.class\n    );\n    context.refresh();\n    return context;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryPropertiesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.eureka;\n\nimport com.linecorp.armeria.common.auth.BasicToken;\nimport java.net.URI;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ZipkinEurekaDiscoveryPropertiesTest {\n  @Test void stringPropertiesConvertEmptyStringsToNull() {\n    ZipkinEurekaDiscoveryProperties properties = new ZipkinEurekaDiscoveryProperties();\n    properties.setServiceUrl(URI.create(\"\"));\n    properties.setAppName(\"\");\n    properties.setInstanceId(\"\");\n    properties.setHostname(\"\");\n    assertThat(properties.getServiceUrl()).isNull();\n    assertThat(properties.getAppName()).isNull();\n    assertThat(properties.getInstanceId()).isNull();\n    assertThat(properties.getHostname()).isNull();\n    assertThat(properties).extracting(\"auth\").isNull();\n  }\n\n  // Less logic to strip than fail on unlikely, but invalid input\n  @Test void setServiceUrl_stripsQueryAndFragment() {\n    ZipkinEurekaDiscoveryProperties properties = new ZipkinEurekaDiscoveryProperties();\n    properties.setServiceUrl(URI.create(\"http://localhost:8761/eureka/v2?q1=v1#toc\"));\n\n    assertThat(properties.getServiceUrl())\n      .isEqualTo(URI.create(\"http://localhost:8761/eureka/v2\"));\n    assertThat(properties).extracting(\"auth\").isNull();\n  }\n\n  @Test void setServiceUrl_extractsBasicToken() {\n    ZipkinEurekaDiscoveryProperties properties = new ZipkinEurekaDiscoveryProperties();\n    properties.setServiceUrl(URI.create(\"http://myuser:mypassword@localhost:8761/eureka/v2\"));\n\n    assertThat(properties.getServiceUrl())\n      .isEqualTo(URI.create(\"http://localhost:8761/eureka/v2\"));\n    assertThat(properties).extracting(\"auth\")\n      .isEqualTo(BasicToken.ofBasic(\"myuser\", \"mypassword\"));\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/health/ComponentHealthTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport com.linecorp.armeria.common.ClosedSessionException;\nimport java.io.IOException;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ComponentHealthTest {\n  @Test void addsMessageToDetails() {\n    ComponentHealth health = ComponentHealth.ofComponent(new Component() {\n      @Override public CheckResult check() {\n        return CheckResult.failed(new IOException(\"socket disconnect\"));\n      }\n    });\n\n    assertThat(health.error).isEqualTo(\"IOException: socket disconnect\");\n  }\n\n  @Test void doesntAddNullMessageToDetails() {\n    ComponentHealth health = ComponentHealth.ofComponent(new Component() {\n      @Override public CheckResult check() {\n        return CheckResult.failed(ClosedSessionException.get());\n      }\n    });\n\n    assertThat(health.error).isEqualTo(\"ClosedSessionException\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/health/ITZipkinHealth.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport com.jayway.jsonpath.JsonPath;\nimport com.linecorp.armeria.server.Server;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport java.io.IOException;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\nclass ITZipkinHealth {\n  @Autowired InMemoryStorage storage;\n  @Autowired PrometheusMeterRegistry registry;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @BeforeEach void init() {\n    storage.clear();\n  }\n\n  @Test void healthIsOK() throws Exception {\n    Response health = get(\"/health\");\n    assertThat(health.isSuccessful()).isTrue();\n    assertThat(health.body().contentType())\n      .hasToString(\"application/json; charset=utf-8\");\n    assertThat(health.body().string()).isEqualTo(\"\"\"\n      {\n        \"status\" : \"UP\",\n        \"zipkin\" : {\n          \"status\" : \"UP\",\n          \"details\" : {\n            \"InMemoryStorage{}\" : {\n              \"status\" : \"UP\"\n            }\n          }\n        }\n      }\"\"\"\n    );\n\n    // ensure we don't track health in prometheus\n    assertThat(scrape())\n      // \"application_ready_time_seconds\" includes this test's full class name\n      // which includes its package (named health). We care about the endpoint\n      // /health not being in the results, so check for that here.\n      .doesNotContain(\"/health\");\n  }\n\n  String scrape() throws InterruptedException {\n    Thread.sleep(100);\n    return registry.scrape();\n  }\n\n  @Test void readsHealth() throws Exception {\n    String json = getAsString(\"/health\");\n    assertThat(readString(json, \"$.status\"))\n      .isIn(\"UP\", \"DOWN\", \"UNKNOWN\");\n    assertThat(readString(json, \"$.zipkin.status\"))\n      .isIn(\"UP\", \"DOWN\", \"UNKNOWN\");\n  }\n\n  private String getAsString(String path) throws IOException {\n    Response response = get(path);\n    assertThat(response.isSuccessful())\n      .withFailMessage(response.toString())\n      .isTrue();\n    return response.body().string();\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n\n  static String readString(String json, String jsonPath) {\n    return JsonPath.compile(jsonPath).read(json);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/health/ITZipkinHealthDown.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.storage.type=elasticsearch\",\n    \"zipkin.storage.elasticsearch.hosts=127.0.0.1:9999\"\n  }\n)\nclass ITZipkinHealthDown {\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @Test void downHasCorrectCode() throws Exception {\n    Response check = get(\"/health\");\n    assertThat(check.code()).isEqualTo(503);\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/health/ZipkinHealthControllerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.health;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.health.ComponentHealth.STATUS_DOWN;\nimport static zipkin2.server.internal.health.ComponentHealth.STATUS_UP;\n\nclass ZipkinHealthControllerTest {\n  @Test void writeJsonError_writesNestedError() throws Exception {\n    assertThat(ZipkinHealthController.writeJsonError(\"robots\")).isEqualTo(\"\"\"\n      {\n        \"status\" : \"DOWN\",\n        \"zipkin\" : {\n          \"status\" : \"DOWN\",\n          \"details\" : {\n            \"error\" : \"robots\"\n          }\n        }\n      }\"\"\"\n    );\n  }\n\n  @Test void writeJson_mappedByName() throws Exception {\n    List<ComponentHealth> healths = List.of(\n      new ComponentHealth(\"foo\", STATUS_UP, null),\n      new ComponentHealth(\"bar\", STATUS_DOWN, \"java.io.IOException: socket disconnect\")\n    );\n    assertThat(ZipkinHealthController.writeJson(STATUS_DOWN, healths)).isEqualTo(\"\"\"\n      {\n        \"status\" : \"DOWN\",\n        \"zipkin\" : {\n          \"status\" : \"DOWN\",\n          \"details\" : {\n            \"foo\" : {\n              \"status\" : \"UP\"\n            },\n            \"bar\" : {\n              \"status\" : \"DOWN\",\n              \"details\" : {\n                \"error\" : \"java.io.IOException: socket disconnect\"\n              }\n            }\n          }\n        }\n      }\"\"\"\n    );\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/kafka/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.kafka;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.collector.kafka.KafkaCollector;\n\n/** opens package access for testing */\npublic final class Access {\n\n  /** Just registering properties to avoid automatically connecting to a Kafka server */\n  public static void registerKafkaProperties(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, EnableKafkaCollectorProperties.class);\n  }\n\n  @Configuration\n  @EnableConfigurationProperties(ZipkinKafkaCollectorProperties.class)\n  static class EnableKafkaCollectorProperties {}\n\n  public static KafkaCollector.Builder collectorBuilder(\n      AnnotationConfigApplicationContext context) {\n    return context.getBean(ZipkinKafkaCollectorProperties.class).toBuilder();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.kafka;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.collector.kafka.KafkaCollector;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinKafkaCollectorConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesNotProvideCollectorComponent_whenBootstrapServersUnset() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinKafkaCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(KafkaCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenBootstrapServersEmptyString() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.collector.kafka.bootstrap-servers:\").applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinKafkaCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(KafkaCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenBootstrapServersSet() {\n    TestPropertyValues.of(\"zipkin.collector.kafka.bootstrap-servers:localhost:9092\")\n      .applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinKafkaCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n    context.refresh();\n\n    assertThat(context.getBean(KafkaCollector.class)).isNotNull();\n  }\n\n  @Test void doesNotProvidesCollectorComponent_whenBootstrapServersSetAndDisabled() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.collector.kafka.bootstrap-servers:localhost:9092\")\n        .applyTo(context);\n      TestPropertyValues.of(\"zipkin.collector.kafka.enabled:false\").applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinKafkaCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(KafkaCollector.class);\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.kafka;\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ZipkinKafkaCollectorPropertiesTest {\n  @Test void stringPropertiesConvertEmptyStringsToNull() {\n    final ZipkinKafkaCollectorProperties properties = new ZipkinKafkaCollectorProperties();\n    properties.setBootstrapServers(\"\");\n    properties.setGroupId(\"\");\n    properties.setTopic(\"\");\n    assertThat(properties.getBootstrapServers()).isNull();\n    assertThat(properties.getGroupId()).isNull();\n    assertThat(properties.getTopic()).isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.mysql;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\n\n/** opens package access for testing */\npublic final class Access {\n\n  public static void registerMySQL(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, ZipkinMySQLStorageConfiguration.class);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ITZipkinMetrics.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.prometheus;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.linecorp.armeria.server.Server;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport java.io.IOException;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Only add tests that do not consider the value of a counter or gauge, as these will flake and so\n * should only exist in {@link ITZipkinMetricsDirty}.\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\nclass ITZipkinMetrics {\n  @Autowired InMemoryStorage storage;\n  @Autowired PrometheusMeterRegistry registry;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @BeforeEach void init() {\n    storage.clear();\n  }\n\n  @Test void metricsIsOK() throws Exception {\n    assertThat(get(\"/metrics\").isSuccessful())\n      .isTrue();\n\n    // ensure we don't track metrics in prometheus\n    assertThat(scrape())\n      .doesNotContain(\"metrics\");\n  }\n\n  @Test void prometheusIsOK() throws Exception {\n    assertThat(get(\"/prometheus\").isSuccessful())\n      .isTrue();\n\n    // ensure we don't track prometheus, UI requests in prometheus\n    assertThat(scrape())\n      .doesNotContain(\"uri=\\\"/prometheus\")\n      .doesNotContain(\"uri=\\\"/zipkin\")\n      .doesNotContain(\"uri=\\\"/\\\"\");\n  }\n\n  @Test void apiTemplate_prometheus() throws Exception {\n    List<Span> spans = List.of(LOTS_OF_SPANS[0]);\n    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);\n    assertThat(post(\"/api/v2/spans\", body).isSuccessful())\n      .isTrue();\n\n    assertThat(get(\"/api/v2/trace/\" + LOTS_OF_SPANS[0].traceId()).isSuccessful())\n      .isTrue();\n\n    assertThat(get(\"/api/v2/traceMany?traceIds=abcde,\" + LOTS_OF_SPANS[0].traceId()).isSuccessful())\n      .isTrue();\n\n    assertThat(scrape())\n      .contains(\"uri=\\\"/api/v2/traceMany\\\"\") // sanity check\n      .contains(\"uri=\\\"/api/v2/trace/{traceId}\\\"\")\n      .doesNotContain(LOTS_OF_SPANS[0].traceId());\n  }\n\n  @Test void forwardedRoute_prometheus() throws Exception {\n    assertThat(get(\"/zipkin/api/v2/services\").isSuccessful())\n        .isTrue();\n\n    assertThat(scrape())\n        .contains(\"uri=\\\"/api/v2/services\\\"\")\n        .doesNotContain(\"uri=\\\"/zipkin/api/v2/services\\\"\");\n  }\n\n  @Test void jvmMetrics_prometheus() throws Exception {\n    assertThat(scrape())\n        .contains(\"jvm_memory_max_bytes\")\n        .contains(\"jvm_memory_used_bytes\")\n        .contains(\"jvm_memory_committed_bytes\")\n        .contains(\"jvm_buffer_count_buffers\")\n        .contains(\"jvm_buffer_memory_used_bytes\")\n        .contains(\"jvm_buffer_total_capacity_bytes\")\n        .contains(\"jvm_classes_loaded_classes\")\n        .contains(\"jvm_classes_unloaded_classes_total\")\n        .contains(\"jvm_threads_live_threads\")\n        .contains(\"jvm_threads_states_threads\")\n        .contains(\"jvm_threads_peak_threads\")\n        .contains(\"jvm_threads_daemon_threads\");\n    // gc metrics are not tested as are not present during test running\n  }\n\n  String scrape() throws Exception {\n    Thread.sleep(100);\n    return registry.scrape();\n  }\n\n  @Test void writesSpans_readMetricsFormat() throws Exception {\n    byte[] span = {'z', 'i', 'p', 'k', 'i', 'n'};\n    List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);\n    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);\n    post(\"/api/v2/spans\", body);\n    post(\"/api/v2/spans\", body);\n    post(\"/api/v2/spans\", span);\n    Thread.sleep(1500);\n\n    String metrics = getAsString(\"/metrics\");\n\n    assertThat(readJson(metrics)).containsOnlyKeys(\n      \"gauge.zipkin_collector.message_spans.grpc\"\n      , \"gauge.zipkin_collector.message_bytes.grpc\"\n      , \"counter.zipkin_collector.messages.grpc\"\n      , \"counter.zipkin_collector.bytes.grpc\"\n      , \"counter.zipkin_collector.spans.grpc\"\n      , \"counter.zipkin_collector.messages_dropped.grpc\"\n      , \"counter.zipkin_collector.spans_dropped.grpc\"\n      , \"gauge.zipkin_collector.message_spans.http\"\n      , \"gauge.zipkin_collector.message_bytes.http\"\n      , \"counter.zipkin_collector.messages.http\"\n      , \"counter.zipkin_collector.bytes.http\"\n      , \"counter.zipkin_collector.spans.http\"\n      , \"counter.zipkin_collector.messages_dropped.http\"\n      , \"counter.zipkin_collector.spans_dropped.http\"\n    );\n  }\n\n  private String getAsString(String path) throws IOException {\n    Response response = get(path);\n    assertThat(response.isSuccessful())\n      .withFailMessage(response.toString())\n      .isTrue();\n    return response.body().string();\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n\n  private Response post(String path, byte[] body) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .post(RequestBody.create(body))\n      .build()).execute();\n  }\n\n  static Map<String, Integer> readJson(String json) throws Exception {\n    Map<String, Integer> result = new LinkedHashMap<>();\n    JsonParser parser = new JsonFactory().createParser(json);\n    assertThat(parser.nextToken()).isEqualTo(JsonToken.START_OBJECT);\n    String nextField;\n    while ((nextField = parser.nextFieldName()) != null) {\n      result.put(nextField, parser.nextIntValue(0));\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ITZipkinMetricsDirty.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.prometheus;\n\nimport com.jayway.jsonpath.JsonPath;\nimport com.linecorp.armeria.server.Server;\nimport io.micrometer.prometheus.PrometheusMeterRegistry;\nimport java.io.IOException;\nimport java.util.List;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.annotation.DirtiesContext;\nimport zipkin.server.ZipkinServer;\nimport zipkin2.Span;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.InMemoryStorage;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD;\nimport static zipkin2.TestObjects.LOTS_OF_SPANS;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n/**\n * Tests here look at values based on counter values, so need to run independently. It would seem\n * correct to {@link PrometheusMeterRegistry#clear() clear the registry} to isolate counters\n * incremented in one test from interfering with another. However, this clears the metrics\n * themselves, resulting in an empty {@link PrometheusMeterRegistry#scrape()}.\n *\n * <p>Currently, the only way we know how to reset the whole registry is to recreate the Spring\n * context, via {@link DirtiesContext} on each test. This is extremely slow, so please only add\n * tests that require isolation here!\n */\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n  }\n)\n// Clearing the prometheus registry also clears the metrics themselves, not just the values, so we\n// have to use dirties context so that each test runs in a separate instance of Spring Boot.\n@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)\nclass ITZipkinMetricsDirty {\n\n  @Autowired InMemoryStorage storage;\n  @Autowired PrometheusMeterRegistry registry;\n  @Autowired Server server;\n\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();\n\n  @BeforeEach void init() {\n    // We use DirtiesContext, not registry.clear(), as the latter would cause an empty scrape\n    storage.clear();\n  }\n\n  @Test void writeSpans_updatesMetrics() throws Exception {\n    List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);\n    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);\n    double messagesCount =\n      registry.counter(\"zipkin_collector.messages\", \"transport\", \"http\").count();\n    double bytesCount = registry.counter(\"zipkin_collector.bytes\", \"transport\", \"http\").count();\n    double spansCount = registry.counter(\"zipkin_collector.spans\", \"transport\", \"http\").count();\n    post(\"/api/v2/spans\", body);\n    post(\"/api/v2/spans\", body);\n\n    String json = getAsString(\"/metrics\");\n\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.messages.http']\"))\n      .isEqualTo(messagesCount + 2.0);\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.bytes.http']\"))\n      .isEqualTo(bytesCount + (body.length * 2));\n    assertThat(readDouble(json, \"$.['gauge.zipkin_collector.message_bytes.http']\"))\n      .isEqualTo(body.length);\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.spans.http']\"))\n      .isEqualTo(spansCount + (spans.size() * 2));\n    assertThat(readDouble(json, \"$.['gauge.zipkin_collector.message_spans.http']\"))\n      .isEqualTo(spans.size());\n  }\n\n  @Test void writeSpans_malformedUpdatesMetrics() throws Exception {\n    byte[] body = {'h', 'e', 'l', 'l', 'o'};\n    double messagesCount =\n      registry.counter(\"zipkin_collector.messages\", \"transport\", \"http\").count();\n    double messagesDroppedCount =\n      registry.counter(\"zipkin_collector.messages_dropped\", \"transport\", \"http\").count();\n    post(\"/api/v2/spans\", body);\n\n    String json = getAsString(\"/metrics\");\n\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.messages.http']\"))\n      .isEqualTo(messagesCount + 1);\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.messages_dropped.http']\"))\n      .isEqualTo(messagesDroppedCount + 1);\n  }\n\n  /** This tests logic in {@code BodyIsExceptionMessage} is scoped to POST requests. */\n  @Test void getTrace_malformedDoesntUpdateCollectorMetrics() throws Exception {\n    double messagesCount =\n      registry.counter(\"zipkin_collector.messages\", \"transport\", \"http\").count();\n    double messagesDroppedCount =\n      registry.counter(\"zipkin_collector.messages_dropped\", \"transport\", \"http\").count();\n\n    Response response = get(\"/api/v2/trace/0e8b46e1-81b\");\n    assertThat(response.code()).isEqualTo(400);\n\n    String json = getAsString(\"/metrics\");\n\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.messages.http']\"))\n      .isEqualTo(messagesCount);\n    assertThat(readDouble(json, \"$.['counter.zipkin_collector.messages_dropped.http']\"))\n      .isEqualTo(messagesDroppedCount);\n  }\n\n  /**\n   * Makes sure the prometheus filter doesn't count twice\n   */\n  @Test void writeSpans_updatesPrometheusMetrics() throws Exception {\n    List<Span> spans = List.of(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);\n    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);\n\n    post(\"/api/v2/spans\", body);\n    post(\"/api/v2/spans\", body);\n\n    Thread.sleep(100); // sometimes CI flakes getting the \"http.server.requests\" timer\n    double messagesCount = registry.counter(\"zipkin_collector.spans\", \"transport\", \"http\").count();\n    // Get the http count from the registry and it should match the summation previous count\n    // and count of calls below\n    long httpCount = registry\n      .find(\"http.server.requests\")\n      .tag(\"uri\", \"/api/v2/spans\")\n      .timer()\n      .count();\n\n    // ensure unscoped counter does not exist\n    assertThat(scrape())\n      .doesNotContain(\"zipkin_collector_spans_total \" + messagesCount)\n      .contains(\"zipkin_collector_spans_total{transport=\\\"http\\\",} \" + messagesCount)\n      .contains(\n        \"http_server_requests_seconds_count{method=\\\"POST\\\",status=\\\"202\\\",uri=\\\"/api/v2/spans\\\",} \"\n          + httpCount);\n  }\n\n  String getAsString(String path) throws IOException {\n    Response response = get(path);\n    assertThat(response.isSuccessful())\n      .withFailMessage(response.toString())\n      .isTrue();\n    return response.body().string();\n  }\n\n  Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n\n  Response post(String path, byte[] body) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .post(RequestBody.create(body))\n      .build()).execute();\n  }\n\n  String scrape() throws Exception {\n    Thread.sleep(100);\n    return registry.scrape();\n  }\n\n  static double readDouble(String json, String jsonPath) {\n    return JsonPath.compile(jsonPath).read(json);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.prometheus;\n\nimport com.linecorp.armeria.spring.ArmeriaServerConfigurator;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class ZipkinPrometheusMetricsConfigurationTest {\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  public void refresh() {\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinPrometheusMetricsConfiguration.class\n    );\n    context.refresh();\n  }\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void providesHttpRequestDurationCustomizer() {\n    refresh();\n\n    context.getBeansOfType(ArmeriaServerConfigurator.class);\n  }\n\n  @Test void defaultMetricName() {\n    refresh();\n\n    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration.class).metricName)\n      .isEqualTo(\"http.server.requests\");\n  }\n\n  @Test void overrideMetricName() {\n    TestPropertyValues.of(\"management.metrics.web.server.requests-metric-name:foo\")\n      .applyTo(context);\n    refresh();\n\n    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration.class).metricName)\n      .isEqualTo(\"foo\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/pulsar/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.pulsar;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.collector.pulsar.PulsarCollector;\n\n/** opens package access for testing */\npublic final class Access {\n\n  /** Just registering properties to avoid automatically connecting to a Pulsar server */\n  public static void registerPulsarProperties(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, EnablePulsarCollectorProperties.class);\n  }\n\n  @Configuration\n  @EnableConfigurationProperties(ZipkinPulsarCollectorProperties.class)\n  static class EnablePulsarCollectorProperties {\n  }\n\n  public static PulsarCollector.Builder collectorBuilder(\n      AnnotationConfigApplicationContext context) {\n    return context.getBean(ZipkinPulsarCollectorProperties.class).toBuilder();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/pulsar/ZipkinPulsarCollectorConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.pulsar;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.collector.pulsar.PulsarCollector;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinPulsarCollectorConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesNotProvideCollectorComponent_whenBootstrapServersUnset() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context.register(\n          PropertyPlaceholderAutoConfiguration.class,\n          ZipkinPulsarCollectorConfiguration.class,\n          InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(PulsarCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenServiceUrlEmptyString() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.collector.pulsar.service-url:\").applyTo(context);\n      context.register(\n          PropertyPlaceholderAutoConfiguration.class,\n          ZipkinPulsarCollectorConfiguration.class,\n          InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(PulsarCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenServiceUrlServersSet() {\n    TestPropertyValues.of(\"zipkin.collector.pulsar.service-url:pulsar://localhost:6650\", \n            \"zipkin.collector.pulsar.subscription-name:zipkin-subscriptionName\")\n        .applyTo(context);\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinPulsarCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n    try {\n      context.refresh();\n      failBecauseExceptionWasNotThrown(BeanCreationException.class);\n    } catch (BeanCreationException e) {\n      assertThat(e.getCause()).hasMessageContaining(\n          \"Pulsar Client is unable to subscribe to the topic\");\n    }\n  }\n\n  @Test void doesNotProvidesCollectorComponent_whenServiceUrlSetAndDisabled() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      TestPropertyValues.of(\"zipkin.collector.pulsar.service-url:pulsar://127.0.0.1:6650\")\n          .applyTo(context);\n      TestPropertyValues.of(\"zipkin.collector.pulsar.enabled:false\").applyTo(context);\n      context.register(\n          PropertyPlaceholderAutoConfiguration.class,\n          ZipkinPulsarCollectorConfiguration.class,\n          InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(PulsarCollector.class);\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/pulsar/ZipkinPulsarCollectorPropertiesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.pulsar;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ZipkinPulsarCollectorPropertiesTest {\n  @Test void stringPropertiesConvertEmptyStringsToNull() {\n    final ZipkinPulsarCollectorProperties properties = new ZipkinPulsarCollectorProperties();\n    properties.setServiceUrl(\"pulsar://127.0.0.1:6650\");\n    properties.setTopic(\"zipkin\");\n    properties.setClientProps(new HashMap<>());\n    properties.setConsumerProps(new HashMap<>());\n    assertThat(properties.getServiceUrl()).isEqualTo(\"pulsar://127.0.0.1:6650\");\n    assertThat(properties.getTopic()).isEqualTo(\"zipkin\");\n    assertThat(properties.getClientProps()).isEmpty();\n    assertThat(properties.getConsumerProps()).isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/Access.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.rabbitmq;\n\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.context.annotation.Configuration;\nimport zipkin2.collector.rabbitmq.RabbitMQCollector;\n\n/** opens package access for testing */\npublic final class Access {\n\n  /** Just registering properties to avoid automatically connecting to a Rabbit MQ server */\n  public static void registerRabbitMQProperties(AnnotationConfigApplicationContext context) {\n    context.register(\n        PropertyPlaceholderAutoConfiguration.class, EnableRabbitMQCollectorProperties.class);\n  }\n\n  @Configuration\n  @EnableConfigurationProperties(ZipkinRabbitMQCollectorProperties.class)\n  static class EnableRabbitMQCollectorProperties {}\n\n  public static RabbitMQCollector.Builder collectorBuilder(\n      AnnotationConfigApplicationContext context) throws Exception {\n    return context.getBean(ZipkinRabbitMQCollectorProperties.class).toBuilder();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.rabbitmq;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.collector.rabbitmq.RabbitMQCollector;\nimport zipkin2.server.internal.InMemoryConfiguration;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinRabbitMQCollectorConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesNotProvideCollectorComponent_whenAddressAndUriNotSet() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinRabbitMQCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(RabbitMQCollector.class);\n    });\n  }\n\n  @Test void doesNotProvideCollectorComponent_whenAddressesAndUriIsEmptyString() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context = new AnnotationConfigApplicationContext();\n      TestPropertyValues.of(\n        \"zipkin.collector.rabbitmq.addresses:\",\n        \"zipkin.collector.rabbitmq.uri:\")\n        .applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinRabbitMQCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(RabbitMQCollector.class);\n    });\n  }\n\n  @Test void providesCollectorComponent_whenAddressesSet() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\"zipkin.collector.rabbitmq.addresses:localhost:1234\").applyTo(context);\n    context.register(\n      PropertyPlaceholderAutoConfiguration.class,\n      ZipkinRabbitMQCollectorConfiguration.class,\n      InMemoryConfiguration.class);\n\n    try {\n      context.refresh();\n      failBecauseExceptionWasNotThrown(BeanCreationException.class);\n    } catch (BeanCreationException e) {\n      assertThat(e.getCause()).hasMessageContaining(\n        \"Unable to establish connection to RabbitMQ server: Connection refused\");\n    }\n  }\n\n  @Test void doesNotProvidesCollectorComponent_whenAddressesSetAndDisabled() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context = new AnnotationConfigApplicationContext();\n      TestPropertyValues.of(\"zipkin.collector.rabbitmq.addresses:localhost:1234\").applyTo(context);\n      TestPropertyValues.of(\"zipkin.collector.rabbitmq.enabled:false\").applyTo(context);\n      context.register(\n        PropertyPlaceholderAutoConfiguration.class,\n        ZipkinRabbitMQCollectorConfiguration.class,\n        InMemoryConfiguration.class);\n      context.refresh();\n      context.getBean(RabbitMQCollector.class);\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.rabbitmq;\n\nimport com.rabbitmq.client.ConnectionFactory;\nimport java.net.URI;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ZipkinRabbitMQCollectorPropertiesTest {\n  ZipkinRabbitMQCollectorProperties properties = new ZipkinRabbitMQCollectorProperties();\n\n  @Test void uriProperlyParsedAndIgnoresOtherProperties_whenUriSet() throws Exception {\n    properties.setUri(URI.create(\"amqp://admin:admin@localhost:5678/myv\"));\n    properties.setAddresses(List.of(\"will_not^work!\"));\n    properties.setUsername(\"bob\");\n    properties.setPassword(\"letmein\");\n    properties.setVirtualHost(\"drwho\");\n\n    assertThat(properties.toBuilder())\n      .extracting(\"connectionFactory\")\n      .satisfies(object -> {\n        ConnectionFactory connFactory = (ConnectionFactory) object;\n        assertThat(connFactory.getHost()).isEqualTo(\"localhost\");\n        assertThat(connFactory.getPort()).isEqualTo(5678);\n        assertThat(connFactory.getUsername()).isEqualTo(\"admin\");\n        assertThat(connFactory.getPassword()).isEqualTo(\"admin\");\n        assertThat(connFactory.getVirtualHost()).isEqualTo(\"myv\");\n      });\n  }\n\n  /** This prevents an empty RABBIT_URI variable from being mistaken as a real one */\n  @Test void ignoresEmptyURI() {\n    properties.setUri(URI.create(\"\"));\n\n    assertThat(properties.getUri()).isNull();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/throttle/FakeCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport java.util.concurrent.RejectedExecutionException;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nclass FakeCall extends Call<Void> {\n  boolean overCapacity = false;\n\n  void setOverCapacity(boolean isOverCapacity) {\n    this.overCapacity = isOverCapacity;\n  }\n\n  @Override public Void execute() {\n    if (overCapacity) throw new RejectedExecutionException();\n    return null;\n  }\n\n  @Override public void enqueue(Callback<Void> callback) {\n    if (overCapacity) {\n      callback.onError(new RejectedExecutionException());\n    } else {\n      callback.onSuccess(null);\n    }\n  }\n\n  @Override public void cancel() {\n  }\n\n  @Override public boolean isCanceled() {\n    return false;\n  }\n\n  @Override public Call<Void> clone() {\n    return null;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/throttle/ThrottledCallTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport com.linecorp.armeria.common.metric.NoopMeterRegistry;\nimport com.netflix.concurrency.limits.Limiter;\nimport com.netflix.concurrency.limits.Limiter.Listener;\nimport com.netflix.concurrency.limits.limit.SettableLimit;\nimport com.netflix.concurrency.limits.limiter.SimpleLimiter;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.Optional;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Predicate;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nimport static com.linecorp.armeria.common.util.Exceptions.clearTrace;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static zipkin2.server.internal.throttle.ThrottledCall.NOOP_CALLBACK;\nimport static zipkin2.server.internal.throttle.ThrottledCall.STORAGE_THROTTLE_MAX_CONCURRENCY;\nimport static zipkin2.server.internal.throttle.ThrottledStorageComponent.STORAGE_THROTTLE_MAX_QUEUE_SIZE;\n\nclass ThrottledCallTest {\n  SettableLimit limit = SettableLimit.startingAt(0);\n  SimpleLimiter limiter = SimpleLimiter.newBuilder().limit(limit).build();\n  LimiterMetrics limiterMetrics = new LimiterMetrics(NoopMeterRegistry.get());\n  Predicate<Throwable> isOverCapacity = RejectedExecutionException.class::isInstance;\n\n  int numThreads = 1;\n  ExecutorService executor = Executors.newSingleThreadExecutor();\n\n  @AfterEach void shutdownExecutor() {\n    executor.shutdown();\n  }\n\n  @Test void niceToString() {\n    Call<Void> delegate = mock(Call.class);\n    when(delegate.toString()).thenReturn(\"StoreSpansCall{}\");\n\n    assertThat(new ThrottledCall(delegate, executor, limiter, limiterMetrics, isOverCapacity))\n      .hasToString(\"Throttled(StoreSpansCall{})\");\n  }\n\n  @Test void execute_isThrottled() throws Exception {\n    int queueSize = 1;\n    int totalTasks = numThreads + queueSize;\n    limit.setLimit(totalTasks);\n\n    Semaphore startLock = new Semaphore(numThreads);\n    Semaphore waitLock = new Semaphore(totalTasks);\n    Semaphore failLock = new Semaphore(1);\n    ThrottledCall throttled = throttle(new LockedCall(startLock, waitLock));\n\n    // Step 1: drain appropriate locks\n    startLock.drainPermits();\n    waitLock.drainPermits();\n    failLock.drainPermits();\n\n    // Step 2: saturate threads and fill queue\n    ExecutorService backgroundPool = Executors.newCachedThreadPool();\n    for (int i = 0; i < totalTasks; i++) {\n      backgroundPool.submit(() -> throttled.clone().execute());\n    }\n\n    try {\n      // Step 3: make sure the threads actually started\n      startLock.acquire(numThreads);\n\n      // Step 4: submit something beyond our limits\n      Future<?> future = backgroundPool.submit(() -> {\n        try {\n          throttled.execute();\n        } catch (IOException e) {\n          throw new UncheckedIOException(e);\n        } finally {\n          // Step 6: signal that we tripped the limit\n          failLock.release();\n        }\n      });\n\n      // Step 5: wait to make sure our limit actually tripped\n      failLock.acquire();\n\n      future.get();\n\n      // Step 7: Expect great things\n      failBecauseExceptionWasNotThrown(ExecutionException.class);\n    } catch (ExecutionException t) {\n      assertThat(t)\n        .isInstanceOf(ExecutionException.class) // from future.get\n        .hasCauseInstanceOf(RejectedExecutionException.class);\n    } finally {\n      waitLock.release(totalTasks);\n      startLock.release(totalTasks);\n      backgroundPool.shutdownNow();\n    }\n  }\n\n  @Test void execute_throttlesBack_whenStorageRejects() throws Exception {\n    Listener listener = mock(Listener.class);\n    FakeCall call = new FakeCall();\n    call.overCapacity = true;\n\n    ThrottledCall throttle =\n      new ThrottledCall(call, executor, mockLimiter(listener), limiterMetrics, isOverCapacity);\n\n    try {\n      throttle.execute();\n      assertThat(true).isFalse(); // should raise a RejectedExecutionException\n    } catch (RejectedExecutionException e) {\n      verify(listener).onDropped();\n    }\n  }\n\n  @Test void execute_ignoresLimit_whenPoolFull() throws Exception {\n    Listener listener = mock(Listener.class);\n\n    ThrottledCall throttle = new ThrottledCall(new FakeCall(), mockExhaustedPool(),\n      mockLimiter(listener), limiterMetrics, isOverCapacity);\n\n    try {\n      throttle.execute();\n      assertThat(true).isFalse(); // should raise a RejectedExecutionException\n    } catch (RejectedExecutionException e) {\n      verify(listener).onIgnore();\n    }\n  }\n\n  @Test void enqueue_isThrottled() throws Exception {\n    int queueSize = 1;\n    int totalTasks = numThreads + queueSize;\n    limit.setLimit(totalTasks);\n\n    Semaphore startLock = new Semaphore(numThreads);\n    Semaphore waitLock = new Semaphore(totalTasks);\n    ThrottledCall throttle = throttle(new LockedCall(startLock, waitLock));\n\n    // Step 1: drain appropriate locks\n    startLock.drainPermits();\n    waitLock.drainPermits();\n\n    // Step 2: saturate threads and fill queue\n    Callback<Void> callback = mock(Callback.class);\n    for (int i = 0; i < totalTasks; i++) {\n      throttle.clone().enqueue(callback);\n    }\n\n    // Step 3: make sure the threads actually started\n    startLock.acquire(numThreads);\n\n    // Step 4: submit something beyond our limits and make sure it fails\n    assertThatThrownBy(() -> throttle.clone().enqueue(callback))\n      .isEqualTo(STORAGE_THROTTLE_MAX_CONCURRENCY);\n  }\n\n  @Test void enqueue_throttlesBack_whenStorageRejects() throws Exception {\n    Listener listener = mock(Listener.class);\n    FakeCall call = new FakeCall();\n    call.overCapacity = true;\n\n    ThrottledCall throttle =\n      new ThrottledCall(call, executor, mockLimiter(listener), limiterMetrics, isOverCapacity);\n\n    final CountDownLatch countDown = new CountDownLatch(1);\n    final AtomicReference<Throwable> throwable = new AtomicReference<>();\n    throttle.enqueue(new Callback<>() {\n      @Override public void onSuccess(Void value) {\n        countDown.countDown();\n      }\n\n      @Override public void onError(Throwable t) {\n        throwable.set(t);\n        countDown.countDown();\n      }\n    });\n\n    countDown.await();\n\n    assertThat(throwable).hasValue(OVER_CAPACITY);\n\n    verify(listener).onDropped();\n  }\n\n  @Test void enqueue_ignoresLimit_whenPoolFull() {\n    Listener listener = mock(Listener.class);\n\n    ThrottledCall throttle = new ThrottledCall(new FakeCall(), mockExhaustedPool(),\n      mockLimiter(listener), limiterMetrics, isOverCapacity);\n\n    assertThatThrownBy(() -> throttle.enqueue(NOOP_CALLBACK))\n      .isEqualTo(STORAGE_THROTTLE_MAX_QUEUE_SIZE);\n\n    verify(listener).onIgnore();\n  }\n\n  ThrottledCall throttle(Call<Void> delegate) {\n    return new ThrottledCall(delegate, executor, limiter, limiterMetrics, isOverCapacity);\n  }\n\n  static final class LockedCall extends Call.Base<Void> {\n    final Semaphore startLock, waitLock;\n\n    LockedCall(Semaphore startLock, Semaphore waitLock) {\n      this.startLock = startLock;\n      this.waitLock = waitLock;\n    }\n\n    @Override public Void doExecute() {\n      try {\n        startLock.release();\n        waitLock.acquire();\n        return null;\n      } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n        throw new AssertionError(e);\n      }\n    }\n\n    @Override public void doEnqueue(Callback<Void> callback) {\n      try {\n        callback.onSuccess(doExecute());\n      } catch (Throwable t) {\n        propagateIfFatal(t);\n        callback.onError(t);\n      }\n    }\n\n    @Override public LockedCall clone() {\n      return new LockedCall(startLock, waitLock);\n    }\n  }\n\n  ExecutorService mockExhaustedPool() {\n    ExecutorService mock = mock(ExecutorService.class);\n    doThrow(STORAGE_THROTTLE_MAX_QUEUE_SIZE).when(mock).execute(any());\n    doThrow(STORAGE_THROTTLE_MAX_QUEUE_SIZE).when(mock).submit(any(Callable.class));\n    return mock;\n  }\n\n  Limiter<Void> mockLimiter(Listener listener) {\n    Limiter<Void> mock = mock(Limiter.class);\n    when(mock.acquire(any())).thenReturn(Optional.of(listener));\n    return mock;\n  }\n\n  static final Exception OVER_CAPACITY = clearTrace(new RejectedExecutionException(\"overCapacity\"));\n\n  static final class FakeCall extends Call.Base<Void> {\n    boolean overCapacity = false;\n\n    @Override public Void doExecute() {\n      throw new AssertionError(\"throttling never uses execute\");\n    }\n\n    @Override public void doEnqueue(Callback<Void> callback) {\n      if (overCapacity) {\n        callback.onError(OVER_CAPACITY);\n      } else {\n        callback.onSuccess(null);\n      }\n    }\n\n    @Override public FakeCall clone() {\n      return new FakeCall();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/throttle/ThrottledStorageComponentTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.throttle;\n\nimport brave.Tracing;\nimport com.linecorp.armeria.common.metric.NoopMeterRegistry;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Component;\nimport zipkin2.internal.Nullable;\nimport zipkin2.server.internal.throttle.ThrottledStorageComponent.ThrottledSpanConsumer;\nimport zipkin2.storage.InMemoryStorage;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nclass ThrottledStorageComponentTest {\n  InMemoryStorage delegate = InMemoryStorage.newBuilder().build();\n  @Nullable Tracing tracing;\n  NoopMeterRegistry registry = NoopMeterRegistry.get();\n\n  @Test void spanConsumer_isProxied() {\n    ThrottledStorageComponent throttle =\n      new ThrottledStorageComponent(delegate, registry, tracing, 1, 2, 1);\n\n    assertThat(ThrottledSpanConsumer.class)\n      .isSameAs(throttle.spanConsumer().getClass());\n  }\n\n  @Test void createComponent_withZeroSizedQueue() {\n    int queueSize = 0;\n    new ThrottledStorageComponent(delegate, registry, tracing, 1, 2, queueSize);\n    // no exception == pass\n  }\n\n  @Test void createComponent_withNegativeQueue() {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      int queueSize = -1;\n      new ThrottledStorageComponent(delegate, registry, tracing, 1, 2, queueSize);\n    });\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    assertThat(new ThrottledStorageComponent(delegate, registry, tracing, 1, 2, 1))\n      .hasToString(\"Throttled{InMemoryStorage{}}\");\n  }\n\n  @Test void delegatesCheck() {\n    StorageComponent mock = mock(StorageComponent.class);\n\n    new ThrottledStorageComponent(mock, registry, tracing, 1, 2, 1).check();\n    verify(mock, times(1)).check();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.ui;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.server.Server;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport zipkin.server.ZipkinServer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.server.internal.ITZipkinServer.stringFromClasspath;\nimport static zipkin2.server.internal.ITZipkinServer.url;\n\n@SpringBootTest(\n  classes = ZipkinServer.class,\n  webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web\n  properties = {\n    \"server.port=0\",\n    \"spring.config.name=zipkin-server\",\n    \"zipkin.ui.base-path=/admin/zipkin\",\n    \"server.compression.enabled=true\",\n    \"server.compression.min-response-size=128\"\n  })\nclass ITZipkinUiConfiguration {\n  @Autowired Server server;\n  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();\n\n  @Test void configJson() throws Exception {\n    assertThat(get(\"/zipkin/config.json\").body().string()).isEqualTo(\"\"\"\n      {\n        \"environment\" : \"\",\n        \"queryLimit\" : 10,\n        \"defaultLookback\" : 900000,\n        \"searchEnabled\" : true,\n        \"logsUrl\" : null,\n        \"supportUrl\" : null,\n        \"archivePostUrl\" : null,\n        \"archiveUrl\" : null,\n        \"dependency\" : {\n          \"enabled\" : true,\n          \"lowErrorRate\" : 0.5,\n          \"highErrorRate\" : 0.75\n        }\n      }\"\"\"\n    );\n  }\n\n  /** The zipkin-lens is a single-page app. This prevents reloading all resources on each click. */\n  @Test void setsMaxAgeOnUiResources() throws Exception {\n    assertThat(get(\"/zipkin/config.json\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=600\");\n    assertThat(get(\"/zipkin/index.html\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=60\");\n    assertThat(get(\"/zipkin/test.txt\").header(\"Cache-Control\"))\n      .isEqualTo(\"max-age=31536000\");\n  }\n\n  @Test void redirectsIndex() throws Exception {\n    String index = get(\"/zipkin/index.html\").body().string();\n\n    client = new OkHttpClient.Builder().followRedirects(true).build();\n\n    List.of(\"/zipkin\", \"/\").forEach(path -> {\n      try {\n        assertThat(get(path).body().string()).isEqualTo(index);\n      } catch (IOException e) {\n        throw new UncheckedIOException(e);\n      }\n    });\n  }\n\n  /** Browsers honor conditional requests such as eTag. Let's make sure the server does */\n  @Test void conditionalRequests() {\n    List.of(\"/zipkin/config.json\", \"/zipkin/index.html\", \"/zipkin/test.txt\").forEach(path -> {\n      try {\n        String etag = get(path).header(\"etag\");\n        assertThat(conditionalGet(path, etag).code())\n          .isEqualTo(304);\n        assertThat(conditionalGet(path, \"aargh\").code())\n          .isEqualTo(200);\n      } catch (IOException e) {\n        throw new UncheckedIOException(e);\n      }\n    });\n  }\n\n  /** Some assets are pretty big. ensure they use compression. */\n  @Test void supportsCompression() {\n    assertThat(getContentEncodingFromRequestThatAcceptsGzip(\"/zipkin/test.txt\"))\n      .isNull(); // too small to compress\n    assertThat(getContentEncodingFromRequestThatAcceptsGzip(\"/zipkin/config.json\"))\n      .isEqualTo(\"gzip\");\n  }\n\n  /**\n   * The test sets the property {@code zipkin.ui.base-path=/foozipkin}, which should reflect in\n   * index.html\n   */\n  @Test void replacesBaseTag() throws Exception {\n    assertThat(get(\"/zipkin/index.html\").body().string()).isEqualTo(\"\"\"\n      <!-- simplified version of /zipkin-lens/index.html -->\n      <html>\n        <head>\n          <base href=\"/admin/zipkin/\">\n          <link rel=\"icon\" href=\"./favicon.ico\">\n          <script type=\"module\" crossorigin=\"\" src=\"./static/js/index.js\"></script>\n          <link rel=\"stylesheet\" href=\"./static/css/index.css\">\n        </head>\n        <body>zipkin-lens</body>\n      </html>\n      \"\"\"\n    );\n  }\n\n  /** index.html is served separately. This tests other content is also loaded from the classpath. */\n  @Test void servesOtherContentFromClasspath() throws Exception {\n    assertThat(get(\"/zipkin/test.txt\").body().string())\n      .isEqualToIgnoringWhitespace(stringFromClasspath(getClass(), \"zipkin-lens/test.txt\"));\n  }\n\n  private Response get(String path) throws IOException {\n    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();\n  }\n\n  private Response conditionalGet(String path, String etag) throws IOException {\n    return client.newCall(new Request.Builder()\n      .url(url(server, path))\n      .header(\"If-None-Match\", etag)\n      .build()).execute();\n  }\n\n  private String getContentEncodingFromRequestThatAcceptsGzip(String path) {\n    // We typically use OkHttp in our tests, but that automatically unzips..\n    AggregatedHttpResponse response = WebClient.of(url(server, \"/\"))\n      .execute(RequestHeaders.of(HttpMethod.GET, path, HttpHeaderNames.ACCEPT_ENCODING, \"gzip\"))\n      .aggregate().join();\n\n    return response.headers().get(HttpHeaderNames.CONTENT_ENCODING);\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.server.internal.ui;\n\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport io.netty.handler.codec.http.cookie.ClientCookieEncoder;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.DefaultCookie;\nimport java.io.ByteArrayInputStream;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.BeanCreationException;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.core.io.ClassPathResource;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinUiConfigurationTest {\n\n  AnnotationConfigApplicationContext context;\n\n  @AfterEach void close() {\n    if (context != null) {\n      context.close();\n    }\n  }\n\n  @Test void indexContentType() {\n    context = createContext();\n    assertThat(\n      serveIndex().headers().contentType())\n      .isEqualTo(MediaType.HTML_UTF_8);\n  }\n\n  @Test void indexHtml() {\n    // Instantiate directly so that spring doesn't cache it\n    ZipkinUiConfiguration ui = new ZipkinUiConfiguration();\n    ui.ui = new ZipkinUiProperties();\n    ui.lensIndexHtml = new ClassPathResource(\"does-not-exist.html\");\n    assertThatExceptionOfType(BeanCreationException.class).isThrownBy(ui::indexService);\n  }\n\n  @Test void canOverridesProperty_defaultLookback() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.defaultLookback:100\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getDefaultLookback())\n      .isEqualTo(100);\n  }\n\n  @Test void canOverrideProperty_logsUrl() {\n    final String url = \"http://mycompany.com/kibana\";\n    context = createContextWithOverridenProperty(\"zipkin.ui.logs-url:\" + url);\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isEqualTo(url);\n  }\n\n  @Test void canOverrideProperty_archivePostUrl() {\n    final String url = \"http://zipkin.archive.com/api/v2/spans\";\n    context = createContextWithOverridenProperty(\"zipkin.ui.archive-post-url:\" + url);\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getArchivePostUrl()).isEqualTo(url);\n  }\n\n  @Test void canOverrideProperty_archiveUrl() {\n    final String url = \"http://zipkin.archive.com/zipkin/traces/{traceId}\";\n    context = createContextWithOverridenProperty(\"zipkin.ui.archive-url:\" + url);\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getArchiveUrl()).isEqualTo(url);\n  }\n\n  @Test void canOverrideProperty_supportUrl() {\n    final String url = \"http://mycompany.com/file-a-bug\";\n    context = createContextWithOverridenProperty(\"zipkin.ui.support-url:\" + url);\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getSupportUrl()).isEqualTo(url);\n  }\n\n  @Test void logsUrlIsNullIfOverridenByEmpty() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.logs-url:\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isNull();\n  }\n\n  @Test void logsUrlIsNullByDefault() {\n    context = createContext();\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isNull();\n  }\n\n  @Test void canOverridesProperty_disable() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context = createContextWithOverridenProperty(\"zipkin.ui.enabled:false\");\n\n      context.getBean(ZipkinUiProperties.class);\n    });\n  }\n\n  @Test void canOverridesProperty_searchEnabled() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.search-enabled:false\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).isSearchEnabled()).isFalse();\n  }\n\n  @Test void canOverridesProperty_dependenciesEnabled() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.dependency.enabled:false\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getDependency().isEnabled()).isFalse();\n  }\n\n  @Test void canOverrideProperty_dependencyLowErrorRate() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.dependency.low-error-rate:0.1\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getDependency().getLowErrorRate())\n      .isEqualTo(0.1f);\n  }\n\n  @Test void canOverrideProperty_dependencyHighErrorRate() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.dependency.high-error-rate:0.1\");\n\n    assertThat(context.getBean(ZipkinUiProperties.class).getDependency().getHighErrorRate())\n      .isEqualTo(0.1f);\n  }\n\n  @Test void defaultBaseUrl_doesNotChangeResource() {\n    context = createContext();\n\n    assertThat(new ByteArrayInputStream(serveIndex().content().array()))\n      .hasSameContentAs(getClass().getResourceAsStream(\"/zipkin-lens/index.html\"));\n  }\n\n  @Test void canOverrideProperty_basePath() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.basepath:/admin/zipkin\");\n\n    assertThat(serveIndex().contentUtf8()).isEqualTo(\"\"\"\n      <!-- simplified version of /zipkin-lens/index.html -->\n      <html>\n        <head>\n          <base href=\"/admin/zipkin/\">\n          <link rel=\"icon\" href=\"./favicon.ico\">\n          <script type=\"module\" crossorigin=\"\" src=\"./static/js/index.js\"></script>\n          <link rel=\"stylesheet\" href=\"./static/css/index.css\">\n        </head>\n        <body>zipkin-lens</body>\n      </html>\n      \"\"\"\n    );\n  }\n\n  @Test void lensCookieOverridesIndex() {\n    context = createContext();\n\n    assertThat(serveIndex(new DefaultCookie(\"lens\", \"true\")).contentUtf8())\n      .contains(\"zipkin-lens\");\n  }\n\n  @Test void canOverrideProperty_root() {\n    context = createContextWithOverridenProperty(\"zipkin.ui.basepath:/\");\n\n    assertThat(serveIndex().contentUtf8()).isEqualTo(\"\"\"\n      <!-- simplified version of /zipkin-lens/index.html -->\n      <html>\n        <head>\n          <base href=\"/\">\n          <link rel=\"icon\" href=\"./favicon.ico\">\n          <script type=\"module\" crossorigin=\"\" src=\"./static/js/index.js\"></script>\n          <link rel=\"stylesheet\" href=\"./static/css/index.css\">\n        </head>\n        <body>zipkin-lens</body>\n      </html>\n      \"\"\"\n    );\n  }\n\n  AggregatedHttpResponse serveIndex(Cookie... cookies) {\n    RequestHeaders headers = RequestHeaders.of(HttpMethod.GET, \"/\");\n    String encodedCookies = ClientCookieEncoder.LAX.encode(cookies);\n    if (encodedCookies != null) {\n      headers = headers.toBuilder().set(HttpHeaderNames.COOKIE, encodedCookies).build();\n    }\n    HttpRequest req = HttpRequest.of(headers);\n    try {\n      return context.getBean(HttpService.class)\n        .serve(ServiceRequestContext.of(req), req).aggregate()\n        .get();\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  private static AnnotationConfigApplicationContext createContext() {\n    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n    context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiConfiguration.class);\n    context.refresh();\n    return context;\n  }\n\n  private static AnnotationConfigApplicationContext createContextWithOverridenProperty(\n    String pair) {\n    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(pair).applyTo(context);\n    context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiConfiguration.class);\n    context.refresh();\n    return context;\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.cassandra3.Access;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass ZipkinCassandraStorageAutoConfigurationTest {\n\n  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();\n\n  @AfterEach void close() {\n    context.close();\n  }\n\n  @Test void doesntProvidesStorageComponent_whenStorageTypeNotCassandra() {\n    TestPropertyValues.of(\"zipkin.storage.type:elasticsearch\").applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThatThrownBy(() -> context.getBean(CassandraStorage.class))\n      .isInstanceOf(NoSuchBeanDefinitionException.class);\n  }\n\n  @Test void providesStorageComponent_whenStorageTypeCassandra() {\n    TestPropertyValues.of(\"zipkin.storage.type:cassandra3\").applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class)).isNotNull();\n  }\n\n  @Test void canOverridesProperty_contactPoints() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.cassandra3.contact-points:host1,host2\" // note snake-case supported\n    ).applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).contactPoints).isEqualTo(\"host1,host2\");\n  }\n\n  @Test void strictTraceId_defaultsToTrue() {\n    TestPropertyValues.of(\"zipkin.storage.type:cassandra3\").applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).strictTraceId).isTrue();\n  }\n\n  @Test void strictTraceId_canSetToFalse() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.strict-trace-id:false\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).strictTraceId).isFalse();\n  }\n\n  @Test void searchEnabled_canSetToFalse() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.search-enabled:false\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).searchEnabled).isFalse();\n  }\n\n  @Test void autocompleteKeys_list() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.autocomplete-keys:environment\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).autocompleteKeys)\n      .containsOnly(\"environment\");\n  }\n\n  @Test void autocompleteTtl() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.autocomplete-ttl:60000\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).autocompleteTtl)\n      .isEqualTo(60000);\n  }\n\n  @Test void autocompleteCardinality() {\n    TestPropertyValues.of(\n      \"zipkin.storage.type:cassandra3\",\n      \"zipkin.storage.autocomplete-cardinality:5000\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).autocompleteCardinality)\n      .isEqualTo(5000);\n  }\n\n  @Test void useSsl() {\n    TestPropertyValues.of(\n        \"zipkin.storage.type:cassandra3\",\n        \"zipkin.storage.cassandra3.use-ssl:true\")\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).useSsl).isTrue();\n    assertThat(context.getBean(CassandraStorage.class).sslHostnameValidation).isTrue();\n  }\n\n  @Test void sslHostnameValidation_canSetToFalse() {\n    TestPropertyValues.of(\n        \"zipkin.storage.type:cassandra3\",\n        \"zipkin.storage.cassandra3.use-ssl:true\",\n        \"zipkin.storage.cassandra3.ssl-hostname-validation:false\"\n      )\n      .applyTo(context);\n    Access.registerCassandra3(context);\n    context.refresh();\n\n    assertThat(context.getBean(CassandraStorage.class).useSsl).isTrue();\n    assertThat(context.getBean(CassandraStorage.class).sslHostnameValidation).isFalse();\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/java/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.test.util.TestPropertyValues;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport zipkin2.server.internal.mysql.Access;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\n\nclass ZipkinMySQLStorageConfigurationTest {\n\n  AnnotationConfigApplicationContext context;\n\n  @AfterEach void close() {\n    if (context != null) {\n      context.close();\n    }\n  }\n\n  @Test void doesntProvidesStorageComponent_whenStorageTypeNotMySQL() {\n    assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> {\n      context = new AnnotationConfigApplicationContext();\n      TestPropertyValues.of(\"zipkin.storage.type:cassandra\").applyTo(context);\n      Access.registerMySQL(context);\n      context.refresh();\n      context.getBean(MySQLStorage.class);\n    });\n  }\n\n  @Test void providesStorageComponent_whenStorageTypeMySQL() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\"zipkin.storage.type:mysql\").applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(MySQLStorage.class)).isNotNull();\n  }\n\n  @Test void canOverridesProperty_username() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n        \"zipkin.storage.type:mysql\",\n        \"zipkin.storage.mysql.username:robot\")\n    .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(HikariDataSource.class).getUsername()).isEqualTo(\"robot\");\n  }\n\n  @Test void strictTraceId_defaultsToTrue() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\"zipkin.storage.type:mysql\").applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n    assertThat(context.getBean(MySQLStorage.class).strictTraceId).isTrue();\n  }\n\n  @Test void strictTraceId_canSetToFalse() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n        \"zipkin.storage.type:mysql\",\n        \"zipkin.storage.strict-trace-id:false\")\n      .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(MySQLStorage.class).strictTraceId).isFalse();\n  }\n\n  @Test void searchEnabled_canSetToFalse() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n      \"zipkin.storage.type:mysql\",\n      \"zipkin.storage.search-enabled:false\")\n      .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(MySQLStorage.class).searchEnabled).isFalse();\n  }\n\n  @Test void autocompleteKeys_list() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n      \"zipkin.storage.type:mysql\",\n      \"zipkin.storage.autocomplete-keys:environment\")\n      .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(MySQLStorage.class).autocompleteKeys)\n      .containsOnly(\"environment\");\n  }\n\n  @Test void usesJdbcUrl_whenPresent() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n        \"zipkin.storage.type:mysql\",\n        \"\"\"\n      zipkin.storage.mysql\\\n      .jdbc-url:jdbc:mariadb://host1,host2,host3/zipkin\\\n      \"\"\")\n    .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(HikariDataSource.class).getJdbcUrl())\n      .isEqualTo(\"jdbc:mariadb://host1,host2,host3/zipkin\");\n  }\n\n  @Test void usesRegularConfig_whenBlank() {\n    context = new AnnotationConfigApplicationContext();\n    TestPropertyValues.of(\n        \"zipkin.storage.type:mysql\",\n        \"zipkin.storage.mysql.jdbc-url:\",\n        \"zipkin.storage.mysql.host:host\",\n        \"zipkin.storage.mysql.port:3306\",\n        \"zipkin.storage.mysql.username:root\",\n        \"zipkin.storage.mysql.password:secret\",\n        \"zipkin.storage.mysql.db:zipkin\")\n      .applyTo(context);\n    Access.registerMySQL(context);\n    context.refresh();\n\n    assertThat(context.getBean(HikariDataSource.class).getJdbcUrl())\n      .isEqualTo(\"jdbc:mariadb://host:3306/zipkin?autoReconnect=true&useSSL=false&useUnicode=yes&characterEncoding=UTF-8\");\n  }\n}\n"
  },
  {
    "path": "zipkin-server/src/test/resources/application.yml",
    "content": "spring:\n  main:\n    web-application-type: none\n\n# We are using Armeria instead of Tomcat. Have it inherit the default configuration from Spring\narmeria:\n  ports:\n    - port: ${server.port}\n      protocols:\n        - http\n"
  },
  {
    "path": "zipkin-server/src/test/resources/banner.txt",
    "content": ""
  },
  {
    "path": "zipkin-server/src/test/resources/es-credentials",
    "content": "zipkin.storage.elasticsearch.username=foo\nzipkin.storage.elasticsearch.password=bar\n"
  },
  {
    "path": "zipkin-server/src/test/resources/es-credentials-invalid",
    "content": "zipkin.storage.elasticsearch.username\nbar\n"
  },
  {
    "path": "zipkin-server/src/test/resources/log4j2.properties",
    "content": "# Maven configuration conflicts on simplelogger vs Log4J2, but IntelliJ unit tests use Log4J2\nappenders=console\nappender.console.type=Console\nappender.console.name=STDOUT\nappender.console.layout.type=PatternLayout\nappender.console.layout.pattern=%d{ABSOLUTE} %-5p [%t] %C{2} (%F:%L) - %m%n\nrootLogger.level=warn\nrootLogger.appenderRefs=stdout\nrootLogger.appenderRef.stdout.ref=STDOUT\n\n# uncomment to see outbound client connections (useful in Elasticsearch troubleshooting)\n#logger.client.name=com.linecorp.armeria.client\n#logger.client.level=info\n\n# example of enabling logging for a unit test\n#logger.healthchecktest.name=zipkin2.server.internal.elasticsearch.ITElasticsearchHealthCheck\n#logger.healthchecktest.level=info\n"
  },
  {
    "path": "zipkin-server/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# uncomment to include kafka consumer configuration in test logs\n#logger.org.apache.kafka.clients.level=info\n"
  },
  {
    "path": "zipkin-server/src/test/resources/zipkin-lens/index.html",
    "content": "<!-- simplified version of /zipkin-lens/index.html -->\n<html>\n  <head>\n    <base href=\"/zipkin/\">\n    <link rel=\"icon\" href=\"/zipkin/favicon.ico\">\n    <script type=\"module\" crossorigin=\"\" src=\"/zipkin/static/js/index.js\"></script>\n    <link rel=\"stylesheet\" href=\"/zipkin/static/css/index.css\">\n  </head>\n  <body>zipkin-lens</body>\n</html>\n"
  },
  {
    "path": "zipkin-server/src/test/resources/zipkin-lens/test.txt",
    "content": "hello world\n"
  },
  {
    "path": "zipkin-storage/README.md",
    "content": "# zipkin-storage\n\nModules here implement popular storage options available by default in\nthe [server build](../zipkin-server).\n\nPlease note all modules here require JRE 17+ even if `InMemoryStorage`\nwill run on JRE 8+.\n\nThese libraries are also usable outside the server, for example in\ncustom collectors or storage pipelines. While compatibility guarantees\nare strong, choices may be dropped over time.\n\nStorage modules ending in `-v1` are discouraged for new sites as they\nuse an older data model. At some point in the future, we will stop\npublishing v1 storage options.\n"
  },
  {
    "path": "zipkin-storage/cassandra/RATIONALE.md",
    "content": "# zipkin-storage-cassandra rationale\n\n## Why do we use prepared statements?\n\nWe use prepared statements (instead of simple statements) for anything executed more than once.\nThis reduces load on the server, as the CQL query does not have to parsed server-side again and\nagain.\n\nThis applies even for health checks and querying for service names, which have only constant\nparameters and do not select partition keys.\n\nWhen partition keys are in use, ex `SELECT * FROM span WHERE trace_id = ?`, prepared statements\noffer a second advantage in that you get automatic token-aware routing.\n\nThe above was distilled from https://groups.google.com/a/lists.datastax.com/d/msg/java-driver-user/d6wLkH3xDLI/jUWOokKVAgAJ\n"
  },
  {
    "path": "zipkin-storage/cassandra/README.md",
    "content": "# zipkin-storage-cassandra\n\nThis is a CQL-based Cassandra storage component, built upon the [Zipkin v2 api and model](https://zipkin.io/zipkin-api/#/default/post_spans).\nThis uses Cassandra 3.11.3+ features, but is tested against the latest patch of Cassandra 3.11.\n\n`CassandraSpanStore.getDependencies()` returns pre-aggregated dependency links (ex via [zipkin-dependencies](https://github.com/openzipkin/zipkin-dependencies)).\n\nThe implementation uses the [Apache Cassandra Java Driver 4.x](https://github.com/apache/cassandra-java-driver).\n\n`zipkin2.storage.cassandra.CassandraStorage.Builder` includes defaults that will operate against a local Cassandra installation.\n\n## Logging\nSince the underlying driver uses SLF4J, Zipkin's storage layer also uses\nthis (note SLF4J is supported out-of-the-box with no configuration in\nzipkin-server).\n\nZipkin's storage layer logs to the category \"zipkin2.storage.cassandra\",\nbut you may wish to see the entire \"zipkin2\" when troubleshooting.\n\nIf you want to see requests and latency, set the logging category\n\"com.datastax.oss.driver.internal.core.tracker.RequestLogger\" to DEBUG.\nTRACE includes query values.\n\nSee [Request Logger](https://github.com/apache/cassandra-java-driver/tree/4.x/manual/core/request_tracker#request-logger) for more details.\n\n## Testing\nThis module conditionally runs integration tests against a local Cassandra instance.\n\nThis starts a docker container or attempts to re-use an existing cassandra node running on localhost.\n\nIf you run tests via Maven or otherwise when Cassandra is not running,\nyou'll notice tests are silently skipped.\n```\nResults :\n\nTests run: 62, Failures: 0, Errors: 0, Skipped: 48\n```\n\nThis behaviour is intentional: We don't want to burden developers with\ninstalling and running all storage options to test unrelated change.\nThat said, all integration tests run on pull request.\n\n### Running a single test\n\nTo run a single integration test, use the following syntax:\n\n```bash\n$ ./mvnw -Dit.test='ITCassandraStorage$ITSpanStore#getTraces_duration' -pl zipkin-storage/cassandra clean verify\n```\n\n## Strict trace ID\nBy default, trace identifiers are written at the length received to indexes and span tables. This\nmeans if instrumentation downgraded a 128-bit trace ID to 64-bit, it will appear in a search as two\ntraces. This situation is possible when using unmaintained or out-of-date trace instrumentation.\n\nBy setting strict trace ID to false, indexes only consider the right-most 16 chars, allowing mixed\ntrace length lookup at a slight collision risk. Retrieval of the 32-character trace ID is retained\nby concatenating two columns in the span table like so:\n\n```\ntrace_id            text, // when strictTraceId=false, only contains right-most 16 chars\ntrace_id_high       text, // when strictTraceId=false, contains left-most 16 chars if present\n```\n\nIt is important to only set strict trace ID false during a transition and revert once complete, as\ndata written during this period is less intuitive for those using CQL, and contains a small\ncollision risk.\n\n## Tuning\nThis component is tuned to help reduce the size of indexes needed to\nperform query operations. The most important aspects are described below.\nSee [CassandraStorage](src/main/java/zipkin2/storage/cassandra/CassandraStorage.java) for details.\n\n### Autocomplete indexing\nRedundant requests to store autocomplete values are ignored for an hour\nto reduce load. This is implemented by\n[DelayLimiter](../../zipkin/src/main/java/zipkin2/internal/DelayLimiter.java)\n\n### Trace indexing\nIndexing in CQL is simplified by SASI, for example, reducing the number\nof tables from 7 down to 4 (from the original cassandra schema). SASI\nalso moves some write-amplification from CassandraSpanConsumer into C*.\n\nCassandraSpanConsumer directly writes to the tables `span`,\n`trace_by_service_remote_service` `trace_by_service_span` and\n`span_by_service`. The latter service based indexes amplify writes by a\nfactor of the distinct service names (`Span.localServiceName`).\n\nOther amplification happens internally to C*, visible in the increase\nwrite latency (although write latency remains performant at single digit\nmilliseconds).\n\n#### `span` indexing\nWhen queries only include a time range, trace ids are returned from a `ts_uuid`\nrange. This means no indexes are used when `GET /api/v2/traces` includes no\nparameters or only `endTs` or `lookback`.\n\nTwo secondary (SASI) indexes support `annotationQuery` with `serviceName`:\n* `annotation_query` supports LIKE (substring match) in `░error░error=500░`\n* `l_service` in used in conjunction with annotation_query searches.\n\nEx, `GET /api/v2/traces?serviceName=tweetiebird&annotationQuery=error` results\nin a single trace ID query against the above two indexes.\n\nNote: annotations with values longer than 256 characters are not written to the\n`annotation_query` SASI, as they aren't intended for use in user queries.\n\n#### `trace_by_service_X` indexing\n\n`trace_by_service_X` rows are answers to a shard of trace query. A query\nrequest is broken down into possibly multiple shards based on our index\nimplementation.\n\nEx. `GET /api/v2/traces?serviceName=tweetiebird%remoteService=s3`\n\nBreaks down into two query shards (this example omits time range and limit)\n* `(service=tweetiebird, span=)`\n* `(service=tweetiebird, remote_service=s3)`\n\nThe results intersect prioritizing on timestamp to return the distinct\ntrace IDs needed for a follow-up fetch.\n\n#### `trace_by_service_remote_service` indexing\n\nFor example, a span in trace ID 1 named \"get\" created by \"tweetiebird\",\naccessing the remote service \"s3\" results in the following row:\n\n* `service=service1, span=remote_service, ts=timestamp_millis, trace_id=1`\n\nThis index is only used when the `remoteServiceName` query is used. Ex.\n1. `GET /api/v2/traces?serviceName=tweetiebird&remoteServiceName=s3`\n1. `GET /api/v2/traces?serviceName=tweetiebird&maxDuration=199500&remoteServiceName=s3`\n\n#### `trace_by_service_span` indexing\n\nFor example, a span in trace ID 1 named \"get\" created by \"service1\",\ntaking 20 milliseconds results in the following rows:\n\n1. `service=service1, span=get, trace_id=1, ts=timestamp_millis, duration=200`\n2. `service=service1, span=, trace_id=1, ts=timestamp_millis, duration=200`\n\nHere are corresponding queries that relate to the above rows:\n1. `GET /api/v2/traces?serviceName=service1&spanName=get`\n1. `GET /api/v2/traces?serviceName=service1&spanName=get&minDuration=200000`\n1. `GET /api/v2/traces?serviceName=service1&minDuration=200000`\n1. `GET /api/v2/traces?spanName=get`\n1. `GET /api/v2/traces?maxDuration=199500`\n\nAs you'll notice, the duration component is optional, and stored in\nmillisecond resolution as opposed to microsecond (which the query represents).\nThe final query shows that the input is rounded up to the nearest millisecond.\n\nThe reason we can query on `duration` is due to a SASI index. Even though the\nsearch granularity is millisecond, original duration data remains microsecond\ngranularity. Meanwhile, write performance is dramatically better than writing\ndiscrete values, due to fewer distinct writes.\n\n#### Disabling indexing\nIndexing is a good default, but some sites who don't use Zipkin UI's\n\"Find a Trace\" screen may want to disable indexing. This means [indexing schema](src/main/resources/zipkin2-schema-indexes.cql)\nwon't be setup, nor written at runtime. This increases write throughput\nand reduces size on disk by not amplifying writes with index data.\n\n[Disabling search](../../README.md#disabling-search) disables indexing.\n\n### Time-To_live\nTime-To-Live is default now at the table level. It cannot be overridden in write requests.\n\nThere's a different default TTL for trace data and indexes, 7 days vs 3 days respectively. The impact is that you can\nretrieve a trace by ID for up to 7 days, but you can only search the last 3 days of traces (ex by service name).\n\n### Compaction\nTime-series data is compacted using TimeWindowCompactionStrategy, a known improved over DateTieredCompactionStrategy. Data is\noptimised for queries within a single day. The penalty of reading multiple days is small, a few disk seeks, compared to the\notherwise overhead of reading a significantly larger amount of data.\n\n### Benchmarking\nBenchmarking the new data model demonstrates a significant performance improvement on reads. How much of this translates to the\nZipkin UI is hard to tell due to the complexity of CassandraSpanConsumer and how searches are possible. Benchmarking stress\nprofiles are found in traces-stress.yaml and trace_by_service_span-stress.yaml and span_by_service-stress.yaml.\n"
  },
  {
    "path": "zipkin-storage/cassandra/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-storage-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-storage-cassandra</artifactId>\n  <name>Storage: Cassandra</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n\n    <!-- CheckReturnValue:OFF as it is used on some types that do not require it\n         https://groups.google.com/a/lists.datastax.com/g/java-driver-user/c/YbXjSoNV6Ns/m/U9w-HccWBgAJ -->\n    <!-- AutoValueImmutableFields:OFF because error-prone wants InsertSpan to depend on guava! -->\n    <errorprone.args>-Xep:CheckReturnValue:OFF -Xep:AutoValueImmutableFields:OFF</errorprone.args>\n  </properties>\n\n  <!-- Apache Cassandra java-driver is typically behind on jackson and netty -->\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>io.netty</groupId>\n        <artifactId>netty-bom</artifactId>\n        <version>${netty.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n\n      <dependency>\n        <groupId>com.fasterxml.jackson</groupId>\n        <artifactId>jackson-bom</artifactId>\n        <version>${jackson.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <dependencies>\n    <dependency>\n      <groupId>com.google.auto.value</groupId>\n      <artifactId>auto-value-annotations</artifactId>\n      <version>${auto-value.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.google.auto.value</groupId>\n      <artifactId>auto-value</artifactId>\n      <version>${auto-value.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.apache.cassandra</groupId>\n      <artifactId>java-driver-core</artifactId>\n      <version>${java-driver.version}</version>\n      <exclusions>\n        <!-- Exclude unused graph and geo dependencies -->\n        <exclusion>\n          <groupId>com.esri.geometry</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <!-- temporary until https://github.com/apache/cassandra-java-driver/pull/1904 -->\n        <exclusion>\n          <groupId>com.github.jnr</groupId>\n          <artifactId>jnr-posix</artifactId>\n        </exclusion>\n        <exclusion>\n          <groupId>org.apache.tinkerpop</groupId>\n          <artifactId>*</artifactId>\n        </exclusion>\n        <!-- We retain reactivestreams even though we don't use it because\n             Mockito dies trying to mock CqlSession without it. -->\n      </exclusions>\n    </dependency>\n\n    <!-- Set SLF4J version to what's used by the server, without excluding the version\n         of cassandra, which is used in the zipkin-dependencies spark job. -->\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-api</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <!-- temporary until https://github.com/apache/cassandra-java-driver/pull/1904 -->\n    <dependency>\n      <groupId>com.github.jnr</groupId>\n      <artifactId>jnr-posix</artifactId>\n      <version>3.1.21</version>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/AnnotationCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.data.UdtValue;\nimport com.datastax.oss.driver.api.core.type.UserDefinedType;\nimport com.datastax.oss.driver.api.core.type.codec.MappingCodec;\nimport com.datastax.oss.driver.api.core.type.codec.TypeCodec;\nimport com.datastax.oss.driver.api.core.type.reflect.GenericType;\nimport zipkin2.Annotation;\nimport zipkin2.internal.Nullable;\n\n/**\n * For better performance, this relies on ordinals instead of name lookups.\n *\n * <p>0 = \"ts\" = {@link Annotation#timestamp()}\n * <p>0 = \"v\" = {@link Annotation#value()}\n */\nfinal class AnnotationCodec extends MappingCodec<UdtValue, Annotation> {\n  AnnotationCodec(TypeCodec<UdtValue> innerCodec) {\n    super(innerCodec, GenericType.of(Annotation.class));\n  }\n\n  @Override public UserDefinedType getCqlType() {\n    return (UserDefinedType) super.getCqlType();\n  }\n\n  @Nullable @Override protected Annotation innerToOuter(@Nullable UdtValue value) {\n    if (value == null || value.isNull(0) || value.isNull(1)) return null;\n    return Annotation.create(value.getLong(0), value.getString(1));\n  }\n\n  @Nullable @Override protected UdtValue outerToInner(@Nullable Annotation value) {\n    if (value == null) return null;\n    return getCqlType().newValue().setLong(0, value.timestamp()).setString(1, value.value());\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraAutocompleteTags.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.storage.AutocompleteTags;\n\nclass CassandraAutocompleteTags implements AutocompleteTags {\n  final boolean enabled;\n  final Call<List<String>> keysCall;\n  final SelectAutocompleteValues.Factory valuesCallFactory;\n\n  CassandraAutocompleteTags(CassandraStorage storage) {\n    enabled = storage.searchEnabled\n      && !storage.autocompleteKeys.isEmpty()\n      && storage.metadata().hasAutocompleteTags;\n    keysCall = Call.create(List.copyOf(storage.autocompleteKeys));\n    valuesCallFactory = enabled ? new SelectAutocompleteValues.Factory(storage.session()) : null;\n  }\n\n  @Override public Call<List<String>> getKeys() {\n    if (!enabled) return Call.emptyList();\n    return keysCall.clone();\n  }\n\n  @Override public Call<List<String>> getValues(String key) {\n    if (key == null) throw new NullPointerException(\"key == null\");\n    if (key.isEmpty()) throw new IllegalArgumentException(\"key was empty\");\n    if (!enabled) return Call.emptyList();\n    return valuesCallFactory.create(key);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.uuid.Uuids;\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport zipkin2.Annotation;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.internal.AggregateCall;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.cassandra.internal.call.InsertEntry;\n\nimport static zipkin2.storage.cassandra.CassandraUtil.durationIndexBucket;\nimport static zipkin2.storage.cassandra.Schema.TABLE_AUTOCOMPLETE_TAGS;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_REMOTE_SERVICES;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_SPANS;\n\nclass CassandraSpanConsumer implements SpanConsumer { // not final for testing\n  final boolean searchEnabled;\n  final InsertSpan.Factory insertSpan;\n  final Set<String> autocompleteKeys;\n\n  // Everything below here is null when search is disabled\n  @Nullable final InsertTraceByServiceRemoteService.Factory insertTraceByServiceRemoteService;\n  @Nullable final InsertTraceByServiceSpan.Factory insertTraceByServiceSpan;\n  @Nullable final InsertEntry.Factory insertServiceSpan;\n  @Nullable final InsertEntry.Factory insertServiceRemoteService;\n  @Nullable final InsertEntry.Factory insertAutocompleteValue;\n\n  void clear() {\n    if (insertServiceSpan != null) insertServiceSpan.clear();\n    if (insertServiceRemoteService != null) insertServiceRemoteService.clear();\n    if (insertAutocompleteValue != null) insertAutocompleteValue.clear();\n  }\n\n  CassandraSpanConsumer(CassandraStorage storage) {\n    this(\n      storage.session(), storage.metadata(),\n      storage.strictTraceId, storage.searchEnabled,\n      storage.autocompleteKeys, storage.autocompleteTtl, storage.autocompleteCardinality\n    );\n  }\n\n  CassandraSpanConsumer(CqlSession session, Schema.Metadata metadata, boolean strictTraceId,\n    boolean searchEnabled, Set<String> autocompleteKeys, int autocompleteTtl,\n    int autocompleteCardinality) {\n    this.searchEnabled = searchEnabled;\n    this.autocompleteKeys = autocompleteKeys;\n\n    insertSpan = new InsertSpan.Factory(session, strictTraceId, searchEnabled);\n\n    if (!searchEnabled) {\n      insertTraceByServiceRemoteService = null;\n      insertTraceByServiceSpan = null;\n      insertServiceRemoteService = null;\n      insertServiceSpan = null;\n      insertAutocompleteValue = null;\n      return;\n    }\n\n    insertTraceByServiceSpan = new InsertTraceByServiceSpan.Factory(session, strictTraceId);\n    if (metadata.hasRemoteService) {\n      insertTraceByServiceRemoteService =\n        new InsertTraceByServiceRemoteService.Factory(session, strictTraceId);\n      insertServiceRemoteService = new InsertEntry.Factory(\n        \"INSERT INTO \" + TABLE_SERVICE_REMOTE_SERVICES + \" (service, remote_service) VALUES (?,?)\",\n        session, autocompleteTtl, autocompleteCardinality\n      );\n    } else {\n      insertTraceByServiceRemoteService = null;\n      insertServiceRemoteService = null;\n    }\n    insertServiceSpan = new InsertEntry.Factory(\n      \"INSERT INTO \" + TABLE_SERVICE_SPANS + \" (service, span) VALUES (?,?)\",\n      session, autocompleteTtl, autocompleteCardinality\n    );\n    if (metadata.hasAutocompleteTags && !autocompleteKeys.isEmpty()) {\n      insertAutocompleteValue = new InsertEntry.Factory(\n        \"INSERT INTO \" + TABLE_AUTOCOMPLETE_TAGS + \" (key, value) VALUES (?,?)\",\n        session, autocompleteTtl, autocompleteCardinality\n      );\n    } else {\n      insertAutocompleteValue = null;\n    }\n  }\n\n  /**\n   * This fans out into many requests, last count was 2 * spans.size. If any of these fail, the\n   * returned future will fail. Most callers drop or log the result.\n   */\n  @Override public Call<Void> accept(List<Span> input) {\n    if (input.isEmpty()) return Call.create(null);\n\n    Set<InsertSpan.Input> spans = new LinkedHashSet<>();\n    Set<Map.Entry<String, String>> serviceRemoteServices = new LinkedHashSet<>();\n    Set<Map.Entry<String, String>> serviceSpans = new LinkedHashSet<>();\n    Set<InsertTraceByServiceRemoteService.Input> traceByServiceRemoteServices =\n      new LinkedHashSet<>();\n    Set<InsertTraceByServiceSpan.Input> traceByServiceSpans = new LinkedHashSet<>();\n    Set<Map.Entry<String, String>> autocompleteTags = new LinkedHashSet<>();\n\n    for (Span s : input) {\n      // indexing occurs by timestamp, so derive one if not present.\n      long ts_micro = s.timestampAsLong();\n      if (ts_micro == 0L) ts_micro = guessTimestamp(s);\n\n      // fallback to current time on the ts_uuid for span data, so we know when it was inserted\n      UUID ts_uuid =\n        new UUID(\n          Uuids.startOf(ts_micro != 0L ? (ts_micro / 1000L) : System.currentTimeMillis())\n            .getMostSignificantBits(),\n          Uuids.random().getLeastSignificantBits());\n\n      spans.add(insertSpan.newInput(s, ts_uuid));\n\n      if (!searchEnabled) continue;\n\n      // Empty values allow for api queries with blank service or span name\n      String service = s.localServiceName() != null ? s.localServiceName() : \"\";\n      String span =\n        null != s.name() ? s.name() : \"\"; // Empty value allows for api queries without span name\n\n      if (null == s.localServiceName()) continue; // don't index further w/o a service name\n\n      // service span and remote service indexes is refreshed regardless of timestamp\n      String remoteService = s.remoteServiceName();\n      if (insertServiceRemoteService != null && remoteService != null) {\n        serviceRemoteServices.add(Map.entry(service, remoteService));\n      }\n      serviceSpans.add(Map.entry(service, span));\n\n      if (ts_micro == 0L) continue; // search is only valid with a timestamp, don't index w/o it!\n      int bucket = durationIndexBucket(ts_micro); // duration index is milliseconds not microseconds\n      long duration = s.durationAsLong() / 1000L;\n      traceByServiceSpans.add(\n        insertTraceByServiceSpan.newInput(service, span, bucket, ts_uuid, s.traceId(), duration));\n      if (span.isEmpty()) continue;\n\n      if (insertServiceRemoteService != null && remoteService != null) {\n        traceByServiceRemoteServices.add(\n          insertTraceByServiceRemoteService.newInput(service, remoteService, bucket, ts_uuid,\n            s.traceId()));\n      }\n      traceByServiceSpans.add( // Allows lookup without the span name\n        insertTraceByServiceSpan.newInput(service, \"\", bucket, ts_uuid, s.traceId(), duration));\n\n      if (insertAutocompleteValue != null) {\n        for (Map.Entry<String, String> entry : s.tags().entrySet()) {\n          if (autocompleteKeys.contains(entry.getKey())) autocompleteTags.add(entry);\n        }\n      }\n    }\n    List<Call<Void>> calls = new ArrayList<>();\n    for (InsertSpan.Input span : spans) {\n      calls.add(insertSpan.create(span));\n    }\n    for (Map.Entry<String, String> serviceSpan : serviceSpans) {\n      insertServiceSpan.maybeAdd(serviceSpan, calls);\n    }\n    for (Map.Entry<String, String> serviceRemoteService : serviceRemoteServices) {\n      insertServiceRemoteService.maybeAdd(serviceRemoteService, calls);\n    }\n    for (InsertTraceByServiceSpan.Input serviceSpan : traceByServiceSpans) {\n      calls.add(insertTraceByServiceSpan.create(serviceSpan));\n    }\n    for (InsertTraceByServiceRemoteService.Input serviceRemoteService : traceByServiceRemoteServices) {\n      calls.add(insertTraceByServiceRemoteService.create(serviceRemoteService));\n    }\n    for (Map.Entry<String, String> autocompleteTag : autocompleteTags) {\n      insertAutocompleteValue.maybeAdd(autocompleteTag, calls);\n    }\n    return calls.isEmpty() ? Call.create(null) : AggregateCall.newVoidCall(calls);\n  }\n\n  static long guessTimestamp(Span span) {\n    assert 0L == span.timestampAsLong() : \"method only for when span has no timestamp\";\n    for (Annotation annotation : span.annotations()) {\n      if (0L < annotation.timestamp()) return annotation.timestamp();\n    }\n    return 0L; // return a timestamp that won't match a query\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraSpanStore.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.DriverException;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport com.datastax.oss.driver.api.core.uuid.Uuids;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Call;\nimport zipkin2.Call.FlatMapper;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.Traces;\nimport zipkin2.storage.cassandra.internal.KeyspaceMetadataUtil;\nimport zipkin2.storage.cassandra.internal.call.IntersectKeySets;\nimport zipkin2.storage.cassandra.internal.call.IntersectMaps;\n\nimport static java.util.Arrays.asList;\nimport static zipkin2.storage.cassandra.CassandraUtil.durationIndexBucket;\nimport static zipkin2.storage.cassandra.CassandraUtil.traceIdsSortedByDescTimestamp;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_REMOTE_SERVICES;\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_SPAN;\n\nclass CassandraSpanStore implements SpanStore, Traces, ServiceAndSpanNames { //not final for testing\n  static final Logger LOG = LoggerFactory.getLogger(CassandraSpanStore.class);\n\n  final int indexFetchMultiplier;\n  final boolean searchEnabled;\n  final SelectFromSpan.Factory spans;\n  final SelectDependencies.Factory dependencies;\n\n  // Everything below here is null when search is disabled\n  final int indexTtl; // zero when disabled\n  @Nullable final Call<List<String>> serviceNames;\n  @Nullable final SelectRemoteServiceNames.Factory remoteServiceNames;\n  @Nullable final SelectSpanNames.Factory spanNames;\n  @Nullable final SelectTraceIdsFromSpan.Factory spanTable;\n  @Nullable final SelectTraceIdsFromServiceSpan.Factory traceIdsFromServiceSpan;\n  @Nullable final SelectTraceIdsFromServiceRemoteService.Factory traceIdsFromServiceRemoteService;\n\n  CassandraSpanStore(CassandraStorage storage) {\n    this(storage.session(),\n      storage.metadata(),\n      Schema.ensureKeyspaceMetadata(storage.session(), storage.keyspace),\n      storage.maxTraceCols,\n      storage.indexFetchMultiplier,\n      storage.strictTraceId,\n      storage.searchEnabled);\n  }\n\n  CassandraSpanStore(CqlSession session, Schema.Metadata metadata, KeyspaceMetadata keyspace,\n    int maxTraceCols, int indexFetchMultiplier, boolean strictTraceId, boolean searchEnabled) {\n    this.indexFetchMultiplier = indexFetchMultiplier;\n    this.searchEnabled = searchEnabled;\n    spans = new SelectFromSpan.Factory(session, strictTraceId, maxTraceCols);\n    dependencies = new SelectDependencies.Factory(session);\n\n    if (!searchEnabled) {\n      indexTtl = 0;\n      serviceNames = null;\n      remoteServiceNames = null;\n      spanNames = null;\n      spanTable = null;\n      traceIdsFromServiceSpan = null;\n      traceIdsFromServiceRemoteService = null;\n      return;\n    }\n\n    indexTtl = KeyspaceMetadataUtil.getDefaultTtl(keyspace, TABLE_TRACE_BY_SERVICE_SPAN);\n    serviceNames = new SelectServiceNames.Factory(session).create();\n    if (metadata.hasRemoteService) {\n      remoteServiceNames = new SelectRemoteServiceNames.Factory(session);\n      traceIdsFromServiceRemoteService =\n        new SelectTraceIdsFromServiceRemoteService.Factory(session);\n    } else {\n      remoteServiceNames = null;\n      traceIdsFromServiceRemoteService = null;\n    }\n    spanNames = new SelectSpanNames.Factory(session);\n    traceIdsFromServiceSpan = new SelectTraceIdsFromServiceSpan.Factory(session);\n    spanTable = initialiseSelectTraceIdsFromSpan(session);\n  }\n\n  /**\n   * This makes it possible to safely drop the annotations_query SASI.\n   *\n   * <p>If dropped, trying to search by annotation in the UI will throw an IllegalStateException.\n   */\n  static SelectTraceIdsFromSpan.Factory initialiseSelectTraceIdsFromSpan(CqlSession session) {\n    try {\n      return new SelectTraceIdsFromSpan.Factory(session);\n    } catch (DriverException ex) {\n      LOG.warn(\"failed to prepare annotation_query index statements: {}\", ex.getMessage(), ex);\n      return null;\n    }\n  }\n\n  /**\n   * This fans out into a number of requests corresponding to query input. In simplest case, there\n   * is less than a day of data queried, and only one expression. This implies one call to fetch\n   * trace IDs and another to retrieve the span details.\n   *\n   * <p>The amount of backend calls increase in dimensions of query complexity, days of data, and\n   * limit of traces requested. For example, a query like \"http.path=/foo and error\" will be two\n   * select statements for the expression, possibly follow-up calls for pagination (when over 5K\n   * rows match). Once IDs are parsed, there's one call for each 5K rows of span data. This means\n   * \"http.path=/foo and error\" is minimally 3 network calls, the first two in parallel.\n   */\n  @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {\n    if (!searchEnabled) return Call.emptyList();\n\n    TimestampRange timestampRange = timestampRange(request, indexTtl);\n    // If we have to make multiple queries, over fetch on indexes as they don't return distinct\n    // (trace id, timestamp) rows. This mitigates intersection resulting in < limit traces\n    final int traceIndexFetchSize = request.limit() * indexFetchMultiplier;\n    List<Call<Map<String, Long>>> callsToIntersect = new ArrayList<>();\n\n    List<String> annotationKeys = CassandraUtil.annotationKeys(request);\n    for (String annotationKey : annotationKeys) {\n      if (spanTable == null) {\n        throw new IllegalArgumentException(request.annotationQueryString()\n          + \" query unsupported due to missing annotation_query index\");\n      }\n      callsToIntersect.add(\n        spanTable.newCall(request.serviceName(), annotationKey, timestampRange, traceIndexFetchSize)\n      );\n    }\n\n    // Bucketed calls can be expensive when service name isn't specified. This guards against abuse.\n    if (request.remoteServiceName() != null\n      || request.spanName() != null\n      || request.minDuration() != null\n      || callsToIntersect.isEmpty()) {\n      callsToIntersect.add(newBucketedTraceIdCall(request, timestampRange, traceIndexFetchSize));\n    }\n\n    if (callsToIntersect.size() == 1) {\n      return callsToIntersect.get(0)\n        .map(traceIdsSortedByDescTimestamp())\n        .flatMap(spans.newFlatMapper(request));\n    }\n\n    // We achieve the AND goal, by intersecting each of the key sets.\n    IntersectKeySets intersectedTraceIds = new IntersectKeySets(callsToIntersect);\n    // @xxx the sorting by timestamp desc is broken here^\n    return intersectedTraceIds.flatMap(spans.newFlatMapper(request));\n  }\n\n  /**\n   * Creates a call representing one or more queries against {@link Schema#TABLE_TRACE_BY_SERVICE_SPAN}\n   * and possibly {@link Schema#TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE}.\n   *\n   * <p>The result will be an aggregate if the input request serviceName is null, both span name\n   * and remote service name are supplied, or there's more than one day of data in the timestamp\n   * range.\n   *\n   * <p>Note that when {@link QueryRequest#serviceName()} is null, the returned query composes over\n   * {@link #getServiceNames()}. This means that if you have 1000 service names, you will end up\n   * with a composition of at least 1000 calls.\n   */\n  // TODO: smartly handle when serviceName is null. For example, rank recently written serviceNames\n  // and speculatively query those first.\n  Call<Map<String, Long>> newBucketedTraceIdCall(\n    QueryRequest request, TimestampRange timestampRange, int traceIndexFetchSize) {\n    // trace_by_service_span adds special empty-string span name in order to search by all\n    String spanName = null != request.spanName() ? request.spanName() : \"\";\n    Long minDuration = request.minDuration(), maxDuration = request.maxDuration();\n    int startBucket = durationIndexBucket(timestampRange.startMillis * 1000);\n    int endBucket = durationIndexBucket(timestampRange.endMillis * 1000);\n    if (startBucket > endBucket) {\n      throw new IllegalArgumentException(\n        \"Start bucket (\" + startBucket + \") > end bucket (\" + endBucket + \")\");\n    }\n\n    // \"\" isn't a real value. it is used to template bucketed calls and replaced later\n    String serviceName = null != request.serviceName() ? request.serviceName() : \"\";\n\n    // TODO: ideally, the buckets are traversed backwards, only spawning queries for older buckets\n    // if younger buckets are empty. This will be an async continuation, punted for now.\n    List<SelectTraceIdsFromServiceSpan.Input> serviceSpans = new ArrayList<>();\n    List<SelectTraceIdsFromServiceRemoteService.Input> serviceRemoteServices = new ArrayList<>();\n    String remoteService = request.remoteServiceName();\n    for (int bucket = endBucket; bucket >= startBucket; bucket--) {\n      boolean addSpanQuery = true;\n      if (remoteService != null) {\n        if (traceIdsFromServiceRemoteService == null) {\n          throw new IllegalArgumentException(\"remoteService=\" + remoteService\n            + \" unsupported due to missing table \" + TABLE_SERVICE_REMOTE_SERVICES);\n        }\n        serviceRemoteServices.add(\n          traceIdsFromServiceRemoteService.newInput(\n            serviceName,\n            remoteService,\n            bucket,\n            timestampRange,\n            traceIndexFetchSize));\n        // If the remote service query can satisfy the request, don't make a redundant span query\n        addSpanQuery = !spanName.isEmpty() || minDuration != null;\n      }\n      if (!addSpanQuery) continue;\n\n      serviceSpans.add(\n        traceIdsFromServiceSpan.newInput(\n          serviceName,\n          spanName,\n          bucket,\n          minDuration,\n          maxDuration,\n          timestampRange,\n          traceIndexFetchSize));\n    }\n\n    if (serviceName.isEmpty()) {\n      // If we have no service name, we have to lookup service names before running trace ID queries\n      Call<List<String>> serviceNames = getServiceNames();\n      if (serviceRemoteServices.isEmpty()) {\n        return serviceNames.flatMap(traceIdsFromServiceSpan.newFlatMapper(serviceSpans));\n      } else if (serviceSpans.isEmpty()) {\n        return serviceNames.flatMap(\n          traceIdsFromServiceRemoteService.newFlatMapper(serviceRemoteServices));\n      }\n      return serviceNames.flatMap(new AggregateFlatMapper<>(\n        traceIdsFromServiceSpan.newFlatMapper(serviceSpans),\n        traceIdsFromServiceRemoteService.newFlatMapper(serviceRemoteServices)\n      ));\n    }\n    if (serviceRemoteServices.isEmpty()) {\n      return traceIdsFromServiceSpan.newCall(serviceSpans);\n    } else if (serviceSpans.isEmpty()) {\n      return traceIdsFromServiceRemoteService.newCall(serviceRemoteServices);\n    } else {\n      return new IntersectMaps<>(asList(\n        traceIdsFromServiceSpan.newCall(serviceSpans),\n        traceIdsFromServiceRemoteService.newCall(serviceRemoteServices)\n      ));\n    }\n  }\n\n  static class AggregateFlatMapper<K, V> implements FlatMapper<List<K>, Map<K, V>> {\n    final FlatMapper<List<K>, Map<K, V>> left, right;\n\n    AggregateFlatMapper(FlatMapper<List<K>, Map<K, V>> left, FlatMapper<List<K>, Map<K, V>> right) {\n      this.left = left;\n      this.right = right;\n    }\n\n    @Override public Call<Map<K, V>> map(List<K> input) {\n      return new IntersectMaps<>(asList(left.map(input), right.map(input)));\n    }\n  }\n\n  @Override public Call<List<Span>> getTrace(String traceId) {\n    // make sure we have a 16 or 32 character trace ID\n    String normalizedTraceId = Span.normalizeTraceId(traceId);\n    return spans.newCall(normalizedTraceId);\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n    return spans.newCall(traceIds);\n  }\n\n  @Override public Call<List<String>> getServiceNames() {\n    if (!searchEnabled) return Call.emptyList();\n    return serviceNames.clone();\n  }\n\n  @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {\n    if (serviceName.isEmpty() || !searchEnabled || remoteServiceNames == null) {\n      return Call.emptyList();\n    }\n    return remoteServiceNames.create(serviceName);\n  }\n\n  @Override public Call<List<String>> getSpanNames(String serviceName) {\n    if (serviceName.isEmpty() || !searchEnabled) return Call.emptyList();\n    return spanNames.create(serviceName);\n  }\n\n  @Override public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n    if (endTs <= 0) throw new IllegalArgumentException(\"endTs <= 0\");\n    if (lookback <= 0) throw new IllegalArgumentException(\"lookback <= 0\");\n    return dependencies.create(endTs, lookback);\n  }\n\n  static final class TimestampRange {\n    long startMillis;\n    UUID startUUID;\n    long endMillis;\n    UUID endUUID;\n  }\n\n  TimestampRange timestampRange(QueryRequest request, int indexTtl) {\n    long oldestData = Math.max(System.currentTimeMillis() - indexTtl * 1000L, 0); // >= 1970\n    TimestampRange result = new TimestampRange();\n    result.startMillis = Math.max((request.endTs() - request.lookback()), oldestData);\n    result.startUUID = Uuids.startOf(result.startMillis);\n    result.endMillis = Math.max(request.endTs(), oldestData);\n    result.endUUID = Uuids.endOf(result.endMillis);\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.auth.AuthProvider;\nimport com.datastax.oss.driver.api.core.auth.ProgrammaticPlainTextAuthProvider;\nimport com.datastax.oss.driver.api.core.config.DriverOption;\nimport java.util.Map;\nimport java.util.Set;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.internal.ClosedComponentException;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.AutocompleteTags;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.Traces;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\n/**\n * CQL3 implementation of zipkin storage.\n *\n * <p>Queries are logged to the category \"com.datastax.oss.driver.api.core.cql.QueryLogger\" when\n * debug or trace is enabled via SLF4J. Trace level includes bound values.\n *\n * <p>Schema is installed by default from \"/zipkin2-schema.cql\"\n *\n * <p>When {@link StorageComponent.Builder#strictTraceId(boolean)} is disabled, span and index data\n * are uniformly written with 64-bit trace ID length. When retrieving data, an extra \"trace_id_high\"\n * field clarifies if a 128-bit trace ID was sent.\n */\npublic final class CassandraStorage extends StorageComponent {\n  // @FunctionalInterface, except safe for lower language levels\n  public interface SessionFactory {\n    SessionFactory DEFAULT = new DefaultSessionFactory();\n\n    CqlSession create(CassandraStorage storage);\n  }\n\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder extends CassandraStorageBuilder<Builder> {\n    Builder() {\n      super(Schema.DEFAULT_KEYSPACE);\n    }\n\n    /** Keyspace to store span and index data. Defaults to \"zipkin2\" */\n    @Override public Builder keyspace(String keyspace) {\n      return super.keyspace(keyspace);\n    }\n\n    /**\n     * Ensures that schema exists, if enabled tries to execute:\n     * <ol>\n     *   <li>io.zipkin.zipkin2:zipkin-storage-cassandra/zipkin2-schema.cql</li>\n     *   <li>io.zipkin.zipkin2:zipkin-storage-cassandra/zipkin2-indexes.cql</li>\n     * </ol>\n     * Defaults to true.\n     */\n    @Override public Builder ensureSchema(boolean ensureSchema) {\n      return super.ensureSchema(ensureSchema);\n    }\n\n    @Override public CassandraStorage build() {\n      return new CassandraStorage(this);\n    }\n  }\n\n  final boolean strictTraceId, searchEnabled;\n  final Set<String> autocompleteKeys;\n  final int autocompleteTtl, autocompleteCardinality;\n\n  final String contactPoints, localDc;\n  final Map<DriverOption, Integer> poolingOptions;\n  @Nullable final AuthProvider authProvider;\n  final boolean useSsl;\n  final boolean sslHostnameValidation;\n  final String keyspace;\n  final int maxTraceCols, indexFetchMultiplier;\n\n  final LazySession session;\n\n  CassandraStorage(CassandraStorageBuilder<?> builder) {\n    // Assign generic configuration for all storage components\n    this.strictTraceId = builder.strictTraceId;\n    this.searchEnabled = builder.searchEnabled;\n    this.autocompleteKeys = builder.autocompleteKeys;\n    this.autocompleteTtl = builder.autocompleteTtl;\n    this.autocompleteCardinality = builder.autocompleteCardinality;\n\n    // Assign configuration used to create a session\n    this.contactPoints = builder.contactPoints;\n    this.localDc = builder.localDc;\n    this.poolingOptions = builder.poolingOptions();\n    if (builder.username != null) {\n      this.authProvider = new ProgrammaticPlainTextAuthProvider(builder.username, builder.password);\n    } else {\n      this.authProvider = null;\n    }\n    this.useSsl = builder.useSsl;\n    this.sslHostnameValidation = builder.sslHostnameValidation;\n    this.keyspace = builder.keyspace;\n\n    // Assign configuration used to control queries\n    this.maxTraceCols = builder.maxTraceCols;\n    this.indexFetchMultiplier = builder.indexFetchMultiplier;\n\n    this.session = new LazySession(this, builder.sessionFactory, builder.ensureSchema);\n  }\n\n  /** close is typically called from a different thread */\n  volatile boolean closeCalled;\n\n  volatile CassandraSpanConsumer spanConsumer;\n  volatile CassandraSpanStore spanStore;\n  volatile CassandraAutocompleteTags tagStore;\n\n  /** Lazy initializes or returns the session in use by this storage component. */\n  CqlSession session() {\n    return session.get();\n  }\n\n  Schema.Metadata metadata() {\n    return session.metadata();\n  }\n\n  /** {@inheritDoc} Memoized in order to avoid re-preparing statements */\n  @Override public SpanStore spanStore() {\n    if (spanStore == null) {\n      synchronized (this) {\n        if (spanStore == null) {\n          spanStore = new CassandraSpanStore(this);\n        }\n      }\n    }\n    return spanStore;\n  }\n\n  @Override public Traces traces() {\n    return (Traces) spanStore();\n  }\n\n  @Override public ServiceAndSpanNames serviceAndSpanNames() {\n    return (ServiceAndSpanNames) spanStore();\n  }\n\n  @Override public AutocompleteTags autocompleteTags() {\n    if (tagStore == null) {\n      synchronized (this) {\n        if (tagStore == null) {\n          tagStore = new CassandraAutocompleteTags(this);\n        }\n      }\n    }\n    return tagStore;\n  }\n\n  // Memoized in order to avoid re-preparing statements\n  @Override public SpanConsumer spanConsumer() {\n    if (spanConsumer == null) {\n      synchronized (this) {\n        if (spanConsumer == null) {\n          spanConsumer = new CassandraSpanConsumer(this);\n        }\n      }\n    }\n    return spanConsumer;\n  }\n\n  @Override public boolean isOverCapacity(Throwable e) {\n    return ResultSetFutureCall.isOverCapacity(e);\n  }\n\n  @Override public String toString() {\n    return \"CassandraStorage{contactPoints=\" + contactPoints + \", keyspace=\" + keyspace + \"}\";\n  }\n\n  @Override public CheckResult check() {\n    if (closeCalled) throw new ClosedComponentException();\n    try {\n      session.healthCheck();\n    } catch (Throwable e) {\n      Call.propagateIfFatal(e);\n      return CheckResult.failed(e);\n    }\n    return CheckResult.OK;\n  }\n\n  @Override public void close() {\n    if (closeCalled) return;\n    session.close();\n    closeCalled = true;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraStorageBuilder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.config.DefaultDriverOption;\nimport com.datastax.oss.driver.api.core.config.DriverOption;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiFunction;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.CONNECTION_MAX_REQUESTS;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE;\n\nabstract class CassandraStorageBuilder<B extends CassandraStorageBuilder<B>>\n  extends StorageComponent.Builder {\n\n  CassandraStorage.SessionFactory sessionFactory = CassandraStorage.SessionFactory.DEFAULT;\n  boolean strictTraceId = true, searchEnabled = true;\n  Set<String> autocompleteKeys = Set.of();\n  int autocompleteTtl = (int) TimeUnit.HOURS.toMillis(1);\n  int autocompleteCardinality = 5 * 4000; // Ex. 5 site tags with cardinality 4000 each\n\n  String contactPoints = \"localhost\";\n  // Driver v4 requires this, so take a guess! When we are wrong, the user can override anyway\n  String localDc = \"datacenter1\";\n  @Nullable String username, password;\n  boolean useSsl = false;\n  boolean sslHostnameValidation = true;\n\n  String keyspace;\n  BiFunction<CassandraStorage, CqlSession, Schema.Metadata> ensureSchema = Schema::ensure;\n\n  int maxTraceCols = 100_000;\n  int indexFetchMultiplier = 3;\n\n  // Zipkin collectors can create out a lot of async requests in bursts, so we\n  // increase some properties beyond the norm.\n  /** @see DefaultDriverOption#CONNECTION_POOL_LOCAL_SIZE */\n  // Ported from java-driver v3 PoolingOptions.setMaxConnectionsPerHost(HostDistance.LOCAL, 8)\n  int poolLocalSize = 8;\n  /** @see DefaultDriverOption#CONNECTION_MAX_REQUESTS */\n  // Ported from java-driver v3 PoolingOptions.setMaxQueueSize(40960)\n  final int maxRequestsPerConnection = 40960 / poolLocalSize;\n\n  Map<DriverOption, Integer> poolingOptions() {\n    Map<DriverOption, Integer> result = new LinkedHashMap<>();\n    result.put(CONNECTION_POOL_LOCAL_SIZE, poolLocalSize);\n    result.put(CONNECTION_MAX_REQUESTS, maxRequestsPerConnection);\n    return result;\n  }\n\n  CassandraStorageBuilder(String defaultKeyspace) {\n    keyspace = defaultKeyspace;\n  }\n\n  @Override public B strictTraceId(boolean strictTraceId) {\n    this.strictTraceId = strictTraceId;\n    return (B) this;\n  }\n\n  @Override public B searchEnabled(boolean searchEnabled) {\n    this.searchEnabled = searchEnabled;\n    return (B) this;\n  }\n\n  @Override public B autocompleteKeys(List<String> keys) {\n    if (keys == null) throw new NullPointerException(\"keys == null\");\n    this.autocompleteKeys = Set.copyOf(keys);\n    return (B) this;\n  }\n\n  @Override public B autocompleteTtl(int autocompleteTtl) {\n    if (autocompleteTtl <= 0) throw new IllegalArgumentException(\"autocompleteTtl <= 0\");\n    this.autocompleteTtl = autocompleteTtl;\n    return (B) this;\n  }\n\n  @Override public B autocompleteCardinality(int autocompleteCardinality) {\n    if (autocompleteCardinality <= 0) {\n      throw new IllegalArgumentException(\"autocompleteCardinality <= 0\");\n    }\n    this.autocompleteCardinality = autocompleteCardinality;\n    return (B) this;\n  }\n\n  /**\n   * Comma separated list of host addresses part of Cassandra cluster. You can also specify a custom\n   * port with 'host:port'. Defaults to localhost on port 9042 *\n   */\n  public B contactPoints(String contactPoints) {\n    if (contactPoints == null) throw new NullPointerException(\"contactPoints == null\");\n    this.contactPoints = contactPoints;\n    return (B) this;\n  }\n\n  /**\n   * Name of the datacenter that will be considered \"local\" for latency load balancing. When unset,\n   * load-balancing is round-robin.\n   */\n  public B localDc(String localDc) {\n    if (localDc == null) throw new NullPointerException(\"localDc == null\");\n    this.localDc = localDc;\n    return (B) this;\n  }\n\n  /** Max pooled connections per datacenter-local host. Defaults to 8 */\n  public B maxConnections(int maxConnections) {\n    if (maxConnections <= 0) throw new IllegalArgumentException(\"maxConnections <= 0\");\n    this.poolLocalSize = maxConnections;\n    return (B) this;\n  }\n\n  /** Will throw an exception on startup if authentication fails. No default. */\n  public B username(@Nullable String username) {\n    this.username = username;\n    return (B) this;\n  }\n\n  /** Will throw an exception on startup if authentication fails. No default. */\n  public B password(@Nullable String password) {\n    this.password = password;\n    return (B) this;\n  }\n\n  /** Use ssl for connection. Defaults to false. */\n  public B useSsl(boolean useSsl) {\n    this.useSsl = useSsl;\n    return (B) this;\n  }\n\n  /** Controls validation of Cassandra server hostname. Defaults to true. */\n  public B sslHostnameValidation(boolean sslHostnameValidation) {\n    this.sslHostnameValidation = sslHostnameValidation;\n    return (B) this;\n  }\n\n  /** Keyspace to store span and index data. Defaults to \"zipkin3\" */\n  public B keyspace(String keyspace) {\n    if (keyspace == null) throw new NullPointerException(\"keyspace == null\");\n    this.keyspace = keyspace;\n    return (B) this;\n  }\n\n  /** Override to control how sessions are created. */\n  public B sessionFactory(CassandraStorage.SessionFactory sessionFactory) {\n    if (sessionFactory == null) throw new NullPointerException(\"sessionFactory == null\");\n    this.sessionFactory = sessionFactory;\n    return (B) this;\n  }\n\n  public B ensureSchema(boolean ensureSchema) {\n    if (ensureSchema) {\n      this.ensureSchema = Schema::ensure;\n    } else {\n      this.ensureSchema = Schema::validate;\n    }\n    return (B) this;\n  }\n\n  /**\n   * Spans have multiple values for the same id. For example, a client and server contribute to the\n   * same span id. When searching for spans by id, the amount of results may be larger than the ids.\n   * This defines a threshold which accommodates this situation, without looking for an unbounded\n   * number of results.\n   */\n  public B maxTraceCols(int maxTraceCols) {\n    if (maxTraceCols <= 0) throw new IllegalArgumentException(\"maxTraceCols <= 0\");\n    this.maxTraceCols = maxTraceCols;\n    return (B) this;\n  }\n\n  /**\n   * How many more index rows to fetch than the user-supplied query limit. Defaults to 3.\n   *\n   * <p>Backend requests will request {@link QueryRequest#limit()} times this factor rows from\n   * Cassandra indexes in attempts to return {@link QueryRequest#limit()} traces.\n   *\n   * <p>Indexing in cassandra will usually have more rows than trace identifiers due to factors\n   * including table design and collection implementation. As there's no way to DISTINCT out\n   * duplicates server-side, this over-fetches client-side when {@code indexFetchMultiplier} &gt;\n   * 1.\n   */\n  public B indexFetchMultiplier(int indexFetchMultiplier) {\n    if (indexFetchMultiplier <= 0) throw new IllegalArgumentException(\"indexFetchMultiplier <= 0\");\n    this.indexFetchMultiplier = indexFetchMultiplier;\n    return (B) this;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/CassandraUtil.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.math.BigInteger;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.ZoneOffset;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Annotation;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.internal.DateUtil;\nimport zipkin2.internal.Nullable;\nimport zipkin2.internal.RecyclableBuffers;\nimport zipkin2.storage.QueryRequest;\n\nimport static zipkin2.internal.RecyclableBuffers.SHORT_STRING_LENGTH;\n\nfinal class CassandraUtil {\n  static final Logger LOG = LoggerFactory.getLogger(CassandraUtil.class);\n\n  /**\n   * Time window covered by a single bucket of the {@link Schema#TABLE_TRACE_BY_SERVICE_SPAN} and\n   * {@link Schema#TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE}, in seconds. Default: 1 day\n   */\n  private static final long DURATION_INDEX_BUCKET_WINDOW_SECONDS =\n    Long.getLong(\"zipkin.store.cassandra.internal.durationIndexBucket\", 24 * 60 * 60);\n\n  public static int durationIndexBucket(long ts_micro) {\n    // if the window constant has microsecond precision, the division produces negative getValues\n    return (int) (ts_micro / (DURATION_INDEX_BUCKET_WINDOW_SECONDS * 1_000_000));\n  }\n\n  /**\n   * Returns a set of annotation getValues and tags joined on equals, delimited by ░\n   *\n   * <p>Values over {@link RecyclableBuffers#SHORT_STRING_LENGTH} are not considered. Zipkin's\n   * {@link QueryRequest#annotationQuery()} are equals match. Not all values are lookup values. For\n   * example, {@code sql.query} isn't something that is likely to be looked up by value and indexing\n   * that could add a kilobyte partition key on {@link Schema#TABLE_SPAN}\n   *\n   * @see QueryRequest#annotationQuery()\n   */\n  @Nullable static String annotationQuery(Span span) {\n    if (span.annotations().isEmpty() && span.tags().isEmpty()) return null;\n\n    char delimiter = '░'; // as very unlikely to be in the query\n    StringBuilder result = new StringBuilder().append(delimiter);\n    for (Annotation a : span.annotations()) {\n      if (a.value().length() > SHORT_STRING_LENGTH) continue;\n\n      result.append(a.value()).append(delimiter);\n    }\n\n    for (Map.Entry<String, String> tag : span.tags().entrySet()) {\n      if (tag.getValue().length() > SHORT_STRING_LENGTH) continue;\n\n      result.append(tag.getKey()).append(delimiter); // search is possible by key alone\n      result.append(tag.getKey()).append('=').append(tag.getValue()).append(delimiter);\n    }\n    return result.length() == 1 ? null : result.toString();\n  }\n\n  static List<String> annotationKeys(QueryRequest request) {\n    Set<String> annotationKeys = new LinkedHashSet<>();\n    for (Map.Entry<String, String> e : request.annotationQuery().entrySet()) {\n      if (e.getValue().isEmpty()) {\n        annotationKeys.add(e.getKey());\n      } else {\n        annotationKeys.add(e.getKey() + \"=\" + e.getValue());\n      }\n    }\n    return new ArrayList<>(annotationKeys);\n  }\n\n  static Call.Mapper<Map<String, Long>, Set<String>> traceIdsSortedByDescTimestamp() {\n    return TraceIdsSortedByDescTimestamp.INSTANCE;\n  }\n\n  enum TraceIdsSortedByDescTimestamp implements Call.Mapper<Map<String, Long>, Set<String>> {\n    INSTANCE;\n\n    @Override public Set<String> map(Map<String, Long> map) {\n      // timestamps can collide, so we need to add some random digits on end before using them as\n      // serviceSpanKeys\n      TreeMap<BigInteger, String> sorted = new TreeMap<>(Collections.reverseOrder());\n      for (Map.Entry<String, Long> entry : map.entrySet()) {\n        BigInteger uncollided =\n          BigInteger.valueOf(entry.getValue())\n            .multiply(OFFSET)\n            .add(BigInteger.valueOf(RAND.nextInt() & Integer.MAX_VALUE));\n        sorted.put(uncollided, entry.getKey());\n      }\n      return new LinkedHashSet<>(sorted.values());\n    }\n\n    @Override public String toString() {\n      return \"TraceIdsSortedByDescTimestamp\";\n    }\n\n    private static final Random RAND = new Random(System.nanoTime());\n    private static final BigInteger OFFSET = BigInteger.valueOf(Integer.MAX_VALUE);\n  }\n\n  static List<LocalDate> getDays(long endTs, @Nullable Long lookback) {\n    List<LocalDate> result = new ArrayList<>();\n    for (long epochMillis : DateUtil.epochDays(endTs, lookback)) {\n      result.add(Instant.ofEpochMilli(epochMillis).atZone(ZoneOffset.UTC).toLocalDate());\n    }\n    return result;\n  }\n\n  @Nullable static InetAddress inetAddressOrNull(@Nullable String string, @Nullable byte[] bytes) {\n    try {\n      return bytes == null ? null : InetAddress.getByAddress(bytes);\n    } catch (UnknownHostException e) {\n      LOG.debug(\"InetAddress.getByAddress failed with input {}: {}\", string, e.getMessage(), e);\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/DefaultSessionFactory.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport zipkin2.storage.cassandra.internal.SessionBuilder;\n\nfinal class DefaultSessionFactory implements CassandraStorage.SessionFactory {\n  @Override public CqlSession create(CassandraStorage cassandra) {\n    return buildSession(cassandra);\n  }\n\n  static CqlSession buildSession(CassandraStorage cassandra) {\n    return SessionBuilder.buildSession(\n      cassandra.contactPoints,\n      cassandra.localDc,\n      cassandra.poolingOptions,\n      cassandra.authProvider,\n      cassandra.useSsl,\n      cassandra.sslHostnameValidation\n    );\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/EndpointCodec.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.data.UdtValue;\nimport com.datastax.oss.driver.api.core.type.UserDefinedType;\nimport com.datastax.oss.driver.api.core.type.codec.MappingCodec;\nimport com.datastax.oss.driver.api.core.type.codec.TypeCodec;\nimport com.datastax.oss.driver.api.core.type.reflect.GenericType;\nimport zipkin2.Endpoint;\nimport zipkin2.internal.Nullable;\n\nimport static zipkin2.storage.cassandra.CassandraUtil.inetAddressOrNull;\n\nfinal class EndpointCodec extends MappingCodec<UdtValue, Endpoint> {\n\n  EndpointCodec(TypeCodec<UdtValue> innerCodec) {\n    super(innerCodec, GenericType.of(Endpoint.class));\n  }\n\n  @Override public UserDefinedType getCqlType() {\n    return (UserDefinedType) super.getCqlType();\n  }\n\n  @Nullable @Override protected Endpoint innerToOuter(@Nullable UdtValue value) {\n    if (value == null) return null;\n    Endpoint.Builder builder =\n      Endpoint.newBuilder().serviceName(value.getString(\"service\")).port(value.getInt(\"port\"));\n    builder.parseIp(value.getInetAddress(\"ipv4\"));\n    builder.parseIp(value.getInetAddress(\"ipv6\"));\n    return builder.build();\n  }\n\n  @Nullable @Override protected UdtValue outerToInner(@Nullable Endpoint endpoint) {\n    if (endpoint == null) return null;\n    UdtValue result = getCqlType().newValue();\n    result.setString(\"service\", endpoint.serviceName());\n    result.setInetAddress(\"ipv4\", inetAddressOrNull(endpoint.ipv4(), endpoint.ipv4Bytes()));\n    result.setInetAddress(\"ipv6\", inetAddressOrNull(endpoint.ipv6(), endpoint.ipv6Bytes()));\n    result.setInt(\"port\", endpoint.portAsInt());\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/InsertSpan.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.google.auto.value.AutoValue;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Annotation;\nimport zipkin2.Call;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\nfinal class InsertSpan extends ResultSetFutureCall<Void> {\n  @AutoValue abstract static class Input {\n    abstract UUID ts_uuid();\n\n    @Nullable abstract String trace_id_high();\n\n    abstract String trace_id();\n\n    @Nullable abstract String parent_id();\n\n    abstract String id();\n\n    @Nullable abstract String kind();\n\n    @Nullable abstract String span();\n\n    abstract long ts();\n\n    abstract long duration();\n\n    @Nullable abstract Endpoint l_ep();\n\n    @Nullable abstract Endpoint r_ep();\n\n    abstract List<Annotation> annotations();\n\n    abstract Map<String, String> tags();\n\n    @Nullable abstract String annotation_query();\n\n    abstract boolean debug();\n\n    abstract boolean shared();\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n    final boolean strictTraceId, searchEnabled;\n\n    Factory(CqlSession session, boolean strictTraceId, boolean searchEnabled) {\n      this.session = session;\n      String insertQuery = \"INSERT INTO \" + TABLE_SPAN\n        + \" (trace_id,trace_id_high,ts_uuid,parent_id,id,kind,span,ts,duration,l_ep,r_ep,annotations,tags,debug,shared)\"\n        + \" VALUES (:trace_id,:trace_id_high,:ts_uuid,:parent_id,:id,:kind,:span,:ts,:duration,:l_ep,:r_ep,:annotations,:tags,:debug,:shared)\";\n\n      if (searchEnabled) {\n        insertQuery = insertQuery.replace(\",shared)\", \",shared, l_service, annotation_query)\");\n        insertQuery = insertQuery.replace(\",:shared)\", \",:shared, :l_service, :annotation_query)\");\n      }\n\n      this.preparedStatement = session.prepare(insertQuery);\n      this.strictTraceId = strictTraceId;\n      this.searchEnabled = searchEnabled;\n    }\n\n    Input newInput(Span span, UUID ts_uuid) {\n      boolean traceIdHigh = !strictTraceId && span.traceId().length() == 32;\n      String annotation_query = searchEnabled ? CassandraUtil.annotationQuery(span) : null;\n      return new AutoValue_InsertSpan_Input(\n        ts_uuid,\n        traceIdHigh ? span.traceId().substring(0, 16) : null,\n        traceIdHigh ? span.traceId().substring(16) : span.traceId(),\n        span.parentId(),\n        span.id(),\n        span.kind() != null ? span.kind().name() : null,\n        span.name(),\n        span.timestampAsLong(),\n        span.durationAsLong(),\n        span.localEndpoint(),\n        span.remoteEndpoint(),\n        span.annotations(),\n        span.tags(),\n        annotation_query,\n        Boolean.TRUE.equals(span.debug()),\n        Boolean.TRUE.equals(span.shared()));\n    }\n\n    Call<Void> create(Input span) {\n      return new InsertSpan(this, span);\n    }\n  }\n\n  final Factory factory;\n  final Input input;\n\n  InsertSpan(Factory factory, Input input) {\n    this.factory = factory;\n    this.input = input;\n  }\n\n  /**\n   * TLDR: we are guarding against setting null, as doing so implies tombstones. We are dodging setX\n   * to keep code simpler than other alternatives described below.\n   *\n   * <p>If there's consistently 8 tombstones (nulls) per row, then we'll only need 125 spans in a\n   * trace (rows in a partition) to trigger the `tombstone_warn_threshold warnings being logged in\n   * the C* nodes. And if we go to 12500 spans in a trace then that whole trace partition would\n   * become unreadable. Cassandra warns at 1000 tombstones in any query, and fails on 100000\n   * tombstones.\n   *\n   * <p>There's also a small question about disk usage efficiency. Each tombstone is a cell name\n   * and basically empty cell value entry stored on disk. Given that the cells are, apart from tags\n   * and annotations, generally very small then this could be proportionally an unnecessary waste of\n   * disk.\n   *\n   * <p>To avoid this relying upon a number of variant prepared statements for inserting a span is\n   * the normal practice.\n   *\n   * <p>Another popular practice is to insert those potentially null columns as separate statements\n   * (and optionally put them together into UNLOGGED batches). This works as multiple writes to the\n   * same partition has little overhead, and here we're not worried about lack of isolation between\n   * writes, as it is asynchronous anyway. An example of this approach is in the cassandra-reaper\n   * project here: https://github.com/thelastpickle/cassandra-reaper/blob/master/src/server/src/main/java/io/cassandrareaper/storage/CassandraStorage.java#L622-L642\n   */\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    BoundStatementBuilder bound = factory.preparedStatement.boundStatementBuilder()\n      .setUuid(\"ts_uuid\", input.ts_uuid())\n      .setString(\"trace_id\", input.trace_id())\n      .setString(\"id\", input.id());\n\n    // Don't set null as we don't want to add tombstones\n    if (null != input.trace_id_high()) bound.setString(\"trace_id_high\", input.trace_id_high());\n    if (null != input.parent_id()) bound.setString(\"parent_id\", input.parent_id());\n    if (null != input.kind()) bound.setString(\"kind\", input.kind());\n    if (null != input.span()) bound.setString(\"span\", input.span());\n    if (0L != input.ts()) bound.setLong(\"ts\", input.ts());\n    if (0L != input.duration()) bound.setLong(\"duration\", input.duration());\n    if (null != input.l_ep()) bound.set(\"l_ep\", input.l_ep(), Endpoint.class);\n    if (null != input.r_ep()) bound.set(\"r_ep\", input.r_ep(), Endpoint.class);\n    if (!input.annotations().isEmpty()) {\n      bound.setList(\"annotations\", input.annotations(), Annotation.class);\n    }\n    if (!input.tags().isEmpty()) bound.setMap(\"tags\", input.tags(), String.class, String.class);\n    if (input.debug()) bound.setBoolean(\"debug\", true);\n    if (input.shared()) bound.setBoolean(\"shared\", true);\n\n    if (factory.searchEnabled) {\n      if (null != input.l_ep()) bound.setString(\"l_service\", input.l_ep().serviceName());\n      if (null != input.annotation_query()) {\n        bound.setString(\"annotation_query\", input.annotation_query());\n      }\n    }\n    return factory.session.executeAsync(bound.build());\n  }\n\n  @Override public Void map(AsyncResultSet input) {\n    return null;\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"InsertSpan\");\n  }\n\n  @Override public InsertSpan clone() {\n    return new InsertSpan(factory, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/InsertTraceByServiceRemoteService.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.google.auto.value.AutoValue;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE;\n\nfinal class InsertTraceByServiceRemoteService extends ResultSetFutureCall<Void> {\n  @AutoValue abstract static class Input {\n    abstract String service();\n\n    abstract String remote_service();\n\n    abstract int bucket();\n\n    abstract UUID ts();\n\n    abstract String trace_id();\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n    final boolean strictTraceId;\n\n    Factory(CqlSession session, boolean strictTraceId) {\n      this.session = session;\n      this.preparedStatement =\n        session.prepare(\"INSERT INTO \" + TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE\n          + \" (service,remote_service,bucket,ts,trace_id)\"\n          + \" VALUES (?,?,?,?,?)\");\n      this.strictTraceId = strictTraceId;\n    }\n\n    Input newInput(String service, String remote_service, int bucket, UUID ts, String trace_id) {\n      return new AutoValue_InsertTraceByServiceRemoteService_Input(\n        service,\n        remote_service,\n        bucket,\n        ts,\n        !strictTraceId && trace_id.length() == 32 ? trace_id.substring(16) : trace_id);\n    }\n\n    Call<Void> create(Input input) {\n      return new InsertTraceByServiceRemoteService(this, input);\n    }\n  }\n\n  final Factory factory;\n  final Input input;\n\n  InsertTraceByServiceRemoteService(Factory factory, Input input) {\n    this.factory = factory;\n    this.input = input;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setString(0, input.service())\n      .setString(1, input.remote_service())\n      .setInt(2, input.bucket())\n      .setUuid(3, input.ts())\n      .setString(4, input.trace_id()).build());\n  }\n\n  @Override public Void map(AsyncResultSet input) {\n    return null;\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"InsertTraceByServiceRemoteService\");\n  }\n\n  @Override public InsertTraceByServiceRemoteService clone() {\n    return new InsertTraceByServiceRemoteService(factory, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/InsertTraceByServiceSpan.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.google.auto.value.AutoValue;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_SPAN;\n\nfinal class InsertTraceByServiceSpan extends ResultSetFutureCall<Void> {\n  @AutoValue abstract static class Input {\n    abstract String service();\n\n    abstract String span();\n\n    abstract int bucket();\n\n    abstract UUID ts();\n\n    abstract String trace_id();\n\n    abstract long duration();\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n    final boolean strictTraceId;\n\n    Factory(CqlSession session, boolean strictTraceId) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"INSERT INTO \" + TABLE_TRACE_BY_SERVICE_SPAN\n        + \" (service,span,bucket,ts,trace_id,duration)\"\n        + \" VALUES (?,?,?,?,?,?)\");\n      this.strictTraceId = strictTraceId;\n    }\n\n    /**\n     * While {@link zipkin2.Span#duration()} cannot be zero, zero duration in milliseconds is\n     * permitted, as it implies the span took less than 1 millisecond (1-999us).\n     */\n    Input newInput(\n      String service, String span, int bucket, UUID ts, String trace_id, long durationMillis) {\n      return new AutoValue_InsertTraceByServiceSpan_Input(\n        service,\n        span,\n        bucket,\n        ts,\n        !strictTraceId && trace_id.length() == 32 ? trace_id.substring(16) : trace_id,\n        durationMillis);\n    }\n\n    Call<Void> create(Input input) {\n      return new InsertTraceByServiceSpan(this, input);\n    }\n  }\n\n  final Factory factory;\n  final Input input;\n\n  InsertTraceByServiceSpan(Factory factory, Input input) {\n    this.factory = factory;\n    this.input = input;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    BoundStatementBuilder bound = factory.preparedStatement.boundStatementBuilder()\n      .setString(0, input.service())\n      .setString(1, input.span())\n      .setInt(2, input.bucket())\n      .setUuid(3, input.ts())\n      .setString(4, input.trace_id());\n\n    if (0L != input.duration()) bound.setLong(5, input.duration());\n\n    return factory.session.executeAsync(bound.build());\n  }\n\n  @Override public Void map(AsyncResultSet input) {\n    return null;\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"InsertTraceByServiceSpan\");\n  }\n\n  @Override public InsertTraceByServiceSpan clone() {\n    return new InsertTraceByServiceSpan(factory, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/LazySession.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.function.BiFunction;\nimport zipkin2.internal.ClosedComponentException;\nimport zipkin2.storage.cassandra.CassandraStorage.SessionFactory;\n\nimport static zipkin2.Call.propagateIfFatal;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\nfinal class LazySession {\n  final CassandraStorage storage;\n  final SessionFactory sessionFactory;\n  final BiFunction<CassandraStorage, CqlSession, Schema.Metadata> ensureSchema;\n\n  volatile CqlSession session;\n  volatile PreparedStatement healthCheck; // guarded by session\n  volatile Schema.Metadata metadata; // guarded by session\n\n  LazySession(CassandraStorage storage, SessionFactory sessionFactory,\n    BiFunction<CassandraStorage, CqlSession, Schema.Metadata> ensureSchema) {\n    this.sessionFactory = sessionFactory;\n    this.ensureSchema = ensureSchema;\n    this.storage = storage;\n  }\n\n  /** Creates a session and ensures schema if configured. */\n  CqlSession get() {\n    if (session != null) return session;\n    synchronized (this) {\n      if (session != null) return session; // lost race\n      session = sessionFactory.create(storage);\n\n      // If we got this far, the session is healthy. So, everything below only happens once.\n      try {\n        metadata = ensureSchema.apply(storage, session);\n        session.execute(\"USE \" + storage.keyspace);\n        Schema.initializeUDTs(session, storage.keyspace);\n        healthCheck = session.prepare(\"SELECT trace_id FROM \" + TABLE_SPAN + \" limit 1\");\n      } catch (RuntimeException | Error e) {\n        propagateIfFatal(e);\n        // An error here was from installing or validating the schema. To ensure we don't repeat\n        // failed commands, close, but don't null the session. For example, repeating may look like\n        // an upgrade due to the first failure, and distract from the original problem.\n        session.close();\n      }\n    }\n    if (session.isClosed()) {\n      throw new ClosedComponentException(\"Session initialization failed. See server logs\");\n    }\n    return session;\n  }\n\n  Schema.Metadata metadata() {\n    get();\n    return metadata;\n  }\n\n  void healthCheck() {\n    get();\n    session.execute(healthCheck.bind());\n  }\n\n  void close() {\n    CqlSession maybeSession = session;\n    if (maybeSession != null) {\n      session.close();\n      session = null;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/Schema.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.Version;\nimport com.datastax.oss.driver.api.core.data.UdtValue;\nimport com.datastax.oss.driver.api.core.metadata.Node;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;\nimport com.datastax.oss.driver.api.core.type.codec.TypeCodec;\nimport com.datastax.oss.driver.api.core.type.codec.registry.MutableCodecRegistry;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport static zipkin2.storage.cassandra.internal.Resources.resourceToString;\n\nfinal class Schema {\n  static final Logger LOG = LoggerFactory.getLogger(Schema.class);\n\n  static final String TABLE_SPAN = \"span\";\n  static final String TABLE_TRACE_BY_SERVICE_SPAN = \"trace_by_service_span\";\n  static final String TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE = \"trace_by_service_remote_service\";\n  static final String TABLE_SERVICE_SPANS = \"span_by_service\";\n  static final String TABLE_SERVICE_REMOTE_SERVICES = \"remote_service_by_service\";\n  static final String TABLE_DEPENDENCY = \"dependency\";\n  static final String TABLE_AUTOCOMPLETE_TAGS = \"autocomplete_tags\";\n\n  static final String DEFAULT_KEYSPACE = \"zipkin2\";\n  static final String SCHEMA_RESOURCE = \"/zipkin2-schema.cql\";\n  static final String INDEX_RESOURCE = \"/zipkin2-schema-indexes.cql\";\n  static final String UPGRADE_1 = \"/zipkin2-schema-upgrade-1.cql\";\n  static final String UPGRADE_2 = \"/zipkin2-schema-upgrade-2.cql\";\n\n  static Metadata ensure(CassandraStorage cassandra, CqlSession session) {\n    String keyspace = cassandra.keyspace;\n    Schema.ensureExists(session, keyspace, cassandra.searchEnabled);\n    return validate(cassandra, session);\n  }\n\n  static Metadata validate(CassandraStorage cassandra, CqlSession session) {\n    String keyspace = cassandra.keyspace;\n    KeyspaceMetadata keyspaceMetadata = ensureKeyspaceMetadata(session, keyspace);\n\n    Map<String, String> replication = keyspaceMetadata.getReplication();\n    if (\"SimpleStrategy\".equals(replication.get(\"class\"))) {\n      if (\"1\".equals(replication.get(\"replication_factor\"))) {\n        LOG.warn(\"running with RF=1, this is not suitable for production. Optimal is 3+\");\n      }\n    }\n\n    boolean hasAutocompleteTags = hasUpgrade1_autocompleteTags(keyspaceMetadata);\n    boolean hasRemoteService = hasUpgrade2_remoteService(keyspaceMetadata);\n\n    Metadata md = new Metadata(hasAutocompleteTags, hasRemoteService);\n    // Begin validation of externally provided schema.\n    if (!has_schema(keyspaceMetadata)) {\n      logAndThrow(\"schema not installed: apply %s, or set CASSANDRA_ENSURE_SCHEMA=true\",\n        SCHEMA_RESOURCE);\n    }\n\n    if (!cassandra.searchEnabled) {\n      return md;\n    }\n\n    if (!has_indexing(keyspaceMetadata)) {\n      logAndThrow(\n        \"schema lacks indexing: apply %s, or set CASSANDRA_ENSURE_SCHEMA=true\", INDEX_RESOURCE);\n    }\n\n    // Don't throw on more esoteric features\n    if (!hasAutocompleteTags) {\n      LOG.warn(\n        \"schema lacks autocomplete indexing: apply {}, or set CASSANDRA_ENSURE_SCHEMA=true\",\n        UPGRADE_1);\n    }\n\n    if (!hasRemoteService) {\n      LOG.warn(\n        \"schema lacks remote service indexing: apply {}, or set CASSANDRA_ENSURE_SCHEMA=true\",\n        UPGRADE_2);\n    }\n\n    return md;\n  }\n\n  static void logAndThrow(String messageFormat, Object... args) {\n    String message = messageFormat.formatted(args);\n    // Ensure we can look at logs to see the problem. Otherwise, it may only\n    // be visible in API error responses, such as /health or /api/v2/traces.\n    LOG.error(message);\n    throw new RuntimeException(message);\n  }\n\n  static void initializeUDTs(CqlSession session, String keyspace) {\n    KeyspaceMetadata ks = session.getMetadata().getKeyspace(keyspace).get();\n    MutableCodecRegistry codecRegistry =\n      (MutableCodecRegistry) session.getContext().getCodecRegistry();\n\n    TypeCodec<UdtValue> annotationUDTCodec =\n      codecRegistry.codecFor(ks.getUserDefinedType(\"annotation\").get());\n    codecRegistry.register(new AnnotationCodec(annotationUDTCodec));\n\n    LOG.debug(\"Registering endpoint and annotation UDTs to keyspace {}\", keyspace);\n    TypeCodec<UdtValue> endpointUDTCodec =\n      codecRegistry.codecFor(ks.getUserDefinedType(\"endpoint\").get());\n    codecRegistry.register(new EndpointCodec(endpointUDTCodec));\n  }\n\n  static final class Metadata {\n    final boolean hasAutocompleteTags, hasRemoteService;\n\n    Metadata(boolean hasAutocompleteTags, boolean hasRemoteService) {\n      this.hasAutocompleteTags = hasAutocompleteTags;\n      this.hasRemoteService = hasRemoteService;\n    }\n  }\n\n  static KeyspaceMetadata ensureKeyspaceMetadata(CqlSession session, String keyspace) {\n    KeyspaceMetadata keyspaceMetadata = session.getMetadata().getKeyspace(keyspace).orElse(null);\n    if (keyspaceMetadata == null) {\n      throw new IllegalStateException(\n        \n          \"Cannot read keyspace metadata for keyspace: %s and cluster: %s\".formatted(\n          keyspace, session.getMetadata().getClusterName()));\n    }\n    return keyspaceMetadata;\n  }\n\n  static Version ensureVersion(com.datastax.oss.driver.api.core.metadata.Metadata metadata) {\n    Version version = null;\n    for (Map.Entry<UUID, Node> entry : metadata.getNodes().entrySet()) {\n      version = entry.getValue().getCassandraVersion();\n      if (version == null) throw new RuntimeException(\"node had no version: \" + entry.getValue());\n      if (Version.parse(\"3.11.3\").compareTo(version) > 0) {\n        throw new RuntimeException(\n          \"Node %s is running Cassandra %s, but minimum version is 3.11.3\".formatted(\n          entry.getKey(), entry.getValue().getCassandraVersion()));\n      }\n    }\n    if (version == null) throw new RuntimeException(\"No nodes in the cluster\");\n    LOG.info(\"Detected Cassandra version {}\", version);\n    return version;\n  }\n\n  static void ensureExists(CqlSession session, String keyspace, boolean searchEnabled) {\n    KeyspaceMetadata result = session.getMetadata().getKeyspace(keyspace).orElse(null);\n    Version version = ensureVersion(session.getMetadata());\n    if (result == null || result.getTable(Schema.TABLE_SPAN).isEmpty()) {\n      LOG.info(\"Installing schema {} for keyspace {}\", SCHEMA_RESOURCE, keyspace);\n      applyCqlFile(version, keyspace, session, SCHEMA_RESOURCE);\n      if (searchEnabled) {\n        LOG.info(\"Installing indexes {} for keyspace {}\", INDEX_RESOURCE, keyspace);\n        applyCqlFile(version, keyspace, session, INDEX_RESOURCE);\n      }\n    } else if (searchEnabled) { // prior installation\n      if (!hasUpgrade1_autocompleteTags(result)) {\n        LOG.info(\"Upgrading schema {}\", UPGRADE_1);\n        applyCqlFile(version, keyspace, session, UPGRADE_1);\n      }\n      if (!hasUpgrade2_remoteService(result)) {\n        LOG.info(\"Upgrading schema {}\", UPGRADE_2);\n        applyCqlFile(version, keyspace, session, UPGRADE_2);\n      }\n    }\n  }\n\n  static boolean has_schema(KeyspaceMetadata keyspaceMetadata) {\n    return keyspaceMetadata.getTable(TABLE_SPAN).isPresent();\n  }\n\n  static boolean has_indexing(KeyspaceMetadata keyspaceMetadata) {\n    return keyspaceMetadata.getTable(TABLE_SERVICE_SPANS).isPresent();\n  }\n\n  static boolean hasUpgrade1_autocompleteTags(KeyspaceMetadata keyspaceMetadata) {\n    return keyspaceMetadata.getTable(TABLE_AUTOCOMPLETE_TAGS).isPresent();\n  }\n\n  static boolean hasUpgrade2_remoteService(KeyspaceMetadata keyspaceMetadata) {\n    return keyspaceMetadata.getTable(TABLE_SERVICE_REMOTE_SERVICES).isPresent();\n  }\n\n  static void applyCqlFile(Version version, String keyspace, CqlSession session, String resource) {\n    for (String cmd : resourceToString(resource).split(\";\", 100)) {\n      cmd = cmd.trim().replace(\" \" + DEFAULT_KEYSPACE, \" \" + keyspace);\n      if (cmd.isEmpty()) continue;\n      cmd = reviseCQL(version, cmd);\n      try {\n        session.execute(cmd);\n      } catch (InvalidQueryException e) {\n        // Add context so it is obvious which line was wrong\n        String message = \"Failed to execute [%s]: %s\".formatted(cmd, e.getMessage());\n        // Ensure we can look at logs to see the problem. Otherwise, it may only\n        // be visible in API error responses, such as /health or /api/v2/traces.\n        LOG.error(message);\n        throw new RuntimeException(message, e);\n      }\n    }\n  }\n\n  static String reviseCQL(Version version, String cql) {\n    if (version.getMajor() >= 4) {\n      // read_repair_chance options were removed and make Cassandra crash starting in v4\n      // See https://cassandra.apache.org/doc/latest/operating/read_repair.html#background-read-repair\n      cql = cql.replaceAll(\" *AND [^\\\\s]*read_repair_chance = 0\\n\", \"\");\n    }\n    return cql;\n  }\n\n  Schema() {\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectAutocompleteValues.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.List;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.DistinctSortedStrings;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_AUTOCOMPLETE_TAGS;\n\nfinal class SelectAutocompleteValues extends ResultSetFutureCall<AsyncResultSet> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT value\"\n        + \" FROM \" + TABLE_AUTOCOMPLETE_TAGS\n        + \" WHERE key=?\"\n        + \" LIMIT \" + 10000);\n    }\n\n    Call<List<String>> create(String key) {\n      return new SelectAutocompleteValues(this, key).flatMap(DistinctSortedStrings.get());\n    }\n  }\n\n  final SelectAutocompleteValues.Factory factory;\n  final String key;\n\n  SelectAutocompleteValues(SelectAutocompleteValues.Factory factory, String key) {\n    this.factory = factory;\n    this.key = key;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setString(0, key).build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public Call<AsyncResultSet> clone() {\n    return new SelectAutocompleteValues(factory, key);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectDependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.internal.DependencyLinker;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_DEPENDENCY;\n\nfinal class SelectDependencies extends ResultSetFutureCall<List<DependencyLink>> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT parent,child,errors,calls\"\n        + \" FROM \" + TABLE_DEPENDENCY\n        + \" WHERE day IN ?\");\n    }\n\n    Call<List<DependencyLink>> create(long endTs, long lookback) {\n      List<LocalDate> days = CassandraUtil.getDays(endTs, lookback);\n      return new SelectDependencies(this, days);\n    }\n  }\n\n  final Factory factory;\n  final List<LocalDate> days;\n\n  SelectDependencies(Factory factory, List<LocalDate> days) {\n    this.factory = factory;\n    this.days = days;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setList(0, days, LocalDate.class).build());\n  }\n\n  @Override public String toString() {\n    return \"SelectDependencies{days=\" + days + \"}\";\n  }\n\n  @Override public SelectDependencies clone() {\n    return new SelectDependencies(factory, days);\n  }\n\n  @Override public List<DependencyLink> map(AsyncResultSet rs) {\n    List<DependencyLink> unmerged = new ArrayList<>();\n    for (Row row : rs.currentPage()) {\n      unmerged.add(DependencyLink.newBuilder()\n        .parent(row.getString(\"parent\"))\n        .child(row.getString(\"child\"))\n        .errorCount(row.getLong(\"errors\"))\n        .callCount(row.getLong(\"calls\"))\n        .build());\n    }\n    return DependencyLinker.merge(unmerged);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectFromSpan.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletionStage;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\nimport zipkin2.Annotation;\nimport zipkin2.Call;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.FilterTraces;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.GroupByTraceId;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.StrictTraceId;\nimport zipkin2.storage.cassandra.internal.call.AccumulateAllResults;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\nfinal class SelectFromSpan extends ResultSetFutureCall<AsyncResultSet> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n    final Call.Mapper<List<Span>, List<List<Span>>> groupByTraceId;\n    final boolean strictTraceId;\n    final int maxTraceCols;\n\n    Factory(CqlSession session, boolean strictTraceId, int maxTraceCols) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\n        \"SELECT trace_id_high,trace_id,parent_id,id,kind,span,ts,duration,l_ep,r_ep,annotations,tags,debug,shared\"\n          + \" FROM \" + TABLE_SPAN\n          + \" WHERE trace_id IN ?\"\n          + \" LIMIT ?\");\n      this.strictTraceId = strictTraceId;\n      this.maxTraceCols = maxTraceCols;\n      this.groupByTraceId = GroupByTraceId.create(strictTraceId);\n    }\n\n    Call<List<Span>> newCall(String hexTraceId) {\n      // Unless we are strict, truncate the trace ID to 64bit (encoded as 16 characters)\n      Set<String> traceIds;\n      if (!strictTraceId && hexTraceId.length() == 32) {\n        traceIds = Set.of(hexTraceId, hexTraceId.substring(16));\n      } else {\n        traceIds = Set.of(hexTraceId);\n      }\n\n      Call<List<Span>> result =\n        new SelectFromSpan(this, traceIds, maxTraceCols).flatMap(READ_SPANS);\n      return strictTraceId ? result.map(StrictTraceId.filterSpans(hexTraceId)) : result;\n    }\n\n    Call<List<List<Span>>> newCall(Iterable<String> traceIds) {\n      Set<String> normalizedTraceIds = new LinkedHashSet<>();\n      for (String traceId : traceIds) {\n        // make sure we have a 16 or 32 character trace ID\n        traceId = Span.normalizeTraceId(traceId);\n        // Unless we are strict, truncate the trace ID to 64bit (encoded as 16 characters)\n        if (!strictTraceId && traceId.length() == 32) traceId = traceId.substring(16);\n        normalizedTraceIds.add(traceId);\n      }\n\n      if (normalizedTraceIds.isEmpty()) return Call.emptyList();\n      Call<List<List<Span>>> result = new SelectFromSpan(this, normalizedTraceIds, maxTraceCols)\n        .flatMap(READ_SPANS)\n        .map(groupByTraceId);\n      return strictTraceId ? result.map(StrictTraceId.filterTraces(normalizedTraceIds)) : result;\n    }\n\n    FlatMapper<Set<String>, List<List<Span>>> newFlatMapper(QueryRequest request) {\n      return new SelectSpansByTraceIds(this, request);\n    }\n  }\n\n  final Factory factory;\n  final Set<String> trace_id;\n  final int limit_;\n\n  /** @param limit_ amount of spans per trace is almost always larger than trace IDs */\n  SelectFromSpan(Factory factory, Set<String> trace_id, int limit_) {\n    this.factory = factory;\n    this.trace_id = trace_id;\n    this.limit_ = limit_;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      // Switched Set to List which is higher overhead, as have to copy into it, but avoids this:\n      // com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException: Codec not found for requested operation: [List(TEXT, not frozen) <-> java.util.Set<java.lang.String>]\n      .setList(0, new ArrayList<>(trace_id), String.class)\n      .setInt(1, limit_).build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return \"SelectFromSpan{trace_id=\" + trace_id + \", limit_=\" + limit_ + \"}\";\n  }\n\n  @Override public SelectFromSpan clone() {\n    return new SelectFromSpan(factory, trace_id, limit_);\n  }\n\n  static final class SelectSpansByTraceIds implements FlatMapper<Set<String>, List<List<Span>>> {\n    final Factory factory;\n    final int limit;\n    @Nullable final Call.Mapper<List<List<Span>>, List<List<Span>>> filter;\n\n    SelectSpansByTraceIds(Factory factory, QueryRequest request) {\n      this.factory = factory;\n      this.limit = request.limit();\n      // Cassandra always looks up traces by 64-bit trace ID, so we have to unconditionally filter\n      // when strict trace ID is enabled.\n      this.filter = factory.strictTraceId ? FilterTraces.create(request) : null;\n    }\n\n    @Override public Call<List<List<Span>>> map(Set<String> input) {\n      if (input.isEmpty()) return Call.emptyList();\n      Set<String> traceIds;\n      if (input.size() > limit) {\n        traceIds = new LinkedHashSet<>();\n        Iterator<String> iterator = input.iterator();\n        for (int i = 0; i < limit; i++) {\n          traceIds.add(iterator.next());\n        }\n      } else {\n        traceIds = input;\n      }\n      Call<List<List<Span>>> result = new SelectFromSpan(factory, traceIds, factory.maxTraceCols)\n        .flatMap(READ_SPANS)\n        .map(factory.groupByTraceId);\n      return filter != null ? result.map(filter) : result;\n    }\n\n    @Override public String toString() {\n      return \"SelectSpansByTraceIds{limit=\" + limit + \"}\";\n    }\n  }\n\n  static final AccumulateAllResults<List<Span>> READ_SPANS = new ReadSpans();\n\n  static final class ReadSpans extends AccumulateAllResults<List<Span>> {\n\n    @Override protected Supplier<List<Span>> supplier() {\n      return ArrayList::new;\n    }\n\n    @Override protected BiConsumer<Row, List<Span>> accumulator() {\n      return (row, result) -> {\n        String traceId = row.getString(\"trace_id\");\n        String traceIdHigh = row.getString(\"trace_id_high\");\n        if (traceIdHigh != null) traceId = traceIdHigh + traceId;\n        Span.Builder builder = Span.newBuilder()\n          .traceId(traceId)\n          .parentId(row.getString(\"parent_id\"))\n          .id(row.getString(\"id\"))\n          .name(row.getString(\"span\"));\n\n        if (!row.isNull(\"ts\")) builder.timestamp(row.getLong(\"ts\"));\n        if (!row.isNull(\"duration\")) builder.duration(row.getLong(\"duration\"));\n\n        if (!row.isNull(\"kind\")) {\n          try {\n            builder.kind(Span.Kind.valueOf(row.getString(\"kind\")));\n          } catch (IllegalArgumentException ignored) {\n            // EmptyCatch ignored\n          }\n        }\n\n        if (!row.isNull(\"l_ep\")) builder.localEndpoint(row.get(\"l_ep\", Endpoint.class));\n        if (!row.isNull(\"r_ep\")) builder.remoteEndpoint(row.get(\"r_ep\", Endpoint.class));\n\n        if (!row.isNull(\"debug\")) builder.debug(row.getBoolean(\"debug\"));\n        if (!row.isNull(\"shared\")) builder.shared(row.getBoolean(\"shared\"));\n\n        for (Annotation annotation : row.getList(\"annotations\", Annotation.class)) {\n          builder.addAnnotation(annotation.timestamp(), annotation.value());\n        }\n        for (Map.Entry<String, String> tag :\n          row.getMap(\"tags\", String.class, String.class).entrySet()) {\n          builder.putTag(tag.getKey(), tag.getValue());\n        }\n        result.add(builder.build());\n      };\n    }\n\n    @Override public String toString() {\n      return \"ReadSpans{}\";\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectRemoteServiceNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.DistinctSortedStrings;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_REMOTE_SERVICES;\n\nfinal class SelectRemoteServiceNames extends ResultSetFutureCall<AsyncResultSet> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT remote_service\"\n        + \" FROM \" + TABLE_SERVICE_REMOTE_SERVICES\n        + \" WHERE service=?\"\n        + \" LIMIT \" + 1000);\n    }\n\n    Call<List<String>> create(String serviceName) {\n      if (serviceName == null || serviceName.isEmpty()) return Call.emptyList();\n      String service = serviceName.toLowerCase(Locale.ROOT); // service names are always lowercase!\n      return new SelectRemoteServiceNames(this, service).flatMap(DistinctSortedStrings.get());\n    }\n  }\n\n  final Factory factory;\n  final String service;\n\n  SelectRemoteServiceNames(Factory factory, String service) {\n    this.factory = factory;\n    this.service = service;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setString(0, service).build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return \"SelectSpanNames{service=\" + service + \"}\";\n  }\n\n  @Override public SelectRemoteServiceNames clone() {\n    return new SelectRemoteServiceNames(factory, service);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectServiceNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.List;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.DistinctSortedStrings;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_SPANS;\n\nfinal class SelectServiceNames extends ResultSetFutureCall<AsyncResultSet> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT DISTINCT service\"\n        + \" FROM \" + TABLE_SERVICE_SPANS);\n    }\n\n    Call<List<String>> create() {\n      return new SelectServiceNames(this).flatMap(DistinctSortedStrings.get());\n    }\n  }\n\n  final Factory factory;\n\n  SelectServiceNames(Factory factory) {\n    this.factory = factory;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.bind());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return \"SelectServiceNames{}\";\n  }\n\n  @Override public SelectServiceNames clone() {\n    return new SelectServiceNames(factory);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectSpanNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.internal.call.DistinctSortedStrings;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_SPANS;\n\nfinal class SelectSpanNames extends ResultSetFutureCall<AsyncResultSet> {\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT span\"\n        + \" FROM \" + TABLE_SERVICE_SPANS\n        + \" WHERE service=?\"\n        + \" LIMIT \" + 10000);\n    }\n\n    Call<List<String>> create(String serviceName) {\n      if (serviceName == null || serviceName.isEmpty()) return Call.emptyList();\n      String service = serviceName.toLowerCase(Locale.ROOT); // service names are always lowercase!\n      return new SelectSpanNames(this, service).flatMap(DistinctSortedStrings.get());\n    }\n  }\n\n  final Factory factory;\n  final String service;\n\n  SelectSpanNames(Factory factory, String service) {\n    this.factory = factory;\n    this.service = service;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setString(0, service).build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return \"SelectSpanNames{service=\" + service + \"}\";\n  }\n\n  @Override public SelectSpanNames clone() {\n    return new SelectSpanNames(factory, service);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectTraceIdsFromServiceRemoteService.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.google.auto.value.AutoValue;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.storage.cassandra.CassandraSpanStore.TimestampRange;\nimport zipkin2.storage.cassandra.internal.call.AccumulateTraceIdTsUuid;\nimport zipkin2.storage.cassandra.internal.call.AggregateIntoMap;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE;\n\nfinal class SelectTraceIdsFromServiceRemoteService extends ResultSetFutureCall<AsyncResultSet> {\n  @AutoValue abstract static class Input {\n    abstract String service();\n\n    abstract String remote_service();\n\n    abstract int bucket();\n\n    abstract UUID start_ts();\n\n    abstract UUID end_ts();\n\n    abstract int limit_();\n\n    Input withService(String service) {\n      return new AutoValue_SelectTraceIdsFromServiceRemoteService_Input(\n        service,\n        remote_service(),\n        bucket(),\n        start_ts(),\n        end_ts(),\n        limit_());\n    }\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      this.preparedStatement = session.prepare(\"SELECT trace_id,ts\"\n        + \" FROM \" + TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE\n        + \" WHERE service=? AND remote_service=?\"\n        + \" AND bucket=?\"\n        + \" AND ts>=?\"\n        + \" AND ts<=?\"\n        + \" LIMIT ?\");\n    }\n\n    Input newInput(\n      String serviceName,\n      String remoteServiceName,\n      int bucket,\n      TimestampRange timestampRange,\n      int limit) {\n      return new AutoValue_SelectTraceIdsFromServiceRemoteService_Input(\n        serviceName,\n        remoteServiceName,\n        bucket,\n        timestampRange.startUUID,\n        timestampRange.endUUID,\n        limit);\n    }\n\n    Call<Map<String, Long>> newCall(List<Input> inputs) {\n      if (inputs.isEmpty()) return Call.create(Map.of());\n      if (inputs.size() == 1) return newCall(inputs.get(0));\n\n      List<Call<Map<String, Long>>> bucketedTraceIdCalls = new ArrayList<>();\n      for (SelectTraceIdsFromServiceRemoteService.Input input : inputs) {\n        bucketedTraceIdCalls.add(newCall(input));\n      }\n      return new AggregateIntoMap<>(bucketedTraceIdCalls);\n    }\n\n    Call<Map<String, Long>> newCall(Input input) {\n      return new SelectTraceIdsFromServiceRemoteService(this, preparedStatement, input)\n        .flatMap(AccumulateTraceIdTsUuid.get());\n    }\n\n    /** Applies all deferred service names to all input templates */\n    FlatMapper<List<String>, Map<String, Long>> newFlatMapper(List<Input> inputTemplates) {\n      return new FlatMapServicesToInputs(inputTemplates);\n    }\n\n    class FlatMapServicesToInputs implements FlatMapper<List<String>, Map<String, Long>> {\n      final List<Input> inputTemplates;\n\n      FlatMapServicesToInputs(List<Input> inputTemplates) {\n        this.inputTemplates = inputTemplates;\n      }\n\n      @Override public Call<Map<String, Long>> map(List<String> serviceNames) {\n        List<Call<Map<String, Long>>> bucketedTraceIdCalls = new ArrayList<>();\n\n        for (String service : serviceNames) { // fan out every input for each service name\n          List<Input> scopedInputs = new ArrayList<>();\n          for (Input input : inputTemplates) {\n            scopedInputs.add(input.withService(service));\n          }\n          bucketedTraceIdCalls.add(newCall(scopedInputs));\n        }\n\n        if (bucketedTraceIdCalls.isEmpty()) return Call.create(Map.of());\n        if (bucketedTraceIdCalls.size() == 1) return bucketedTraceIdCalls.get(0);\n        return new AggregateIntoMap<>(bucketedTraceIdCalls);\n      }\n\n      @Override public String toString() {\n        List<String> inputs = new ArrayList<>();\n        for (Input input : inputTemplates) {\n          inputs.add(input.toString().replace(\"Input\", \"SelectTraceIdsFromServiceRemoteService\"));\n        }\n        return \"FlatMapServicesToInputs{\" + inputs + \"}\";\n      }\n    }\n  }\n\n  final Factory factory;\n  final PreparedStatement preparedStatement;\n  final Input input;\n\n  SelectTraceIdsFromServiceRemoteService(Factory factory, PreparedStatement preparedStatement,\n    Input input) {\n    this.factory = factory;\n    this.preparedStatement = preparedStatement;\n    this.input = input;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(preparedStatement.boundStatementBuilder()\n      .setString(0, input.service())\n      .setString(1, input.remote_service())\n      .setInt(2, input.bucket())\n      .setUuid(3, input.start_ts())\n      .setUuid(4, input.end_ts())\n      .setInt(5, input.limit_())\n      .setPageSize(input.limit_()).build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"SelectTraceIdsFromServiceRemoteService\");\n  }\n\n  @Override public SelectTraceIdsFromServiceRemoteService clone() {\n    return new SelectTraceIdsFromServiceRemoteService(factory, preparedStatement, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectTraceIdsFromServiceSpan.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.google.auto.value.AutoValue;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.cassandra.CassandraSpanStore.TimestampRange;\nimport zipkin2.storage.cassandra.internal.call.AccumulateTraceIdTsUuid;\nimport zipkin2.storage.cassandra.internal.call.AggregateIntoMap;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_SPAN;\n\nfinal class SelectTraceIdsFromServiceSpan extends ResultSetFutureCall<AsyncResultSet> {\n  @AutoValue abstract static class Input {\n    abstract String service();\n\n    abstract String span();\n\n    abstract int bucket();\n\n    @Nullable abstract Long start_duration();\n\n    @Nullable abstract Long end_duration();\n\n    abstract UUID start_ts();\n\n    abstract UUID end_ts();\n\n    abstract int limit_();\n\n    Input withService(String service) {\n      return new AutoValue_SelectTraceIdsFromServiceSpan_Input(\n        service,\n        span(),\n        bucket(),\n        start_duration(),\n        end_duration(),\n        start_ts(),\n        end_ts(),\n        limit_());\n    }\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement selectTraceIdsByServiceSpanName;\n    final PreparedStatement selectTraceIdsByServiceSpanNameAndDuration;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      String baseQuery = \"SELECT trace_id,ts\"\n        + \" FROM \" + TABLE_TRACE_BY_SERVICE_SPAN\n        + \" WHERE service=?\"\n        + \" AND span=?\"\n        + \" AND bucket=?\"\n        + \" AND ts>=?\"\n        + \" AND ts<=?\";\n      this.selectTraceIdsByServiceSpanName = session.prepare(baseQuery\n        + \" LIMIT ?\");\n      this.selectTraceIdsByServiceSpanNameAndDuration = session.prepare(baseQuery\n        + \" AND duration>=?\"\n        + \" AND duration<=?\"\n        + \" LIMIT ?\");\n    }\n\n    Input newInput(\n      String serviceName,\n      String spanName,\n      int bucket,\n      @Nullable Long minDurationMicros,\n      @Nullable Long maxDurationMicros,\n      TimestampRange timestampRange,\n      int limit) {\n      Long start_duration = null, end_duration = null;\n      if (minDurationMicros != null) {\n        start_duration = minDurationMicros / 1000L;\n        end_duration = maxDurationMicros != null ? maxDurationMicros / 1000L : Long.MAX_VALUE;\n      }\n      return new AutoValue_SelectTraceIdsFromServiceSpan_Input(\n        serviceName,\n        spanName,\n        bucket,\n        start_duration,\n        end_duration,\n        timestampRange.startUUID,\n        timestampRange.endUUID,\n        limit);\n    }\n\n    Call<Map<String, Long>> newCall(List<Input> inputs) {\n      if (inputs.isEmpty()) return Call.create(Map.of());\n      if (inputs.size() == 1) return newCall(inputs.get(0));\n\n      List<Call<Map<String, Long>>> bucketedTraceIdCalls = new ArrayList<>();\n      for (SelectTraceIdsFromServiceSpan.Input input : inputs) {\n        bucketedTraceIdCalls.add(newCall(input));\n      }\n      return new AggregateIntoMap<>(bucketedTraceIdCalls);\n    }\n\n    Call<Map<String, Long>> newCall(Input input) {\n      PreparedStatement preparedStatement = input.start_duration() != null\n        ? selectTraceIdsByServiceSpanNameAndDuration\n        : selectTraceIdsByServiceSpanName;\n      return new SelectTraceIdsFromServiceSpan(this, preparedStatement, input)\n        .flatMap(AccumulateTraceIdTsUuid.get());\n    }\n\n    /** Applies all deferred service names to all input templates */\n    FlatMapper<List<String>, Map<String, Long>> newFlatMapper(List<Input> inputTemplates) {\n      return new FlatMapServicesToInputs(inputTemplates);\n    }\n\n    class FlatMapServicesToInputs implements FlatMapper<List<String>, Map<String, Long>> {\n      final List<SelectTraceIdsFromServiceSpan.Input> inputTemplates;\n\n      FlatMapServicesToInputs(List<SelectTraceIdsFromServiceSpan.Input> inputTemplates) {\n        this.inputTemplates = inputTemplates;\n      }\n\n      @Override public Call<Map<String, Long>> map(List<String> serviceNames) {\n        List<Call<Map<String, Long>>> bucketedTraceIdCalls = new ArrayList<>();\n\n        for (String service : serviceNames) { // fan out every input for each service name\n          List<SelectTraceIdsFromServiceSpan.Input> scopedInputs = new ArrayList<>();\n          for (SelectTraceIdsFromServiceSpan.Input input : inputTemplates) {\n            scopedInputs.add(input.withService(service));\n          }\n          bucketedTraceIdCalls.add(newCall(scopedInputs));\n        }\n\n        if (bucketedTraceIdCalls.isEmpty()) return Call.create(Map.of());\n        if (bucketedTraceIdCalls.size() == 1) return bucketedTraceIdCalls.get(0);\n        return new AggregateIntoMap<>(bucketedTraceIdCalls);\n      }\n\n      @Override public String toString() {\n        List<String> inputs = new ArrayList<>();\n        for (Input input : inputTemplates) {\n          inputs.add(input.toString().replace(\"Input\", \"SelectTraceIdsFromServiceSpan\"));\n        }\n        return \"FlatMapServicesToInputs{\" + inputs + \"}\";\n      }\n    }\n  }\n\n  final Factory factory;\n  final PreparedStatement preparedStatement;\n  final Input input;\n\n  SelectTraceIdsFromServiceSpan(Factory factory, PreparedStatement preparedStatement, Input input) {\n    this.factory = factory;\n    this.preparedStatement = preparedStatement;\n    this.input = input;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    int i = 0;\n    BoundStatementBuilder bound = preparedStatement.boundStatementBuilder()\n      .setString(i++, input.service())\n      .setString(i++, input.span())\n      .setInt(i++, input.bucket())\n      .setUuid(i++, input.start_ts())\n      .setUuid(i++, input.end_ts());\n\n    if (input.start_duration() != null) {\n      bound.setLong(i++, input.start_duration());\n      bound.setLong(i++, input.end_duration());\n    }\n\n    bound\n      .setInt(i, input.limit_())\n      .setPageSize(input.limit_());\n\n    return factory.session.executeAsync(bound.build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"SelectTraceIdsFromServiceSpan\");\n  }\n\n  @Override public SelectTraceIdsFromServiceSpan clone() {\n    return new SelectTraceIdsFromServiceSpan(factory, preparedStatement, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/SelectTraceIdsFromSpan.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport com.google.auto.value.AutoValue;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletionStage;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\nimport zipkin2.Call;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.cassandra.CassandraSpanStore.TimestampRange;\nimport zipkin2.storage.cassandra.internal.call.AccumulateAllResults;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\n/**\n * Selects from the {@link Schema#TABLE_SPAN} using data in the partition key or SASI indexes.\n *\n * <p>Note: While queries here use \"ALLOW FILTERING\", they do so within a SASI clause, and only\n * return (traceId, timestamp) tuples. This means the entire spans table is not scanned, unless the\n * time range implies that.\n *\n * <p>The spans table is sorted descending by timestamp. When a query includes only a time range,\n * the first N rows are already in the correct order. However, the cardinality of rows is a function\n * of span count, not trace count. This implies an over-fetch function based on average span count\n * per trace in order to achieve N distinct trace IDs. For example if there are 3 spans per trace,\n * and over-fetch function of 3 * intended limit will work. See {@link\n * CassandraStorage#indexFetchMultiplier} for an associated parameter.\n */\nfinal class SelectTraceIdsFromSpan extends ResultSetFutureCall<AsyncResultSet> {\n  @AutoValue abstract static class Input {\n    @Nullable abstract String l_service();\n\n    @Nullable abstract String annotation_query();\n\n    abstract UUID start_ts();\n\n    abstract UUID end_ts();\n\n    abstract int limit_();\n  }\n\n  static final class Factory {\n    final CqlSession session;\n    final PreparedStatement withAnnotationQuery, withServiceAndAnnotationQuery;\n\n    Factory(CqlSession session) {\n      this.session = session;\n      String querySuffix = \"\"\"\n        annotation_query LIKE ?\\\n         AND ts_uuid>=?\\\n         AND ts_uuid<=?\\\n         LIMIT ?\\\n         ALLOW FILTERING\\\n        \"\"\";\n      this.withAnnotationQuery = session.prepare(\"SELECT trace_id,ts\"\n        + \" FROM \" + TABLE_SPAN\n        + \" WHERE \" + querySuffix);\n      this.withServiceAndAnnotationQuery = session.prepare(\"SELECT trace_id,ts\"\n        + \" FROM \" + TABLE_SPAN\n        + \" WHERE l_service=:l_service\"\n        + \" AND \" + querySuffix);\n    }\n\n    Call<Map<String, Long>> newCall(\n      @Nullable String serviceName,\n      String annotationKey,\n      TimestampRange timestampRange,\n      int limit) {\n      Input input = new AutoValue_SelectTraceIdsFromSpan_Input(\n        serviceName,\n        annotationKey,\n        timestampRange.startUUID,\n        timestampRange.endUUID,\n        limit);\n      PreparedStatement preparedStatement =\n        serviceName != null ? withServiceAndAnnotationQuery : withAnnotationQuery;\n      return new SelectTraceIdsFromSpan(this, preparedStatement, input)\n        .flatMap(AccumulateTraceIdTsLong.get());\n    }\n  }\n\n  final Factory factory;\n  final PreparedStatement preparedStatement;\n  final Input input;\n\n  SelectTraceIdsFromSpan(Factory factory, PreparedStatement preparedStatement, Input input) {\n    this.factory = factory;\n    this.preparedStatement = preparedStatement;\n    this.input = input;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    BoundStatementBuilder bound = preparedStatement.boundStatementBuilder();\n    int i = 0;\n    if (input.l_service() != null) bound.setString(i++, input.l_service());\n    if (input.annotation_query() != null) {\n      bound.setString(i++, input.annotation_query());\n    } else {\n      throw new IllegalArgumentException(input.toString());\n    }\n    bound\n      .setUuid(i++, input.start_ts())\n      .setUuid(i++, input.end_ts())\n      .setInt(i, input.limit_())\n      .setPageSize(input.limit_());\n    return factory.session.executeAsync(bound.build());\n  }\n\n  @Override public AsyncResultSet map(AsyncResultSet input) {\n    return input;\n  }\n\n  @Override public SelectTraceIdsFromSpan clone() {\n    return new SelectTraceIdsFromSpan(factory, preparedStatement, input);\n  }\n\n  @Override public String toString() {\n    return input.toString().replace(\"Input\", \"SelectTraceIdsFromSpan\");\n  }\n\n  static final class AccumulateTraceIdTsLong extends AccumulateAllResults<Map<String, Long>> {\n    static final AccumulateAllResults<Map<String, Long>> INSTANCE = new AccumulateTraceIdTsLong();\n\n    static AccumulateAllResults<Map<String, Long>> get() {\n      return INSTANCE;\n    }\n\n    @Override protected Supplier<Map<String, Long>> supplier() {\n      return LinkedHashMap::new; // because results are not distinct\n    }\n\n    @Override protected BiConsumer<Row, Map<String, Long>> accumulator() {\n      return (row, result) -> {\n        if (row.isNull(1)) return; // no timestamp\n        result.put(row.getString(0), row.getLong(1));\n      };\n    }\n\n    @Override public String toString() {\n      return \"AccumulateTraceIdTsLong{}\";\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/HostAndPort.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport zipkin2.Endpoint;\n\n// Similar to com.google.common.net.HostAndPort, but no guava dep\npublic final class HostAndPort {\n  final String host;\n  final int port;\n\n  HostAndPort(String host, int port) {\n    this.host = host;\n    this.port = port;\n  }\n\n  /** Returns the unvalidated hostname or IP literal */\n  public String getHost() {\n    return host;\n  }\n\n  /** Returns the port */\n  public int getPort() {\n    return port;\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof HostAndPort)) return false;\n    HostAndPort that = (HostAndPort) o;\n    return host.equals(that.host) && port == that.port;\n  }\n\n  @Override public int hashCode() {\n    int h = 1;\n    h *= 1000003;\n    h ^= (host == null) ? 0 : host.hashCode();\n    h *= 1000003;\n    h ^= port;\n    return h;\n  }\n\n  @Override public String toString() {\n    return \"HostAndPort{host=\" + host + \", port=\" + port + \"}\";\n  }\n\n  /**\n   * Constructs a host-port pair from the given string, defaulting to the indicated port if absent\n   */\n  public static HostAndPort fromString(String hostPort, int defaultPort) {\n    if (hostPort == null) throw new NullPointerException(\"hostPort == null\");\n\n    String host = hostPort;\n    int endHostIndex = hostPort.length();\n    if (hostPort.startsWith(\"[\")) { // Bracketed IPv6\n      endHostIndex = hostPort.lastIndexOf(']') + 1;\n      host = hostPort.substring(1, endHostIndex == 0 ? 1 : endHostIndex - 1);\n      if (!Endpoint.newBuilder().parseIp(host)) { // reuse our IPv6 validator\n        throw new IllegalArgumentException(hostPort + \" contains an invalid IPv6 literal\");\n      }\n    } else {\n      int colonIndex = hostPort.indexOf(':'), nextColonIndex = hostPort.lastIndexOf(':');\n      if (colonIndex >= 0) {\n        if (colonIndex == nextColonIndex) { // only 1 colon\n          host = hostPort.substring(0, colonIndex);\n          endHostIndex = colonIndex;\n        } else if (!Endpoint.newBuilder().parseIp(hostPort)) { // reuse our IPv6 validator\n          throw new IllegalArgumentException(hostPort + \" is an invalid IPv6 literal\");\n        }\n      }\n    }\n    if (host.isEmpty()) throw new IllegalArgumentException(hostPort + \" has an empty host\");\n    if (endHostIndex + 1 < hostPort.length() && hostPort.charAt(endHostIndex) == ':') {\n      return new HostAndPort(host, validatePort(hostPort.substring(endHostIndex + 1), hostPort));\n    }\n    return new HostAndPort(host, defaultPort);\n  }\n\n  static int validatePort(String portString, String hostPort) {\n    for (int i = 0, length = portString.length(); i < length; i++) {\n      char c = portString.charAt(i);\n      if (c >= '0' && c <= '9') continue; // isDigit\n      throw new IllegalArgumentException(hostPort + \" has an invalid port\");\n    }\n    int result = Integer.parseInt(portString);\n    if (result == 0 || result > 0xffff) {\n      throw new IllegalArgumentException(hostPort + \" has an invalid port\");\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/KeyspaceMetadataUtil.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport com.datastax.oss.driver.api.core.CqlIdentifier;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;\nimport java.util.Optional;\n\npublic final class KeyspaceMetadataUtil {\n\n  public static int getDefaultTtl(KeyspaceMetadata keyspaceMetadata, String table) {\n    return (int) keyspaceMetadata.getTable(table)\n      .map(TableMetadata::getOptions)\n      .flatMap(o -> Optional.ofNullable(o.get(CqlIdentifier.fromCql(\"default_time_to_live\"))))\n      .orElse(0);\n  }\n\n  KeyspaceMetadataUtil() {\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/Resources.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.io.UncheckedIOException;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic final class Resources {\n  public static String resourceToString(String resource) {\n    try (\n      Reader reader = new InputStreamReader(Resources.class.getResourceAsStream(resource), UTF_8)) {\n      char[] buf = new char[2048];\n      StringBuilder builder = new StringBuilder();\n      int read;\n      while ((read = reader.read(buf)) != -1) {\n        builder.append(buf, 0, read);\n      }\n      return builder.toString();\n    } catch (IOException ex) {\n      throw new UncheckedIOException(ex);\n    }\n  }\n\n  Resources() {\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/SessionBuilder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.CqlSessionBuilder;\nimport com.datastax.oss.driver.api.core.auth.AuthProvider;\nimport com.datastax.oss.driver.api.core.config.DriverConfigLoader;\nimport com.datastax.oss.driver.api.core.config.DriverOption;\nimport com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder;\nimport com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory;\nimport com.datastax.oss.driver.internal.core.tracker.RequestLogger;\nimport java.net.InetSocketAddress;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.internal.Nullable;\n\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_CONSISTENCY;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_LOGGER_SUCCESS_ENABLED;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_LOGGER_VALUES;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_TIMEOUT;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_TRACKER_CLASSES;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.REQUEST_WARN_IF_SET_KEYSPACE;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS;\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.SSL_HOSTNAME_VALIDATION;\n\npublic final class SessionBuilder {\n  /** Returns a connected session. Closes the cluster if any exception occurred. */\n  public static CqlSession buildSession(\n    String contactPoints,\n    String localDc,\n    Map<DriverOption, Integer> poolingOptions,\n    @Nullable AuthProvider authProvider,\n    boolean useSsl,\n    boolean sslHostnameValidation\n  ) {\n    // Some options aren't supported by builder methods. In these cases, we use driver config\n    // See https://groups.google.com/a/lists.datastax.com/forum/#!topic/java-driver-user/Z8HrCDX47Q0\n    ProgrammaticDriverConfigLoaderBuilder config =\n      // We aren't reading any resources from the classpath, but this prevents errors running in the\n      // server, where Thread.currentThread().getContextClassLoader() returns null\n      DriverConfigLoader.programmaticBuilder(SessionBuilder.class.getClassLoader());\n\n    // Ported from java-driver v3 PoolingOptions.setPoolTimeoutMillis as request timeout includes that\n    config.withDuration(REQUEST_TIMEOUT, Duration.ofMinutes(1));\n\n    CqlSessionBuilder builder = CqlSession.builder();\n    builder.addContactPoints(parseContactPoints(contactPoints));\n    if (authProvider != null) builder.withAuthProvider(authProvider);\n\n    // In java-driver v3, we used LatencyAwarePolicy(DCAwareRoundRobinPolicy|RoundRobinPolicy)\n    //   where DCAwareRoundRobinPolicy was used if localDc != null\n    //\n    // In java-driver v4, the default policy is token-aware and localDc is required. Hence, we\n    // use the default load balancing policy\n    //  * https://github.com/datastax/java-driver/blob/master/manual/core/load_balancing/README.md\n    builder.withLocalDatacenter(localDc);\n    config = config.withString(REQUEST_CONSISTENCY, \"LOCAL_ONE\");\n    // Pooling options changed dramatically from v3->v4. This is a close match.\n    poolingOptions.forEach(config::withInt);\n\n    // All Zipkin CQL writes are idempotent\n    config = config.withBoolean(REQUEST_DEFAULT_IDEMPOTENCE, true);\n\n    if (useSsl) {\n      config = config.withClass(SSL_ENGINE_FACTORY_CLASS, DefaultSslEngineFactory.class);\n      config = config.withBoolean(SSL_HOSTNAME_VALIDATION, sslHostnameValidation);\n    }\n\n    // Log categories can enable query logging\n    Logger requestLogger = LoggerFactory.getLogger(SessionBuilder.class);\n    if (requestLogger.isDebugEnabled()) {\n      config = config.withClassList(REQUEST_TRACKER_CLASSES, List.of(RequestLogger.class));\n      config = config.withBoolean(REQUEST_LOGGER_SUCCESS_ENABLED, true);\n      // Only show bodies when TRACE is enabled\n      config = config.withBoolean(REQUEST_LOGGER_VALUES, requestLogger.isTraceEnabled());\n    }\n\n    // Don't warn: ensureSchema creates the keyspace. Hence, we need to \"use\" it later.\n    config = config.withBoolean(REQUEST_WARN_IF_SET_KEYSPACE, false);\n\n    return builder.withConfigLoader(config.build()).build();\n  }\n\n  static List<InetSocketAddress> parseContactPoints(String contactPoints) {\n    List<InetSocketAddress> result = new ArrayList<>();\n    for (String contactPoint : contactPoints.split(\",\", 100)) {\n      HostAndPort parsed = HostAndPort.fromString(contactPoint, 9042);\n      result.add(new InetSocketAddress(parsed.getHost(), parsed.getPort()));\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/AccumulateAllResults.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport java.util.concurrent.CompletionStage;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport zipkin2.Call;\nimport zipkin2.Call.FlatMapper;\n\npublic abstract class AccumulateAllResults<T> implements FlatMapper<AsyncResultSet, T> {\n  protected abstract Supplier<T> supplier();\n\n  protected abstract BiConsumer<Row, T> accumulator();\n\n  /** Customizes the aggregated result. For example, summarizing or making immutable. */\n  protected Function<T, T> finisher() {\n    return Function.identity();\n  }\n\n  @Override public Call<T> map(AsyncResultSet rs) {\n    return new AccumulateNextResults<>(\n      supplier().get(),\n      accumulator(),\n      finisher()\n    ).map(rs);\n  }\n\n  static final class FetchMoreResults extends ResultSetFutureCall<AsyncResultSet> {\n    final AsyncResultSet resultSet;\n\n    FetchMoreResults(AsyncResultSet resultSet) {\n      this.resultSet = resultSet;\n    }\n\n    @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n      return resultSet.fetchNextPage().toCompletableFuture();\n    }\n\n    @Override public AsyncResultSet map(AsyncResultSet input) {\n      return input;\n    }\n\n    @Override public Call<AsyncResultSet> clone() {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override public String toString() {\n      return \"FetchMoreResults{\" + resultSet + \"}\";\n    }\n  }\n\n  static final class AccumulateNextResults<T> implements FlatMapper<AsyncResultSet, T> {\n    final T pendingResults;\n    final BiConsumer<Row, T> accumulator;\n    final Function<T, T> finisher;\n\n    AccumulateNextResults(\n      T pendingResults, BiConsumer<Row, T> accumulator, Function<T, T> finisher) {\n      this.pendingResults = pendingResults;\n      this.accumulator = accumulator;\n      this.finisher = finisher;\n    }\n\n    /** Iterates through the rows in each page, flatmapping on more results until exhausted */\n    @Override public Call<T> map(AsyncResultSet rs) {\n      while (rs.remaining() > 0) {\n        accumulator.accept(rs.one(), pendingResults);\n      }\n      // Return collected results if there are no more pages\n      return rs.getExecutionInfo().getPagingState() == null && !rs.hasMorePages()\n        ? Call.create(finisher.apply(pendingResults))\n        : new FetchMoreResults(rs).flatMap(this);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/AccumulateTraceIdTsUuid.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport com.datastax.oss.driver.api.core.uuid.Uuids;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic final class AccumulateTraceIdTsUuid\n  extends AccumulateAllResults<Map<String, Long>> {\n  static final AccumulateAllResults<Map<String, Long>> INSTANCE = new AccumulateTraceIdTsUuid();\n\n  public static AccumulateAllResults<Map<String, Long>> get() {\n    return INSTANCE;\n  }\n\n  @Override protected Supplier<Map<String, Long>> supplier() {\n    return LinkedHashMap::new; // because results are not distinct\n  }\n\n  @Override protected BiConsumer<Row, Map<String, Long>> accumulator() {\n    return (row, result) ->\n      result.put(row.getString(0), Uuids.unixTimestamp(row.getUuid(1)));\n  }\n\n  @Override public String toString() {\n    return \"AccumulateTraceIdTsUuid{}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/AggregateIntoMap.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport zipkin2.Call;\nimport zipkin2.internal.AggregateCall;\n\npublic final class AggregateIntoMap<K, V> extends AggregateCall<Map<K, V>, Map<K, V>> {\n  public AggregateIntoMap(List<Call<Map<K, V>>> calls) {\n    super(calls);\n  }\n\n  @Override protected Map<K, V> newOutput() {\n    return new LinkedHashMap<>();\n  }\n\n  @Override protected void append(Map<K, V> input, Map<K, V> output) {\n    output.putAll(input);\n  }\n\n  @Override public AggregateIntoMap<K, V> clone() {\n    return new AggregateIntoMap<>(cloneCalls());\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/DeduplicatingInsert.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.internal.DelayLimiter;\n\npublic abstract class DeduplicatingInsert<I> extends ResultSetFutureCall<Void> {\n  public static abstract class Factory<I> {\n    protected final DelayLimiter<I> delayLimiter;\n\n    protected Factory(long ttl, int cardinality) {\n      delayLimiter =\n        DelayLimiter.newBuilder().ttl(ttl, TimeUnit.MILLISECONDS).cardinality(cardinality).build();\n    }\n\n    protected abstract Call<Void> newCall(I input);\n\n    public final void maybeAdd(I input, List<Call<Void>> calls) {\n      if (input == null) throw new NullPointerException(\"input == null\");\n      if (!delayLimiter.shouldInvoke(input)) return;\n      calls.add(newCall(input));\n    }\n\n    public void clear() {\n      delayLimiter.clear();\n    }\n  }\n\n  protected final DelayLimiter<I> delayLimiter;\n  protected final I input;\n\n  protected DeduplicatingInsert(DelayLimiter<I> delayLimiter, I input) {\n    this.delayLimiter = delayLimiter;\n    this.input = input;\n  }\n\n  @Override protected final Void doExecute() {\n    try {\n      return super.doExecute();\n    } catch (RuntimeException | Error e) {\n      delayLimiter.invalidate(input);\n      throw e;\n    }\n  }\n\n  @Override protected final void doEnqueue(Callback<Void> callback) {\n    super.doEnqueue(new Callback<>() {\n      @Override public void onSuccess(Void value) {\n        callback.onSuccess(value);\n      }\n\n      @Override public void onError(Throwable t) {\n        delayLimiter.invalidate(input);\n        callback.onError(t);\n      }\n    });\n  }\n\n  @Override public final void doCancel() {\n    delayLimiter.invalidate(input);\n    super.doCancel();\n  }\n\n  @Override public final Void map(AsyncResultSet input) {\n    return null;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/DistinctSortedStrings.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.cql.Row;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\npublic final class DistinctSortedStrings extends AccumulateAllResults<List<String>> {\n  static final AccumulateAllResults<List<String>> INSTANCE = new DistinctSortedStrings();\n\n  public static AccumulateAllResults<List<String>> get() {\n    return INSTANCE;\n  }\n\n  @Override protected Supplier<List<String>> supplier() {\n    return ArrayList::new;\n  }\n\n  @Override protected Function<List<String>, List<String>> finisher() {\n    return SortDistinct.INSTANCE;\n  }\n\n  enum SortDistinct implements Function<List<String>, List<String>> {\n    INSTANCE;\n\n    @Override public List<String> apply(List<String> strings) {\n      Collections.sort(strings);\n      return new ArrayList<>(new LinkedHashSet<>(strings));\n    }\n  }\n\n  @Override protected BiConsumer<Row, List<String>> accumulator() {\n    return (row, list) -> {\n      String result = row.getString(0);\n      if (!result.isEmpty()) list.add(result);\n    };\n  }\n\n  @Override public String toString() {\n    return \"DistinctSortedStrings{}\";\n  }\n\n  DistinctSortedStrings() {\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/InsertEntry.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport java.util.Map;\nimport java.util.concurrent.CompletionStage;\nimport zipkin2.Call;\n\npublic final class InsertEntry extends DeduplicatingInsert<Map.Entry<String, String>> {\n  public static final class Factory extends DeduplicatingInsert.Factory<Map.Entry<String, String>> {\n    final CqlSession session;\n    final PreparedStatement preparedStatement;\n\n    public Factory(String statement, CqlSession session, long ttl, int cardinality) {\n      this(statement, session, ttl, cardinality, 0);\n    }\n\n    /** Cassandra v1 has deprecated support for indexTtl. */\n    public Factory(String statement, CqlSession session, long ttl, int cardinality, int indexTtl) {\n      super(ttl, cardinality);\n      this.session = session;\n      this.preparedStatement =\n        session.prepare(indexTtl > 0 ? statement + \" USING TTL \" + indexTtl : statement);\n    }\n\n    @Override protected Call<Void> newCall(Map.Entry<String, String> input) {\n      return new InsertEntry(this, input);\n    }\n  }\n\n  final Factory factory;\n\n  InsertEntry(Factory factory, Map.Entry<String, String> input) {\n    super(factory.delayLimiter, input);\n    this.factory = factory;\n  }\n\n  @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n    return factory.session.executeAsync(factory.preparedStatement.boundStatementBuilder()\n      .setString(0, input.getKey())\n      .setString(1, input.getValue()).build());\n  }\n\n  @Override public String toString() {\n    return factory.preparedStatement.getQuery()\n      .replace(\"(?,?)\", \"(\" + input.getKey() + \",\" + input.getValue() + \")\");\n  }\n\n  @Override public Call<Void> clone() {\n    return new InsertEntry(factory, input);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/IntersectKeySets.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport zipkin2.Call;\nimport zipkin2.internal.AggregateCall;\n\npublic final class IntersectKeySets extends AggregateCall<Map<String, Long>, Set<String>> {\n  public IntersectKeySets(List<Call<Map<String, Long>>> calls) {\n    super(calls);\n  }\n\n  @Override protected Set<String> newOutput() {\n    return new LinkedHashSet<>();\n  }\n\n  boolean firstInput = true;\n\n  @Override protected void append(Map<String, Long> input, Set<String> output) {\n    if (firstInput) {\n      firstInput = false;\n      output.addAll(input.keySet());\n    } else {\n      output.retainAll(input.keySet());\n    }\n  }\n\n  @Override public IntersectKeySets clone() {\n    return new IntersectKeySets(cloneCalls());\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/IntersectMaps.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport zipkin2.Call;\nimport zipkin2.internal.AggregateCall;\n\npublic final class IntersectMaps<K, V> extends AggregateCall<Map<K, V>, Map<K, V>> {\n\n  public IntersectMaps(List<Call<Map<K, V>>> calls) {\n    super(calls);\n  }\n\n  @Override protected Map<K, V> newOutput() {\n    return new LinkedHashMap<>();\n  }\n\n  boolean firstInput = true;\n\n  @Override protected void append(Map<K, V> input, Map<K, V> output) {\n    if (firstInput) {\n      firstInput = false;\n      output.putAll(input);\n    } else {\n      output.keySet().retainAll(input.keySet());\n    }\n  }\n\n  @Override public IntersectMaps<K, V> clone() {\n    return new IntersectMaps<>(cloneCalls());\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/java/zipkin2/storage/cassandra/internal/call/ResultSetFutureCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.DriverException;\nimport com.datastax.oss.driver.api.core.DriverExecutionException;\nimport com.datastax.oss.driver.api.core.RequestThrottlingException;\nimport com.datastax.oss.driver.api.core.connection.BusyConnectionException;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.servererrors.QueryConsistencyException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport java.util.concurrent.ExecutionException;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport zipkin2.Call;\nimport zipkin2.Call.Mapper;\nimport zipkin2.Callback;\nimport zipkin2.internal.Nullable;\n\n// some copy/pasting is ok here as debugging is obscured when the type hierarchy gets deep.\npublic abstract class ResultSetFutureCall<V> extends Call.Base<V>\n  implements Mapper<AsyncResultSet, V>, Function<AsyncResultSet, V> {\n  /** Defers I/O until {@link #enqueue(Callback)} or {@link #execute()} are called. */\n  protected abstract CompletionStage<AsyncResultSet> newCompletionStage();\n\n  volatile CompletableFuture<V> future;\n\n  @Override protected V doExecute() {\n    return getUninterruptibly(newCompletionStage().thenApply(this));\n  }\n\n  @Override protected void doEnqueue(Callback<V> callback) {\n    try {\n      future = newCompletionStage()\n        .thenApply(this)\n        .handleAsync(new CallbackFunction<>(callback))\n        .toCompletableFuture();\n    } catch (Throwable t) {\n      propagateIfFatal(t);\n      callback.onError(t);\n    }\n  }\n\n  @Override public V apply(AsyncResultSet input) {\n    return map(input); // dispatched to Function so that toString is nicer vs a lambda\n  }\n\n  @Override protected void doCancel() {\n    CompletableFuture<V> maybeFuture = future;\n    if (maybeFuture != null) maybeFuture.cancel(true);\n  }\n\n  @Override protected final boolean doIsCanceled() {\n    CompletableFuture<V> maybeFuture = future;\n    return maybeFuture != null && maybeFuture.isCancelled();\n  }\n\n  static final class CallbackFunction<V> implements BiFunction<V, Throwable, V> {\n    final Callback<V> callback;\n\n    CallbackFunction(Callback<V> callback) {\n      this.callback = callback;\n    }\n\n    @Override public V apply(V input, @Nullable Throwable error) {\n      if (error != null) {\n        callback.onError(error);\n        return input;\n      }\n      try {\n        callback.onSuccess(input);\n      } catch (Throwable t) {\n        propagateIfFatal(t);\n        callback.onError(t);\n      }\n      return input;\n    }\n\n    @Override public String toString() {\n      return callback.toString();\n    }\n  }\n\n  // Avoid internal dependency on Datastax CompletableFutures and shaded Throwables\n  static <T> T getUninterruptibly(CompletionStage<T> stage) {\n    boolean interrupted = false;\n    try {\n      while (true) {\n        try {\n          return stage.toCompletableFuture().get();\n        } catch (InterruptedException e) {\n          interrupted = true;\n        } catch (ExecutionException e) {\n          Throwable cause = e.getCause();\n          if (cause instanceof DriverException exception) {\n            throw exception.copy();\n          }\n          if (cause instanceof RuntimeException exception) throw exception;\n          if (cause instanceof Error error) throw error;\n          throw new DriverExecutionException(cause);\n        }\n      }\n    } finally {\n      if (interrupted) {\n        Thread.currentThread().interrupt();\n      }\n    }\n  }\n\n  /**\n   * Sets {@link zipkin2.storage.StorageComponent#isOverCapacity(java.lang.Throwable)}\n   */\n  public static boolean isOverCapacity(Throwable e) {\n    return e instanceof QueryConsistencyException ||\n      e instanceof BusyConnectionException ||\n      e instanceof RequestThrottlingException;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/resources/zipkin2-schema-indexes.cql",
    "content": "ALTER TABLE zipkin2.span ADD l_service text;\nDROP INDEX IF EXISTS zipkin2.span_l_service_idx;\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.span (l_service) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\n\nALTER TABLE zipkin2.span ADD annotation_query text; //-- can't do SASI on set<text>: ░-joined until CASSANDRA-11182\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.span (annotation_query) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {\n    'mode': 'PREFIX',\n    'analyzed': 'true',\n    'analyzer_class':'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer',\n    'delimiter': '░'};\n\nCREATE TABLE IF NOT EXISTS zipkin2.trace_by_service_span (\n    service       text,             //-- service name\n    span          text,             //-- span name, or blank for queries without span name\n    bucket        int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts            timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id      text,             //-- trace ID\n    duration      bigint,           //-- span duration, in milliseconds\n    PRIMARY KEY ((service, span, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a service, or service and span. span column may be blank (when only looking up by service). bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision. duration column is span duration, rounded up to tens of milliseconds (or hundredths of seconds)';\n\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.trace_by_service_span (duration) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\n\nCREATE TABLE IF NOT EXISTS zipkin2.trace_by_service_remote_service (\n    service         text,             //-- service name\n    remote_service  text,             //-- remote servie name\n    bucket          int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts              timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id        text,             //-- trace ID\n    PRIMARY KEY ((service, remote_service, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a remote service. bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision.';\n\nCREATE TABLE IF NOT EXISTS zipkin2.span_by_service (\n    service text,\n    span    text,\n    PRIMARY KEY (service, span)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up span names by a service name. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n\nCREATE TABLE IF NOT EXISTS zipkin2.remote_service_by_service (\n    service text,\n    remote_service text,\n    PRIMARY KEY (service, remote_service)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up remote service names by a service name. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n\nCREATE TABLE IF NOT EXISTS zipkin2.autocomplete_tags (\n    key     text,\n    value    text,\n    PRIMARY KEY (key, value)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up span tag values for auto-complete purposes. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/resources/zipkin2-schema-upgrade-1.cql",
    "content": "CREATE TABLE IF NOT EXISTS zipkin2.autocomplete_tags (\n    key     text,\n    value    text,\n    PRIMARY KEY (key, value)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up tag key and values for a service';\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/resources/zipkin2-schema-upgrade-2.cql",
    "content": "CREATE TABLE IF NOT EXISTS zipkin2.remote_service_by_service (\n    service text,\n    remote_service text,\n    PRIMARY KEY (service, remote_service)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up remote service names by a service name.';\n\nCREATE TABLE IF NOT EXISTS zipkin2.trace_by_service_remote_service (\n    service         text,             //-- service name\n    remote_service  text,             //-- remote servie name\n    bucket          int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts              timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id        text,             //-- trace ID\n    PRIMARY KEY ((service, remote_service, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a remote service. bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision.';\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/main/resources/zipkin2-schema.cql",
    "content": "CREATE KEYSPACE IF NOT EXISTS zipkin2\n  WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}\n  AND durable_writes = false;\n\nCREATE TYPE IF NOT EXISTS zipkin2.endpoint (\n    service      text,\n    ipv4         inet,\n    ipv6         inet,\n    port         int,\n);\n\nCREATE TYPE IF NOT EXISTS zipkin2.annotation (\n    ts bigint,\n    v  text,\n);\n\nCREATE TABLE IF NOT EXISTS zipkin2.span (\n    trace_id            text, // when strictTraceId=false, only contains right-most 16 chars\n    ts_uuid             timeuuid,\n    id                  text,\n    trace_id_high       text, // when strictTraceId=false, contains left-most 16 chars if present\n    parent_id           text,\n    kind                text,\n    span                text, // span.name\n    ts                  bigint,\n    duration            bigint,\n    l_ep                Endpoint,\n    r_ep                Endpoint,\n    annotations         list<frozen<annotation>>,\n    tags                map<text,text>,\n    debug               boolean,\n    shared              boolean,\n    PRIMARY KEY (trace_id, ts_uuid, id)\n)\n    WITH CLUSTERING ORDER BY (ts_uuid DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  604800\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Primary table for holding trace data';\n\nCREATE TABLE IF NOT EXISTS zipkin2.dependency (\n    day          date,\n    parent       text,\n    child        text,\n    errors       bigint,\n    calls        bigint,\n    PRIMARY KEY (day, parent, child)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND comment = 'Holder for each days generation of zipkin2.DependencyLink';\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraContainer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.codahale.metrics.Gauge;\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.metadata.Node;\nimport com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric;\nimport com.datastax.oss.driver.api.core.metrics.Metrics;\nimport com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assumptions.assumeTrue;\nimport static org.testcontainers.utility.DockerImageName.parse;\nimport static zipkin2.Call.propagateIfFatal;\nimport static zipkin2.storage.cassandra.ITCassandraStorage.SEARCH_TABLES;\nimport static zipkin2.storage.cassandra.Schema.TABLE_DEPENDENCY;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\nclass CassandraContainer extends GenericContainer<CassandraContainer> {\n  static final Logger LOGGER = LoggerFactory.getLogger(CassandraContainer.class);\n  CqlSession globalSession;\n\n  CassandraContainer() {\n    super(parse(\"ghcr.io/openzipkin/zipkin-cassandra:3.4.3\"));\n    addExposedPort(9042);\n    waitStrategy = Wait.forHealthcheck();\n    withLogConsumer(new Slf4jLogConsumer(LOGGER));\n  }\n\n  @Override public void start() {\n    super.start();\n    LOGGER.info(\"Using contactPoint {}\", contactPoint());\n    globalSession = tryToInitializeSession(contactPoint());\n  }\n\n  // Builds a session without trying to use a namespace or init UDTs\n  static CqlSession tryToInitializeSession(String contactPoint) {\n    CassandraStorage storage = newStorageBuilder(contactPoint).build();\n    CqlSession session = null;\n    try {\n      session = DefaultSessionFactory.buildSession(storage);\n      session.execute(\"SELECT now() FROM system.local\");\n    } catch (Throwable e) {\n      propagateIfFatal(e);\n      if (session != null) session.close();\n      assumeTrue(false, e.getMessage());\n    }\n    return session;\n  }\n\n  CassandraStorage.Builder newStorageBuilder() {\n    return newStorageBuilder(contactPoint());\n  }\n\n  static CassandraStorage.Builder newStorageBuilder(String contactPoint) {\n    return CassandraStorage.newBuilder().contactPoints(contactPoint).maxConnections(1);\n  }\n\n  String contactPoint() {\n    return getHost() + \":\" + getMappedPort(9042);\n  }\n\n  void clear(CassandraStorage storage) {\n    // Clear any key cache\n    CassandraSpanConsumer spanConsumer = storage.spanConsumer;\n    if (spanConsumer != null) spanConsumer.clear();\n\n    CqlSession session = storage.session.session;\n    if (session == null) session = globalSession;\n\n    List<String> toTruncate = new ArrayList<>(SEARCH_TABLES);\n    toTruncate.add(TABLE_DEPENDENCY);\n    toTruncate.add(TABLE_SPAN);\n\n    for (String table : toTruncate) {\n      try {\n        session.execute(\"TRUNCATE \" + storage.keyspace + \".\" + table);\n      } catch (InvalidQueryException e) {\n        assertThat(e).hasMessage(\"unconfigured table \" + table);\n      }\n    }\n\n    blockWhileInFlight(storage);\n  }\n\n  @Override public void stop() {\n    if (globalSession != null) globalSession.close();\n    super.stop();\n  }\n\n  static void blockWhileInFlight(CassandraStorage storage) {\n    CqlSession session = storage.session.get();\n    // Now, block until writes complete, notably so we can read them.\n    boolean wasInFlight = false;\n    while (true) {\n      if (!poolInFlight(session)) {\n        if (wasInFlight) sleep(100); // give a little more to avoid flakey tests\n        return;\n      }\n      wasInFlight = true;\n      sleep(100);\n    }\n  }\n\n  static void sleep(long millis) {\n    try {\n      Thread.sleep(millis);\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new AssertionError(e);\n    }\n  }\n\n  // Use metrics to wait for in-flight requests to settle per\n  // https://groups.google.com/a/lists.datastax.com/g/java-driver-user/c/5um_yGNynow/m/cInH5I5jBgAJ\n  static boolean poolInFlight(CqlSession session) {\n    Collection<Node> nodes = session.getMetadata().getNodes().values();\n    Optional<Metrics> metrics = session.getMetrics();\n    for (Node node : nodes) {\n      int inFlight = metrics.flatMap(m -> m.getNodeMetric(node, DefaultNodeMetric.IN_FLIGHT))\n        .map(m -> ((Gauge<Integer>) m).getValue())\n        .orElse(0);\n      if (inFlight > 0) return true;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraSpanConsumerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.internal.AggregateCall;\nimport zipkin2.storage.cassandra.internal.call.InsertEntry;\nimport zipkin2.storage.cassandra.internal.call.ResultSetFutureCall;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.entry;\nimport static org.assertj.core.api.Assertions.tuple;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\n\n@ExtendWith(MockitoExtension.class)\nclass CassandraSpanConsumerTest {\n  @Mock CqlSession session;\n  Schema.Metadata metadata = new Schema.Metadata(true, true);\n  CassandraSpanConsumer consumer;\n\n  @BeforeEach void setup() {\n    consumer = spanConsumer(CassandraStorage.newBuilder());\n  }\n\n  Span spanWithoutAnnotationsOrTags =\n    Span.newBuilder()\n      .traceId(\"a\")\n      .id(\"1\")\n      .name(\"get\")\n      .localEndpoint(FRONTEND)\n      .timestamp(TODAY * 1000L)\n      .duration(207000L)\n      .build();\n\n  @Test void emptyInput_emptyCall() {\n    Call<Void> call = consumer.accept(List.of());\n    assertThat(call).hasSameClassAs(Call.create(null));\n  }\n\n  @Test void doesntSetTraceIdHigh_128() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder()\n      .traceId(\"77fcac3d4c5be8d2a037812820c65f28\")\n      .build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertSpan)\n      .extracting(\"input.trace_id_high\", \"input.trace_id\")\n      .containsExactly(tuple(null, span.traceId()));\n  }\n\n  @Test void doesntSetTraceIdHigh_64() {\n    Span span = spanWithoutAnnotationsOrTags;\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertSpan)\n      .extracting(\"input.trace_id_high\", \"input.trace_id\")\n      .containsExactly(tuple(null, span.traceId()));\n  }\n\n  @Test void strictTraceIdFalse_setsTraceIdHigh() {\n    consumer = spanConsumer(CassandraStorage.newBuilder().strictTraceId(false));\n\n    Span span = spanWithoutAnnotationsOrTags.toBuilder()\n      .traceId(\"77fcac3d4c5be8d2a037812820c65f28\")\n      .build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertSpan)\n      .extracting(\"input.trace_id_high\", \"input.trace_id\")\n      .containsExactly(tuple(\"77fcac3d4c5be8d2\", \"a037812820c65f28\"));\n  }\n\n  @Test void serviceSpanKeys() {\n    Span span = spanWithoutAnnotationsOrTags;\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertEntry)\n      .extracting(\"input\")\n      .containsExactly(entry(FRONTEND.serviceName(), span.name()));\n  }\n\n  @Test void serviceRemoteServiceKeys_addsRemoteServiceName() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().remoteEndpoint(BACKEND).build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertEntry)\n      .extracting(\"input\")\n      .containsExactly(\n        entry(FRONTEND.serviceName(), span.name()),\n        entry(FRONTEND.serviceName(), BACKEND.serviceName())\n      );\n  }\n\n  @Test void serviceRemoteServiceKeys_skipsRemoteServiceNameWhenNoLocalService() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder()\n      .localEndpoint(null)\n      .remoteEndpoint(BACKEND).build();\n\n    Call<Void> call = consumer.accept(List.of(span));\n\n    assertThat(call).isInstanceOf(InsertSpan.class);\n  }\n\n  @Test void serviceSpanKeys_emptyWhenNoEndpoints() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().localEndpoint(null).build();\n\n    assertThat(consumer.accept(List.of(span)))\n      .isInstanceOf(ResultSetFutureCall.class);\n  }\n\n  /**\n   * To allow lookups w/o a span name, we index \"\". \"\" is used instead of null to avoid creating\n   * tombstones.\n   */\n  @Test void traceByServiceSpan_indexesLocalServiceNameAndEmptySpanName() {\n    Span span = spanWithoutAnnotationsOrTags;\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .extracting(\"input.service\", \"input.span\")\n      .containsExactly(\n        tuple(FRONTEND.serviceName(), span.name()), tuple(FRONTEND.serviceName(), \"\"));\n  }\n\n  @Test void traceByServiceSpan_indexesDurationInMillis() {\n    Span span = spanWithoutAnnotationsOrTags;\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .extracting(\"input.duration\")\n      .containsOnly(span.durationAsLong() / 1000L);\n  }\n\n  @Test void traceByServiceSpan_indexesDurationMinimumZero() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().duration(12L).build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .extracting(\"input.duration\")\n      .containsOnly(0L);\n  }\n\n  @Test void traceByServiceSpan_skipsOnNoTimestamp() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().timestamp(null).build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .extracting(\"input.service\", \"input.span\")\n      .isEmpty();\n  }\n\n  @Test void traceByServiceSpan_doesntIndexRemoteService() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().remoteEndpoint(BACKEND).build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .hasSize(2)\n      .extracting(\"input.service\")\n      .doesNotContain(BACKEND.serviceName());\n  }\n\n  @Test void traceByServiceSpan_appendsEmptyWhenNoName() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().name(null).build();\n\n    AggregateCall<?, Void> call = (AggregateCall<?, Void>) consumer.accept(List.of(span));\n    assertThat(call.delegate())\n      .filteredOn(c -> c instanceof InsertTraceByServiceSpan)\n      .extracting(\"input.service\", \"input.span\")\n      .containsExactly(tuple(FRONTEND.serviceName(), \"\"));\n  }\n\n  @Test void traceByServiceSpan_emptyWhenNoEndpoints() {\n    Span span = spanWithoutAnnotationsOrTags.toBuilder().localEndpoint(null).build();\n\n    assertThat(consumer.accept(List.of(span)))\n      .isInstanceOf(ResultSetFutureCall.class);\n  }\n\n  @Test void searchDisabled_doesntIndex() {\n    consumer = spanConsumer(CassandraStorage.newBuilder().searchEnabled(false));\n\n    Span span = spanWithoutAnnotationsOrTags.toBuilder()\n      .addAnnotation(TODAY * 1000L, \"annotation\")\n      .putTag(\"foo\", \"bar\")\n      .duration(10000L)\n      .build();\n\n    assertThat(consumer.accept(List.of(span)))\n      .extracting(\"input.annotation_query\")\n      .satisfies(q -> assertThat(q).isNull());\n  }\n\n  @Test void doesntIndexWhenOnlyIncludesTimestamp() {\n    Span span = Span.newBuilder().traceId(\"a\").id(\"1\").timestamp(TODAY * 1000L).build();\n\n    assertThat(consumer.accept(List.of(span)))\n      .isInstanceOf(ResultSetFutureCall.class);\n  }\n\n  CassandraSpanConsumer spanConsumer(CassandraStorage.Builder builder) {\n    return new CassandraSpanConsumer(session, metadata, builder.strictTraceId,\n      builder.searchEnabled, builder.autocompleteKeys, builder.autocompleteTtl,\n      builder.autocompleteCardinality);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraSpanStoreTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Answers;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.cassandra.SelectTraceIdsFromServiceSpan.Factory.FlatMapServicesToInputs;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\n\n// TODO: tests use toString because the call composition chain is complex (includes flat mapping)\n// This could be made a little less complex if we scrub out map=>map to a list of transformations,\n// or possibly special-casing common transformations.\n@ExtendWith(MockitoExtension.class)\nclass CassandraSpanStoreTest {\n  @Mock CqlSession session;\n  Schema.Metadata metadata = new Schema.Metadata(true, true);\n  @Mock KeyspaceMetadata keyspace;\n  CassandraSpanStore spanStore;\n\n  @BeforeEach void setup() {\n    spanStore = spanStore(CassandraStorage.newBuilder().ensureSchema(false));\n  }\n\n  QueryRequest.Builder queryBuilder = QueryRequest.newBuilder().endTs(TODAY).lookback(DAY).limit(5);\n\n  @Test void timestampRange_withIndexTtlProvidedAvoidsOverflow() {\n    QueryRequest query = QueryRequest.newBuilder().endTs(TODAY).lookback(TODAY).limit(5).build();\n    CassandraSpanStore.TimestampRange timestampRange = spanStore.timestampRange(query, 7890000);\n\n    assertThat(timestampRange.startMillis).isLessThan(timestampRange.endMillis);\n  }\n\n  @Test void getTraces_fansOutAgainstServices() {\n    Call<List<List<Span>>> call = spanStore.getTraces(queryBuilder.build());\n\n    assertThat(call.toString()).contains(FlatMapServicesToInputs.class.getSimpleName());\n  }\n\n  @Test void getTraces_withSpanNameButNoServiceName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(queryBuilder.spanName(\"get\").build());\n\n    assertThat(call.toString())\n      .contains(FlatMapServicesToInputs.class.getSimpleName())\n      .contains(\"span=get\"); // no need to look at two indexes\n  }\n\n  @Test void getTraces_withTagButNoServiceName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(\n      queryBuilder.annotationQuery(Map.of(\"environment\", \"production\")).build());\n\n    assertThat(call.toString())\n      .doesNotContain(FlatMapServicesToInputs.class.getSimpleName()) // works against the span table\n      .contains(\"l_service=null, annotation_query=environment=production\");\n  }\n\n  @Test void getTraces_withDurationButNoServiceName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(queryBuilder.minDuration(1000L).build());\n\n    assertThat(call.toString())\n      .contains(FlatMapServicesToInputs.class.getSimpleName())\n      .contains(\"start_duration=1,\");\n  }\n\n  @Test void getTraces_withRemoteServiceNameButNoServiceName() {\n    Call<List<List<Span>>> call =\n      spanStore.getTraces(queryBuilder.remoteServiceName(\"backend\").build());\n\n    assertThat(call.toString())\n      .contains(FlatMapServicesToInputs.class.getSimpleName())\n      .contains(\"remote_service=backend,\")\n      .doesNotContain(\"span=\"); // no need to look at two indexes\n  }\n\n  @Test void getTraces() {\n    Call<List<List<Span>>> call = spanStore.getTraces(queryBuilder.serviceName(\"frontend\").build());\n\n    assertThat(call.toString()).contains(\"service=frontend, span=,\");\n  }\n\n  @Test void getTraces_withSpanName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(\n      queryBuilder.serviceName(\"frontend\").spanName(\"get\").build());\n\n    assertThat(call.toString())\n      .contains(\"service=frontend, span=get,\");\n  }\n\n  @Test void getTraces_withRemoteServiceName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(\n      queryBuilder.serviceName(\"frontend\").remoteServiceName(\"backend\").build());\n\n    assertThat(call.toString())\n      .contains(\"service=frontend, remote_service=backend,\")\n      .doesNotContain(\"service=frontend, span=\"); // no need to look at two indexes\n  }\n\n  @Test void getTraces_withSpanNameAndRemoteServiceName() {\n    Call<List<List<Span>>> call = spanStore.getTraces(\n      queryBuilder.serviceName(\"frontend\").remoteServiceName(\"backend\").spanName(\"get\").build());\n\n    assertThat(call.toString()) // needs to look at two indexes\n      .contains(\"service=frontend, remote_service=backend,\")\n      .contains(\"service=frontend, span=get,\");\n  }\n\n  @Test void searchDisabled_doesntMakeRemoteQueryRequests() {\n    CassandraSpanStore spanStore = spanStore(CassandraStorage.newBuilder().searchEnabled(false));\n\n    assertThat(spanStore.getTraces(queryBuilder.build())).hasToString(\"ConstantCall{value=[]}\");\n    assertThat(spanStore.getServiceNames()).hasToString(\"ConstantCall{value=[]}\");\n    assertThat(spanStore.getRemoteServiceNames(\"icecream\")).hasToString(\"ConstantCall{value=[]}\");\n    assertThat(spanStore.getSpanNames(\"icecream\")).hasToString(\"ConstantCall{value=[]}\");\n  }\n\n  CassandraSpanStore spanStore(CassandraStorage.Builder builder) {\n    return new CassandraSpanStore(session, metadata, keyspace, builder.maxTraceCols,\n      builder.indexFetchMultiplier, builder.strictTraceId, builder.searchEnabled);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraStorageBuilderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.datastax.oss.driver.api.core.config.DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nclass CassandraStorageBuilderTest {\n  CassandraStorageBuilder<?> builder = new CassandraStorageBuilder(\"zipkin3\") {\n    @Override public StorageComponent build() {\n      return null;\n    }\n  };\n\n  @Test void maxConnections_setsMaxConnectionsPerDatacenterLocalHost() {\n    assertThat(builder.maxConnections(16).poolingOptions().get(CONNECTION_POOL_LOCAL_SIZE))\n      .isEqualTo(16);\n  }\n\n  @Test void badArguments() {\n    List<Function<CassandraStorageBuilder<?>, CassandraStorageBuilder<?>>> badArguments = List.of(\n      b -> b.autocompleteTtl(0),\n      b -> b.autocompleteCardinality(0),\n      b -> b.maxTraceCols(0),\n      b -> b.indexFetchMultiplier(0)\n    );\n    badArguments.forEach(customizer ->\n      assertThatThrownBy(() -> customizer.apply(builder))\n        .isInstanceOf(IllegalArgumentException.class)\n    );\n  }\n\n  /** Ensure NPE happens early. */\n  @Test void nullPointers() {\n    List<Function<CassandraStorageBuilder<?>, CassandraStorageBuilder<?>>> nullPointers = List.of(\n      b -> b.autocompleteKeys(null),\n      b -> b.contactPoints(null),\n      b -> b.localDc(null),\n      b -> b.keyspace(null)\n    );\n    nullPointers.forEach(customizer ->\n      assertThatThrownBy(() -> customizer.apply(builder))\n        .isInstanceOf(NullPointerException.class)\n    );\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraStorageTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.AllNodesFailedException;\nimport com.datastax.oss.driver.api.core.auth.Authenticator;\nimport com.datastax.oss.driver.api.core.auth.ProgrammaticPlainTextAuthProvider;\nimport com.datastax.oss.driver.api.core.metadata.EndPoint;\nimport java.nio.ByteBuffer;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\n\nclass CassandraStorageTest {\n\n  @Test void authProvider_defaultsToNull() {\n    assertThat(CassandraStorage.newBuilder().build().authProvider)\n      .isNull();\n  }\n\n  @Test void usernamePassword_impliesNullDelimitedUtf8Bytes() throws Exception {\n    ProgrammaticPlainTextAuthProvider authProvider =\n      (ProgrammaticPlainTextAuthProvider) CassandraStorage.newBuilder()\n        .username(\"bob\")\n        .password(\"secret\")\n        .build().authProvider;\n\n    Authenticator authenticator =\n      authProvider.newAuthenticator(mock(EndPoint.class), \"serverAuthenticator\");\n\n    byte[] SASLhandshake = {0, 'b', 'o', 'b', 0, 's', 'e', 'c', 'r', 'e', 't'};\n    assertThat(authenticator.initialResponse().toCompletableFuture().get())\n      .extracting(ByteBuffer::array)\n      .isEqualTo(SASLhandshake);\n  }\n\n  @Test void check_failsInsteadOfThrowing() {\n    CheckResult result = CassandraStorage.newBuilder().contactPoints(\"1.1.1.1\").build().check();\n\n    assertThat(result.ok()).isFalse();\n    assertThat(result.error()).isInstanceOf(AllNodesFailedException.class);\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    try (CassandraStorage cassandra =\n           CassandraStorage.newBuilder().contactPoints(\"1.1.1.1\").build()) {\n\n      assertThat(cassandra)\n        .hasToString(\"CassandraStorage{contactPoints=1.1.1.1, keyspace=zipkin2}\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/CassandraUtilTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.time.ZoneOffset;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.internal.DateUtil;\nimport zipkin2.storage.QueryRequest;\n\nimport static java.util.concurrent.TimeUnit.DAYS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.TODAY;\n\nclass CassandraUtilTest {\n  @Test void annotationKeys_emptyRequest() {\n    QueryRequest request = QueryRequest.newBuilder()\n      .endTs(System.currentTimeMillis())\n      .limit(10)\n      .serviceName(\"test\")\n      .lookback(86400000L)\n      .build();\n\n    assertThat(CassandraUtil.annotationKeys(request))\n      .isEmpty();\n  }\n\n  @Test void annotationKeys() {\n    QueryRequest request = QueryRequest.newBuilder()\n      .endTs(System.currentTimeMillis())\n      .limit(10)\n      .lookback(86400000L)\n      .serviceName(\"service\")\n      .parseAnnotationQuery(\"error and http.method=GET\")\n      .build();\n\n    assertThat(CassandraUtil.annotationKeys(request))\n      .containsExactly(\"error\", \"http.method=GET\");\n  }\n\n  @Test void annotationKeys_dedupes() {\n    QueryRequest request = QueryRequest.newBuilder()\n      .endTs(System.currentTimeMillis())\n      .limit(10)\n      .lookback(86400000L)\n      .serviceName(\"service\")\n      .parseAnnotationQuery(\"error and error\")\n      .build();\n\n    assertThat(CassandraUtil.annotationKeys(request))\n      .containsExactly(\"error\");\n  }\n\n  @Test void annotationKeys_skipsTagsLongerThan256chars() {\n    // example long value\n    String arn =\n      \"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012\";\n    // example too long value\n    String url =\n      \"http://webservices.amazon.com/onca/xml?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&AssociateTag=mytag-20&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=Images%2CItemAttributes%2COffers%2CReviews&Service=AWSECommerceService&Timestamp=2014-08-18T12%3A00%3A00Z&Version=2013-08-01&Signature=j7bZM0LXZ9eXeZruTqWm2DIvDYVUU3wxPPpp%2BiXxzQc%3D\";\n\n    Span span =\n      TestObjects.CLIENT_SPAN.toBuilder().putTag(\"aws.arn\", arn).putTag(\"http.url\", url).build();\n\n    assertThat(CassandraUtil.annotationQuery(span))\n      .contains(\"aws.arn\", \"aws.arn=\" + arn)\n      .doesNotContain(\"http.url\")\n      .doesNotContain(\"http.url=\" + url);\n  }\n\n  @Test void annotationKeys_skipsAnnotationsLongerThan256chars() {\n    // example long value\n    String arn =\n      \"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012\";\n    // example too long value\n    String url =\n      \"http://webservices.amazon.com/onca/xml?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&AssociateTag=mytag-20&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=Images%2CItemAttributes%2COffers%2CReviews&Service=AWSECommerceService&Timestamp=2014-08-18T12%3A00%3A00Z&Version=2013-08-01&Signature=j7bZM0LXZ9eXeZruTqWm2DIvDYVUU3wxPPpp%2BiXxzQc%3D\";\n\n    Span span =\n      TestObjects.CLIENT_SPAN.toBuilder().addAnnotation(1L, arn).addAnnotation(1L, url).build();\n\n    assertThat(CassandraUtil.annotationQuery(span)).contains(arn).doesNotContain(url);\n  }\n\n  @Test void annotationKeys_skipsAllocationWhenNoValidInput() {\n    // example too long value\n    String url =\n      \"http://webservices.amazon.com/onca/xml?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&AssociateTag=mytag-20&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=Images%2CItemAttributes%2COffers%2CReviews&Service=AWSECommerceService&Timestamp=2014-08-18T12%3A00%3A00Z&Version=2013-08-01&Signature=j7bZM0LXZ9eXeZruTqWm2DIvDYVUU3wxPPpp%2BiXxzQc%3D\";\n\n    Span span = Span.newBuilder().traceId(\"1\").id(\"1\").build();\n\n    assertThat(CassandraUtil.annotationQuery(span)).isNull();\n\n    span = span.toBuilder().addAnnotation(1L, url).putTag(\"http.url\", url).build();\n\n    assertThat(CassandraUtil.annotationQuery(span)).isNull();\n  }\n\n  /**\n   * Sanity checks our bucketing scheme for numeric overflow\n   */\n  @Test void durationIndexBucket_notNegative() {\n    // today isn't negative\n    assertThat(CassandraUtil.durationIndexBucket(TODAY * 1000L)).isNotNegative();\n    // neither is 10 years from now\n    assertThat(CassandraUtil.durationIndexBucket((TODAY + TimeUnit.DAYS.toMillis(3654)) * 1000L))\n      .isNotNegative();\n  }\n\n  @Test void traceIdsSortedByDescTimestamp_doesntCollideOnSameTimestamp() {\n    Map<String, Long> input = new LinkedHashMap<>();\n    input.put(\"a\", 1L);\n    input.put(\"b\", 1L);\n    input.put(\"c\", 2L);\n\n    Set<String> sortedTraceIds = CassandraUtil.traceIdsSortedByDescTimestamp().map(input);\n\n    try {\n      assertThat(sortedTraceIds).containsExactly(\"c\", \"b\", \"a\");\n    } catch (AssertionError e) {\n      assertThat(sortedTraceIds).containsExactly(\"c\", \"a\", \"b\");\n    }\n  }\n\n  @Test void getDays_consistentWithDateUtil() {\n    assertThat(CassandraUtil.getDays(DAYS.toMillis(2), DAYS.toMillis(1)))\n      .extracting(d -> d.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000)\n      .containsExactlyElementsOf(DateUtil.epochDays(DAYS.toMillis(2), DAYS.toMillis(1)));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/ITCassandraStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.util.List;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\nimport zipkin2.Span;\nimport zipkin2.storage.StorageComponent.Builder;\n\nimport static zipkin2.storage.cassandra.InternalForTests.writeDependencyLinks;\nimport static zipkin2.storage.cassandra.Schema.TABLE_AUTOCOMPLETE_TAGS;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_REMOTE_SERVICES;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_SPANS;\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE;\nimport static zipkin2.storage.cassandra.Schema.TABLE_TRACE_BY_SERVICE_SPAN;\n\n@Testcontainers\n@Tag(\"docker\")\nclass ITCassandraStorage {\n  static final List<String> SEARCH_TABLES = List.of(\n    TABLE_AUTOCOMPLETE_TAGS,\n    TABLE_SERVICE_REMOTE_SERVICES,\n    TABLE_SERVICE_SPANS,\n    TABLE_TRACE_BY_SERVICE_REMOTE_SERVICE,\n    TABLE_TRACE_BY_SERVICE_SPAN\n  );\n\n  @Container static CassandraContainer cassandra = new CassandraContainer();\n\n  @Nested\n  class ITTraces extends zipkin2.storage.ITTraces<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override\n    @Test\n    @Disabled(\"No consumer-side span deduplication\")\n    public void getTrace_deduplicates(TestInfo testInfo) {\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITSpanStore extends zipkin2.storage.ITSpanStore<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITSearchEnabledFalse extends zipkin2.storage.ITSearchEnabledFalse<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITStrictTraceIdFalse extends zipkin2.storage.ITStrictTraceIdFalse<CassandraStorage> {\n    CassandraStorage strictTraceId;\n\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @BeforeEach void initializeStorageBeforeSwitch() {\n      strictTraceId = CassandraContainer.newStorageBuilder(storage.contactPoints)\n        .keyspace(storage.keyspace)\n        .build();\n    }\n\n    @AfterEach void closeStorageBeforeSwitch() {\n      if (strictTraceId != null) {\n        strictTraceId.close();\n        strictTraceId = null;\n      }\n    }\n\n    /** Ensures we can still lookup fully 128-bit traces when strict trace ID disabled */\n    @Test\n    public void getTraces_128BitTraceId(TestInfo testInfo) throws Exception {\n      getTraces_128BitTraceId(accept128BitTrace(strictTraceId, testInfo), testInfo);\n    }\n\n    /** Ensures data written before strict trace ID was enabled can be read */\n    @Test void getTrace_retrievesBy128BitTraceId_afterSwitch(TestInfo testInfo) throws Exception {\n      List<Span> trace = accept128BitTrace(strictTraceId, testInfo);\n\n      assertGetTraceReturns(trace.get(0).traceId(), trace);\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITServiceAndSpanNames extends zipkin2.storage.ITServiceAndSpanNames<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITAutocompleteTags extends zipkin2.storage.ITAutocompleteTags<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n\n  @Nested\n  class ITDependencies extends zipkin2.storage.ITDependencies<CassandraStorage> {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n\n    /**\n     * The current implementation does not include dependency aggregation. It includes retrieval of\n     * pre-aggregated links, usually made via zipkin-dependencies\n     */\n    @Override protected void processDependencies(List<Span> spans) {\n      aggregateLinks(spans).forEach(\n        (midnight, links) -> writeDependencyLinks(storage, links, midnight));\n      blockWhileInFlight();\n    }\n  }\n\n  @Nested\n  class ITSpanConsumer extends zipkin2.storage.cassandra.ITSpanConsumer {\n    @Override protected Builder newStorageBuilder(TestInfo testInfo) {\n      return cassandra.newStorageBuilder();\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      cassandra.clear(storage);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/ITCassandraStorageHeavy.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\nimport zipkin2.Span;\nimport zipkin2.storage.QueryRequest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.appendSuffix;\nimport static zipkin2.TestObjects.newTrace;\nimport static zipkin2.storage.ITDependencies.aggregateLinks;\nimport static zipkin2.storage.cassandra.InternalForTests.writeDependencyLinks;\n\n/**\n * Large amounts of writes can make other tests flake. This can happen for reasons such as\n * overloading the test Cassandra container or knock-on effects of tombstones left from {@link\n * CassandraContainer#clear(CassandraStorage)}.\n *\n * <p>Tests here share a different Cassandra container and each method runs in an isolated\n * keyspace. As schema installation takes ~10s, hesitate adding too many tests here.\n */\n@Testcontainers\n@Tag(\"docker\")\nclass ITCassandraStorageHeavy {\n\n  @Container static CassandraContainer backend = new CassandraContainer();\n\n  @Nested\n  class ITSpanStoreHeavy extends zipkin2.storage.ITSpanStoreHeavy<CassandraStorage> {\n    @Override protected CassandraStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return backend.newStorageBuilder().keyspace(InternalForTests.keyspace(testInfo));\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      // Intentionally don't clean up as each method runs in an isolated keyspace. This prevents\n      // adding more load to the shared Cassandra instance used for all tests.\n    }\n\n    @Test void overFetchesToCompensateForDuplicateIndexData(TestInfo testInfo) throws Exception {\n      String testSuffix = testSuffix(testInfo);\n      int traceCount = 2000;\n\n      List<Span> spans = new ArrayList<>();\n      for (int i = 0; i < traceCount; i++) {\n        final long delta = i * 1000; // all timestamps happen a millisecond later\n        for (Span s : newTrace(testSuffix)) {\n          Span.Builder builder = s.toBuilder()\n            .timestamp(s.timestampAsLong() + delta)\n            .clearAnnotations();\n          s.annotations().forEach(a -> builder.addAnnotation(a.timestamp() + delta, a.value()));\n          spans.add(builder.build());\n        }\n      }\n\n      accept(spans.toArray(new Span[0]));\n\n      // Index ends up containing more rows than services * trace count, and cannot be de-duped\n      // in a server-side query.\n      int localServiceCount = storage.serviceAndSpanNames().getServiceNames().execute().size();\n      assertThat(storage\n        .session()\n        .execute(\"SELECT COUNT(*) from trace_by_service_span\")\n        .one()\n        .getLong(0))\n        .isGreaterThan((long) traceCount * localServiceCount);\n\n      // Implementation over-fetches on the index to allow the user to receive unsurprising results.\n      QueryRequest request = requestBuilder()\n        // Ensure we use serviceName so that trace_by_service_span is used\n        .serviceName(appendSuffix(FRONTEND.serviceName(), testSuffix))\n        .lookback(DAY).limit(traceCount).build();\n      assertThat(store().getTraces(request).execute())\n        .hasSize(traceCount);\n    }\n  }\n\n  @Nested\n  class ITDependenciesHeavy extends zipkin2.storage.ITDependenciesHeavy<CassandraStorage> {\n    @Override protected CassandraStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return backend.newStorageBuilder().keyspace(InternalForTests.keyspace(testInfo));\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      // Intentionally don't clean up as each method runs in an isolated keyspace. This prevents\n      // adding more load to the shared Cassandra instance used for all tests.\n    }\n\n    /**\n     * The current implementation does not include dependency aggregation. It includes retrieval of\n     * pre-aggregated links, usually made via zipkin-dependencies\n     */\n    @Override protected void processDependencies(List<Span> spans) {\n      aggregateLinks(spans).forEach(\n        (midnight, links) -> writeDependencyLinks(storage, links, midnight));\n      blockWhileInFlight();\n    }\n  }\n\n  @Nested\n  class ITEnsureSchema extends zipkin2.storage.cassandra.ITEnsureSchema {\n    @Override protected CassandraStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return backend.newStorageBuilder().keyspace(InternalForTests.keyspace(testInfo));\n    }\n\n    @Override CqlSession session() {\n      return backend.globalSession;\n    }\n\n    @Override protected void blockWhileInFlight() {\n      CassandraContainer.blockWhileInFlight(storage);\n    }\n\n    @Override public void clear() {\n      // Intentionally don't clean up as each method runs in an isolated keyspace. This prevents\n      // adding more load to the shared Cassandra instance used for all tests.\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/ITEnsureSchema.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.Version;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.CheckResult;\nimport zipkin2.Span;\nimport zipkin2.storage.ITStorage;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.StorageComponent;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.appendSuffix;\nimport static zipkin2.TestObjects.newTrace;\nimport static zipkin2.storage.cassandra.ITCassandraStorage.SEARCH_TABLES;\nimport static zipkin2.storage.cassandra.Schema.TABLE_DEPENDENCY;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SPAN;\n\n/** This test is very slow as installing the schema can take 10s per method. */\nabstract class ITEnsureSchema extends ITStorage<CassandraStorage> {\n  @Override protected abstract CassandraStorage.Builder newStorageBuilder(TestInfo testInfo);\n\n  @Override protected void configureStorageForTest(StorageComponent.Builder storage) {\n    ((CassandraStorage.Builder) storage)\n      .ensureSchema(false).autocompleteKeys(List.of(\"environment\"));\n  }\n\n  @Override protected boolean initializeStoragePerTest() {\n    return true; // We need a different keyspace per test\n  }\n\n  @Override protected void checkStorage() {\n    // don't check as it requires the keyspace which these tests install\n  }\n\n  abstract CqlSession session();\n\n  @Test void installsKeyspaceWhenMissing() {\n    Schema.ensureExists(session(), storage.keyspace, true);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n    assertThat(metadata).isNotNull();\n  }\n\n  @Test void installsKeyspaceWhenMissing_searchDisabled() {\n    Schema.ensureExists(session(), storage.keyspace, false);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n    assertThat(metadata).isNotNull();\n  }\n\n  @Test void installsTablesWhenMissing() {\n    session().execute(\"CREATE KEYSPACE \" + storage.keyspace\n      + \" WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'};\");\n\n    Schema.ensureExists(session(), storage.keyspace, false);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n    assertThat(metadata.getTable(TABLE_SPAN)).isNotNull();\n    assertThat(metadata.getTable(TABLE_DEPENDENCY)).isNotNull();\n\n    for (String searchTable : SEARCH_TABLES) {\n      assertThat(metadata.getTable(searchTable))\n        .withFailMessage(\"Expected to not find \" + searchTable).isEmpty();\n    }\n  }\n\n  @Test void installsSearchTablesWhenMissing() {\n    session().execute(\"CREATE KEYSPACE \" + storage.keyspace\n      + \" WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'};\");\n\n    Schema.ensureExists(session(), storage.keyspace, true);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n\n    for (String searchTable : SEARCH_TABLES) {\n      assertThat(metadata.getTable(searchTable))\n        .withFailMessage(\"Expected to find \" + searchTable).isPresent();\n    }\n  }\n\n  @Test void upgradesOldSchema_autocomplete() {\n    Version version = Schema.ensureVersion(session().getMetadata());\n    Schema.applyCqlFile(version, storage.keyspace, session(), \"/zipkin2-schema.cql\");\n    Schema.applyCqlFile(version, storage.keyspace, session(),\n      \"/zipkin2-schema-indexes-original.cql\");\n\n    Schema.ensureExists(session(), storage.keyspace, true);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n    assertThat(Schema.hasUpgrade1_autocompleteTags(metadata)).isTrue();\n  }\n\n  @Test void upgradesOldSchema_remoteService() {\n    Version version = Schema.ensureVersion(session().getMetadata());\n    Schema.applyCqlFile(version, storage.keyspace, session(), \"/zipkin2-schema.cql\");\n    Schema.applyCqlFile(version, storage.keyspace, session(),\n      \"/zipkin2-schema-indexes-original.cql\");\n    Schema.applyCqlFile(version, storage.keyspace, session(), \"/zipkin2-schema-upgrade-1.cql\");\n\n    Schema.ensureExists(session(), storage.keyspace, true);\n\n    KeyspaceMetadata metadata = session().getMetadata().getKeyspace(storage.keyspace).get();\n    assertThat(Schema.hasUpgrade2_remoteService(metadata)).isTrue();\n  }\n\n  /** This tests we don't accidentally rely on new indexes such as autocomplete tags */\n  @Test void worksWithOldSchema(TestInfo testInfo) throws Exception {\n    Version version = Schema.ensureVersion(session().getMetadata());\n    String testSuffix = testSuffix(testInfo);\n    Schema.applyCqlFile(version, storage.keyspace, session(), \"/zipkin2-schema.cql\");\n    Schema.applyCqlFile(version, storage.keyspace, session(),\n      \"/zipkin2-schema-indexes-original.cql\");\n\n    // Ensure the storage component is functional before proceeding\n    CheckResult check = storage.check();\n    if (!check.ok()) {\n      throw new AssertionError(\"Could not connect to storage: \"\n        + check.error().getMessage(), check.error());\n    }\n\n    List<Span> trace = newTrace(testSuffix);\n\n    accept(trace);\n\n    assertGetTraceReturns(trace.get(0).traceId(), trace);\n\n    assertThat(storage.autocompleteTags().getValues(\"environment\").execute())\n      .isEmpty(); // instead of an exception\n    String serviceName = trace.get(0).localServiceName();\n    assertThat(storage.serviceAndSpanNames().getRemoteServiceNames(serviceName).execute())\n      .isEmpty(); // instead of an exception\n\n    QueryRequest request = requestBuilder()\n      .serviceName(serviceName)\n      .remoteServiceName(appendSuffix(BACKEND.serviceName(), testSuffix)).build();\n\n    // Make sure there's an error if a query will return incorrectly vs returning invalid results\n    assertThatThrownBy(() -> storage.spanStore().getTraces(request))\n      .isInstanceOf(IllegalArgumentException.class)\n      .hasMessage(\"remoteService=\" + trace.get(1).remoteServiceName() +\n        \" unsupported due to missing table remote_service_by_service\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/ITSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.internal.AggregateCall;\nimport zipkin2.storage.ITStorage;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.cassandra.internal.call.InsertEntry;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.Span.Kind.SERVER;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.newClientSpan;\nimport static zipkin2.TestObjects.spanBuilder;\n\nabstract class ITSpanConsumer extends ITStorage<CassandraStorage> {\n  @Override protected void configureStorageForTest(StorageComponent.Builder storage) {\n    storage.autocompleteKeys(List.of(\"environment\"));\n  }\n\n  /**\n   * {@link Span#timestamp()} == 0 is likely to be a mistake, and coerces to null. It is not helpful\n   * to index rows who have no timestamp.\n   */\n  @Test void doesntIndexSpansMissingTimestamp(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    accept(spanBuilder(testSuffix).timestamp(0L).duration(0L).build());\n\n    assertThat(rowCountForTraceByServiceSpan(storage)).isZero();\n  }\n\n  /**\n   * Simulates a trace with a step pattern, where each span starts a millisecond after the prior\n   * one. The consumer code optimizes index inserts to only represent the interval represented by\n   * the trace as opposed to each individual timestamp.\n   */\n  @Test void skipsRedundantIndexingInATrace(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span[] trace = new Span[101];\n    trace[0] = newClientSpan(testSuffix).toBuilder().kind(SERVER).build();\n\n    IntStream.range(0, 100).forEach(i -> trace[i + 1] = Span.newBuilder()\n      .traceId(trace[0].traceId())\n      .parentId(trace[0].id())\n      .id(i + 1)\n      .name(\"get\")\n      .kind(Span.Kind.CLIENT)\n      .localEndpoint(trace[0].localEndpoint())\n      .timestamp(\n        trace[0].timestampAsLong() + i * 1000L) // all peer span timestamps happen 1ms later\n      .duration(10L)\n      .build());\n\n    accept(trace);\n    assertThat(rowCountForTraceByServiceSpan(storage))\n      .isGreaterThanOrEqualTo(4L);\n    assertThat(rowCountForTraceByServiceSpan(storage))\n      .isGreaterThanOrEqualTo(4L);\n\n    CassandraSpanConsumer withoutStrictTraceId = new CassandraSpanConsumer(\n      storage.session(), storage.metadata(),\n      false /* strictTraceId */, storage.searchEnabled,\n      storage.autocompleteKeys, storage.autocompleteTtl, storage.autocompleteCardinality\n    );\n\n    // sanity check base case\n    withoutStrictTraceId.accept(List.of(trace)).execute();\n    blockWhileInFlight();\n\n    assertThat(rowCountForTraceByServiceSpan(storage))\n      .isGreaterThanOrEqualTo(120L); // TODO: magic number\n    assertThat(rowCountForTraceByServiceSpan(storage))\n      .isGreaterThanOrEqualTo(120L);\n  }\n\n  @Test void insertTags_SelectTags_CalculateCount(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span[] trace = new Span[101];\n    trace[0] = newClientSpan(testSuffix).toBuilder().kind(SERVER).build();\n\n    IntStream.range(0, 100).forEach(i -> trace[i + 1] = Span.newBuilder()\n      .traceId(trace[0].traceId())\n      .parentId(trace[0].id())\n      .id(i + 1)\n      .name(\"get\")\n      .kind(Span.Kind.CLIENT)\n      .localEndpoint(trace[0].localEndpoint())\n      .putTag(\"environment\", \"dev\")\n      .putTag(\"a\", \"b\")\n      .timestamp(\n        trace[0].timestampAsLong() + i * 1000L) // all peer span timestamps happen 1ms later\n      .duration(10L)\n      .build());\n\n    accept(trace);\n\n    assertThat(rowCountForTags(storage))\n      .isEqualTo(1L); // Since tag {a,b} are not in the whitelist\n\n    assertThat(getTagValue(storage, \"environment\")).isEqualTo(\"dev\");\n  }\n\n  /** It is easier to use a real Cassandra connection than mock a prepared statement. */\n  @Test void insertEntry_niceToString() {\n    // This test can use fake data as it is never written to cassandra\n    AggregateCall<?, ?> acceptCall =\n      (AggregateCall<?, ?>) storage.spanConsumer().accept(List.of(CLIENT_SPAN));\n\n    List<Call<?>> insertEntryCalls = acceptCall.delegate().stream()\n      .filter(c -> c instanceof InsertEntry)\n      .collect(Collectors.toList());\n\n    assertThat(insertEntryCalls.get(0))\n      .hasToString(\"INSERT INTO span_by_service (service, span) VALUES (frontend,get)\");\n    assertThat(insertEntryCalls.get(1))\n      .hasToString(\n        \"INSERT INTO remote_service_by_service (service, remote_service) VALUES (frontend,backend)\");\n  }\n\n  static long rowCountForTraceByServiceSpan(CassandraStorage storage) {\n    return storage\n      .session()\n      .execute(\"SELECT COUNT(*) from \" + Schema.TABLE_TRACE_BY_SERVICE_SPAN)\n      .one()\n      .getLong(0);\n  }\n\n  static long rowCountForTags(CassandraStorage storage) {\n    return storage\n      .session()\n      .execute(\"SELECT COUNT(*) from \" + Schema.TABLE_AUTOCOMPLETE_TAGS)\n      .one()\n      .getLong(0);\n  }\n\n  static String getTagValue(CassandraStorage storage, String key) {\n    return storage\n      .session()\n      .execute(\"SELECT value from \" + Schema.TABLE_AUTOCOMPLETE_TAGS + \" WHERE key='\" + key + \"'\")\n      .one()\n      .getString(0);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/InternalForTests.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.Version;\nimport com.datastax.oss.driver.api.core.cql.PreparedStatement;\nimport com.datastax.oss.driver.api.core.metadata.Metadata;\nimport com.datastax.oss.driver.api.core.metadata.Node;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.ZoneOffset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.DependencyLink;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static zipkin2.storage.cassandra.Schema.TABLE_SERVICE_REMOTE_SERVICES;\n\nclass InternalForTests {\n  static CqlSession mockSession() {\n    CqlSession session = mock(CqlSession.class);\n    Metadata metadata = mock(Metadata.class);\n    Node node = mock(Node.class);\n\n    when(session.getMetadata()).thenReturn(metadata);\n    when(metadata.getNodes()).thenReturn(Map.of(\n      UUID.fromString(\"11111111-1111-1111-1111-111111111111\"), node\n    ));\n    when(node.getCassandraVersion()).thenReturn(Version.parse(\"3.11.9\"));\n\n    KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);\n    when(session.getMetadata()).thenReturn(metadata);\n    when(metadata.getKeyspace(\"zipkin2\")).thenReturn(Optional.of(keyspaceMetadata));\n\n    when(keyspaceMetadata.getTable(TABLE_SERVICE_REMOTE_SERVICES))\n      .thenReturn(Optional.of(mock(TableMetadata.class)));\n    return session;\n  }\n\n  static void writeDependencyLinks(\n    CassandraStorage storage, List<DependencyLink> links, long midnightUTC) {\n    CqlSession session = storage.session();\n    PreparedStatement prepared = session.prepare(\"INSERT INTO \" + Schema.TABLE_DEPENDENCY\n      + \" (day,parent,child,calls,errors)\"\n      + \" VALUES (?,?,?,?,?)\");\n    LocalDate day = Instant.ofEpochMilli(midnightUTC).atZone(ZoneOffset.UTC).toLocalDate();\n    for (DependencyLink link : links) {\n      int i = 0;\n      storage.session().execute(prepared.bind()\n        .setLocalDate(i++, day)\n        .setString(i++, link.parent())\n        .setString(i++, link.child())\n        .setLong(i++, link.callCount())\n        .setLong(i, link.errorCount()));\n    }\n  }\n\n  static String keyspace(TestInfo testInfo) {\n    String result;\n    if (testInfo.getTestMethod().isPresent()) {\n      result = testInfo.getTestMethod().get().getName();\n    } else {\n      assert testInfo.getTestClass().isPresent();\n      result = testInfo.getTestClass().get().getSimpleName();\n    }\n    result = result.toLowerCase();\n    return result.length() <= 48 ? result : result.substring(result.length() - 48);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/SchemaTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra;\n\nimport com.datastax.oss.driver.api.core.CqlSession;\nimport com.datastax.oss.driver.api.core.Version;\nimport com.datastax.oss.driver.api.core.metadata.Metadata;\nimport com.datastax.oss.driver.api.core.metadata.Node;\nimport com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nclass SchemaTest {\n  @Test void ensureVersion_failsWhenVersionLessThan3_11_3() {\n    Metadata metadata = mock(Metadata.class);\n    Node node = mock(Node.class);\n\n    when(metadata.getNodes()).thenReturn(Map.of(\n      UUID.fromString(\"11111111-1111-1111-1111-111111111111\"), node\n    ));\n    when(node.getCassandraVersion()).thenReturn(Version.parse(\"3.11.2\"));\n\n    assertThatThrownBy(() -> Schema.ensureVersion(metadata))\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\n        \"Node 11111111-1111-1111-1111-111111111111 is running Cassandra 3.11.2, but minimum version is 3.11.3\");\n  }\n\n  @Test void ensureVersion_failsWhenOneVersionLessThan3_11_3() {\n    Metadata metadata = mock(Metadata.class);\n    Node node1 = mock(Node.class);\n    Node node2 = mock(Node.class);\n    Map<UUID, Node> nodes = new LinkedHashMap<>();\n    nodes.put(UUID.fromString(\"11111111-1111-1111-1111-111111111111\"), node1);\n    nodes.put(UUID.fromString(\"22222222-2222-2222-2222-222222222222\"), node2);\n\n    when(metadata.getNodes()).thenReturn(nodes);\n    when(node1.getCassandraVersion()).thenReturn(Version.parse(\"3.11.3\"));\n    when(node2.getCassandraVersion()).thenReturn(Version.parse(\"3.11.2\"));\n\n    assertThatThrownBy(() -> Schema.ensureVersion(metadata))\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\n        \"Node 22222222-2222-2222-2222-222222222222 is running Cassandra 3.11.2, but minimum version is 3.11.3\");\n  }\n\n  @Test void ensureVersion_passesWhenVersion3_11_3() {\n    Metadata metadata = mock(Metadata.class);\n    Node node = mock(Node.class);\n\n    when(metadata.getNodes()).thenReturn(Map.of(\n      UUID.fromString(\"11111111-1111-1111-1111-111111111111\"), node\n    ));\n    when(node.getCassandraVersion()).thenReturn(Version.parse(\"3.11.3\"));\n\n    assertThat(Schema.ensureVersion(metadata))\n      .isEqualTo(Version.parse(\"3.11.3\"));\n  }\n\n  @Test void ensureVersion_passesWhenVersion3_11_4() {\n    Metadata metadata = mock(Metadata.class);\n    Node node = mock(Node.class);\n\n    when(metadata.getNodes()).thenReturn(Map.of(\n      UUID.fromString(\"11111111-1111-1111-1111-111111111111\"), node\n    ));\n    when(node.getCassandraVersion()).thenReturn(Version.parse(\"3.11.4\"));\n\n    assertThat(Schema.ensureVersion(metadata))\n      .isEqualTo(Version.parse(\"3.11.4\"));\n  }\n\n  @Test void ensureKeyspaceMetadata() {\n    CqlSession session = mock(CqlSession.class);\n    Metadata metadata = mock(Metadata.class);\n    when(session.getMetadata()).thenReturn(metadata);\n    KeyspaceMetadata keyspaceMetadata = mock(KeyspaceMetadata.class);\n    when(metadata.getKeyspace(\"zipkin2\")).thenReturn(Optional.of(keyspaceMetadata));\n\n    assertThat(Schema.ensureKeyspaceMetadata(session, \"zipkin2\"))\n      .isSameAs(keyspaceMetadata);\n  }\n\n  @Test void ensureKeyspaceMetadata_failsWhenKeyspaceMetadataIsNull() {\n    CqlSession session = mock(CqlSession.class);\n    Metadata metadata = mock(Metadata.class);\n\n    when(session.getMetadata()).thenReturn(metadata);\n\n    assertThatThrownBy(() -> Schema.ensureKeyspaceMetadata(session, \"zipkin2\"))\n      .isInstanceOf(RuntimeException.class)\n      .hasMessageStartingWith(\"Cannot read keyspace metadata for keyspace\");\n  }\n\n  String schemaWithReadRepair = \"\"\"\n    CREATE TABLE IF NOT EXISTS zipkin2.remote_service_by_service (\n        service text,\n        remote_service text,\n        PRIMARY KEY (service, remote_service)\n    )\n        WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n        AND caching = {'rows_per_partition': 'ALL'}\n        AND default_time_to_live =  259200\n        AND gc_grace_seconds = 3600\n        AND read_repair_chance = 0\n        AND dclocal_read_repair_chance = 0\n        AND speculative_retry = '95percentile'\n        AND comment = 'Secondary table for looking up remote service names by a service name.';\\\n    \"\"\";\n\n  @Test void reviseCql_leaves_read_repair_chance_on_v3() {\n    assertThat(Schema.reviseCQL(Version.parse(\"3.11.9\"), schemaWithReadRepair))\n      .isSameAs(schemaWithReadRepair);\n  }\n\n  @Test void reviseCql_removes_dclocal_read_repair_chance_on_v4() {\n    assertThat(Schema.reviseCQL(Version.V4_0_0, schemaWithReadRepair))\n      // literal used to show newlines etc. are in-tact\n      .isEqualTo(\"\"\"\n        CREATE TABLE IF NOT EXISTS zipkin2.remote_service_by_service (\n            service text,\n            remote_service text,\n            PRIMARY KEY (service, remote_service)\n        )\n            WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n            AND caching = {'rows_per_partition': 'ALL'}\n            AND default_time_to_live =  259200\n            AND gc_grace_seconds = 3600\n            AND speculative_retry = '95percentile'\n            AND comment = 'Secondary table for looking up remote service names by a service name.';\\\n        \"\"\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/internal/HostAndPortTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\n// Reuses inputs from com.google.common.net.HostAndPortTest\nclass HostAndPortTest {\n\n  @Test void parsesHost() {\n    Stream.of(\n      \"google.com\",\n      \"google.com\",\n      \"192.0.2.1\",\n      \"2001::3\"\n    ).forEach(host -> assertThat(HostAndPort.fromString(host, 77))\n      .isEqualTo(new HostAndPort(host, 77)));\n  }\n\n  @Test void parsesHost_emptyPortOk() {\n    assertThat(HostAndPort.fromString(\"gmail.com:\", 77))\n      .isEqualTo(new HostAndPort(\"gmail.com\", 77));\n\n    assertThat(HostAndPort.fromString(\"192.0.2.2:\", 77))\n      .isEqualTo(new HostAndPort(\"192.0.2.2\", 77));\n\n    assertThat(HostAndPort.fromString(\"[2001::2]:\", 77))\n      .isEqualTo(new HostAndPort(\"2001::2\", 77));\n  }\n\n  @Test void parsesHostAndPort() {\n    assertThat(HostAndPort.fromString(\"gmail.com:77\", 1))\n      .isEqualTo(new HostAndPort(\"gmail.com\", 77));\n\n    assertThat(HostAndPort.fromString(\"192.0.2.2:77\", 1))\n      .isEqualTo(new HostAndPort(\"192.0.2.2\", 77));\n\n    assertThat(HostAndPort.fromString(\"[2001::2]:77\", 1))\n      .isEqualTo(new HostAndPort(\"2001::2\", 77));\n  }\n\n  @Test void throwsOnInvalidInput() {\n    Stream.of(\n      \"google.com:65536\",\n      \"google.com:9999999999\",\n      \"google.com:port\",\n      \"google.com:-25\",\n      \"google.com:+25\",\n      \"google.com:25  \",\n      \"google.com:25\\t\",\n      \"google.com:0x25 \",\n      \"[goo.gl]\",\n      \"[goo.gl]:80\",\n      \"[\",\n      \"[]:\",\n      \"[]:80\",\n      \"[]bad\",\n      \"[[:]]\",\n      \"x:y:z\",\n      \"\",\n      \":\",\n      \":123\"\n    ).forEach(hostPort -> {\n      try {\n        HostAndPort.fromString(hostPort, 77);\n        throw new AssertionError(hostPort + \" should have failed to parse\");\n      } catch (IllegalArgumentException e) {\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/internal/SessionBuilderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal;\n\nimport java.net.InetSocketAddress;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.storage.cassandra.internal.SessionBuilder.parseContactPoints;\n\nclass SessionBuilderTest {\n  @Test void contactPoints_defaultsToLocalhost() {\n    assertThat(parseContactPoints(\"localhost\"))\n      .containsExactly(new InetSocketAddress(\"127.0.0.1\", 9042));\n  }\n\n  @Test void contactPoints_defaultsToPort9042() {\n    assertThat(parseContactPoints(\"1.1.1.1\"))\n      .containsExactly(new InetSocketAddress(\"1.1.1.1\", 9042));\n  }\n\n  @Test void contactPoints_defaultsToPort9042_multi() {\n    assertThat(parseContactPoints(\"1.1.1.1:9143,2.2.2.2\"))\n      .containsExactly(\n        new InetSocketAddress(\"1.1.1.1\", 9143), new InetSocketAddress(\"2.2.2.2\", 9042));\n  }\n\n  @Test void contactPoints_hostAndPort() {\n    assertThat(parseContactPoints(\"1.1.1.1:9142\"))\n      .containsExactly(new InetSocketAddress(\"1.1.1.1\", 9142));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/internal/call/DeduplicatingInsertTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static org.mockito.Mockito.mock;\n\nclass DeduplicatingInsertTest {\n  @Test void dedupesSameCalls() throws Exception {\n    TestFactory testFactory = new TestFactory();\n\n    List<Call<Void>> calls = new ArrayList<>();\n    testFactory.maybeAdd(\"foo\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n    testFactory.maybeAdd(\"foo\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n    assertThat(calls).hasSize(2);\n\n    for (Call<Void> call : calls) {\n      call.execute();\n    }\n    assertThat(testFactory.values).containsExactly(\"foo\", \"bar\");\n  }\n\n  Callback<Void> assertFailOnError = new Callback<>() {\n    @Override public void onSuccess(Void value) {\n    }\n\n    @Override public void onError(Throwable t) {\n      throw (AssertionError) t;\n    }\n  };\n\n  @Test void enqueuesInOrder() {\n    TestFactory testFactory = new TestFactory();\n\n    List<Call<Void>> calls = new ArrayList<>();\n    testFactory.maybeAdd(\"foo\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n\n    for (Call<Void> call : calls) {\n      call.enqueue(assertFailOnError);\n    }\n    assertThat(testFactory.values).containsExactly(\"foo\", \"bar\");\n  }\n\n  @Disabled(\"Flakey: https://github.com/openzipkin/zipkin/issues/3255\")\n  @Test void exceptionsInvalidate_enqueue() {\n    TestFactory testFactory = new TestFactory();\n\n    List<Call<Void>> calls = new ArrayList<>();\n    testFactory.maybeAdd(\"foo\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n\n    testFactory.failValue.set(\"foo\");\n\n    try {\n      calls.get(0).enqueue(assertFailOnError);\n      failBecauseExceptionWasNotThrown(AssertionError.class);\n    } catch (AssertionError e) {\n    }\n\n    calls.get(1).enqueue(assertFailOnError);\n    assertThat(testFactory.values).containsExactly(\"bar\");\n\n    calls.clear();\n    testFactory.maybeAdd(\"foo\", calls);\n    assertThat(calls).isNotEmpty(); // invalidates on exception\n\n    calls.get(0).enqueue(assertFailOnError);\n    assertThat(testFactory.values).containsExactly(\"bar\", \"foo\");\n  }\n\n  @Test void exceptionsInvalidate_execute() throws Exception {\n    TestFactory testFactory = new TestFactory();\n\n    List<Call<Void>> calls = new ArrayList<>();\n    testFactory.maybeAdd(\"foo\", calls);\n    testFactory.maybeAdd(\"bar\", calls);\n\n    testFactory.failValue.set(\"foo\");\n\n    try {\n      calls.get(0).execute();\n      failBecauseExceptionWasNotThrown(AssertionError.class);\n    } catch (AssertionError e) {\n    }\n\n    calls.get(1).execute();\n    assertThat(testFactory.values).containsExactly(\"bar\");\n\n    calls.clear();\n    testFactory.maybeAdd(\"foo\", calls);\n    assertThat(calls).isNotEmpty(); // invalidates on exception\n\n    calls.get(0).execute();\n    assertThat(testFactory.values).containsExactly(\"bar\", \"foo\");\n  }\n\n  static final class TestFactory extends DeduplicatingInsert.Factory<String> {\n    List<String> values = new ArrayList<>();\n    AtomicReference<String> failValue = new AtomicReference<>();\n\n    TestFactory() {\n      super(1000, 1000);\n    }\n\n    @Override protected Call<Void> newCall(String string) {\n      return new TestDeduplicatingInsert(this, string);\n    }\n  }\n\n  static final class TestDeduplicatingInsert extends DeduplicatingInsert<String> {\n    final TestFactory factory;\n\n    TestDeduplicatingInsert(TestFactory factory, String input) {\n      super(factory.delayLimiter, input);\n      this.factory = factory;\n    }\n\n    @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n      if (input.equals(factory.failValue.get())) {\n        factory.failValue.set(null);\n        CompletableFuture<AsyncResultSet> result = new CompletableFuture<>();\n        result.completeExceptionally(new AssertionError());\n        return result;\n      }\n      factory.values.add(input);\n      return CompletableFuture.completedFuture(mock(AsyncResultSet.class));\n    }\n\n    @Override public Call<Void> clone() {\n      return new TestDeduplicatingInsert(factory, input);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/java/zipkin2/storage/cassandra/internal/call/ResultSetFutureCallTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.cassandra.internal.call;\n\nimport com.datastax.oss.driver.api.core.RequestThrottlingException;\nimport com.datastax.oss.driver.api.core.connection.BusyConnectionException;\nimport com.datastax.oss.driver.api.core.cql.AsyncResultSet;\nimport com.datastax.oss.driver.api.core.servererrors.QueryConsistencyException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.mock;\n\nclass ResultSetFutureCallTest {\n  CompletableFuture<AsyncResultSet> future = new CompletableFuture<>();\n  AsyncResultSet resultSet = mock(AsyncResultSet.class);\n\n  ResultSetFutureCall<AsyncResultSet> call = new ResultSetFutureCall<>() {\n    @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n      return ResultSetFutureCallTest.this.future;\n    }\n\n    @Override public Call<AsyncResultSet> clone() {\n      return null;\n    }\n\n    @Override public AsyncResultSet map(AsyncResultSet input) {\n      return input;\n    }\n  };\n\n  static final class CompletableCallback<T> extends CompletableFuture<T> implements Callback<T> {\n    @Override public void onSuccess(T value) {\n      complete(value);\n    }\n\n    @Override public void onError(Throwable t) {\n      completeExceptionally(t);\n    }\n  }\n\n  CompletableCallback<AsyncResultSet> callback = new CompletableCallback<>();\n\n  @Test void enqueue_cancel_beforeCreateFuture() {\n    call.cancel();\n\n    assertThat(call.isCanceled()).isTrue();\n  }\n\n  @Test void enqueue_callsFutureGet() throws Exception {\n    call.enqueue(callback);\n\n    future.complete(resultSet);\n\n    assertThat(callback.get()).isEqualTo(resultSet);\n  }\n\n  @Test void enqueue_cancel_afterEnqueue() {\n    call.enqueue(callback);\n    call.cancel();\n\n    assertThat(call.isCanceled()).isTrue();\n    // this.future will be wrapped, so can't check if that is canceled.\n    assertThat(call.future.isCancelled()).isTrue();\n  }\n\n  @Test void enqueue_callbackError_onErrorCreatingFuture() {\n    IllegalArgumentException error = new IllegalArgumentException();\n    call = new ResultSetFutureCall<>() {\n      @Override protected CompletionStage<AsyncResultSet> newCompletionStage() {\n        throw error;\n      }\n\n      @Override public Call<AsyncResultSet> clone() {\n        return null;\n      }\n\n      @Override public AsyncResultSet map(AsyncResultSet input) {\n        return input;\n      }\n    };\n\n    call.enqueue(callback);\n\n    // ensure the callback received the exception\n    assertThat(callback.isCompletedExceptionally()).isTrue();\n    assertThatThrownBy(callback::get).hasCause(error);\n  }\n\n  // below are load related exceptions which should result in a backoff of storage requests\n  @Test void isOverCapacity() {\n    assertThat(ResultSetFutureCall.isOverCapacity(\n      new RequestThrottlingException(\"The session is shutting down\"))).isTrue();\n    assertThat(ResultSetFutureCall.isOverCapacity(new BusyConnectionException(100))).isTrue();\n    assertThat(ResultSetFutureCall.isOverCapacity(mock(QueryConsistencyException.class))).isTrue();\n\n    // not applicable\n    assertThat(ResultSetFutureCall.isOverCapacity(\n      new IllegalStateException(\"Rejected execution\"))).isFalse();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/autocomplete_tags-stress.yaml",
    "content": "###\n### Stress test for stress_zipkin2.autocomplete_tags table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=autocomplete_tags-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=autocomplete_tags-stress.yaml ops\\(insert=1,select=1,select_values=1\\)  duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: autocomplete_tags\n\n\n### Column Distribution Specifications ###\n\ncolumnspec:\n  - name: key\n    size: uniform(5..20)\n    population: uniform(1..5)\n\n  - name: value\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   select:\n    cql: SELECT DISTINCT key FROM autocomplete_tags\n    fields: samerow\n   select_values:\n    cql: SELECT value FROM autocomplete_tags WHERE key = ? LIMIT 10000\n    fields: samerow\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/remote_service_by_service-stress.yaml",
    "content": "###\n### Stress test for stress_zipkin2.service_remote_service_index table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=remote_service_by_service-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=remote_service_by_service-stress.yaml ops\\(insert=1,select=1,select_remote_services=1\\) duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: remote_service_by_service\n\n\n### Column Distribution Specifications ###\n\ncolumnspec:\n  - name: service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: remote_service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   select:\n    cql: SELECT DISTINCT service FROM remote_service_by_service\n    fields: samerow\n   select_remote_services:\n    cql: SELECT remote_service FROM remote_service_by_service WHERE service = ? LIMIT 1000\n    fields: samerow\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# Note: this will dump a large amount of data in the logs\n#org.slf4j.simpleLogger.log.zipkin2.storage.cassandra=info\n#org.slf4j.simpleLogger.log.com.datastax.oss.driver.internal.core.tracker.RequestLogger=trace\n# our tests check for schema portability, so hush lack of schema logs\norg.slf4j.simpleLogger.log.zipkin2.storage.cassandra.Schema=off\n# don't spam about SASI\norg.slf4j.simpleLogger.log.com.datastax.oss.driver.internal.core.cql.CqlRequestHandler=error\n# ignore connection close errors when polling for cassandra to start\norg.slf4j.simpleLogger.log.com.datastax.oss.driver.internal.core.control.ControlConnection=error\n# ignore warnings about too many sessions\norg.slf4j.simpleLogger.log.com.datastax.oss.driver.internal.core.session.DefaultSession=error\n# set to debug to see storage details\norg.slf4j.simpleLogger.log.zipkin2=warn\n# set to info to see feedback about starting the container\norg.slf4j.simpleLogger.log.org.testcontainers=warn\norg.slf4j.simpleLogger.log.zipkin2.storage.cassandra.CassandraContainer=info\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/span-stress.yaml",
    "content": "### Stress test for stress_zipkin2.span table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=span-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=span-stress.yaml ops\\(insert=1,by_trace=1,by_trace_ts_id=1,by_annotation=1\\)  duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: span\n\n\n### Column Distribution Specifications ###\n#\n\ncolumnspec:\n  - name: trace_id\n    size: fixed(32)\n    population: uniform(1..10k)\n\n  - name: ts_uuid\n    population: uniform(1..10k)\n\n  - name: id\n    size: fixed(32)\n    population: uniform(1..10k)\n\n  - name: ts\n    size: fixed(12)\n    population: uniform(1..10k)\n\n  - name: span\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: parent_id\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: duration\n    size: fixed(12)\n    population: uniform(1..10k)\n\n  - name: l_service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: annotation_query\n    size: gaussian(50..500)\n    population: uniform(1..1k)\n\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   by_trace:\n    cql: SELECT * FROM span WHERE trace_id = ?\n    fields: samerow\n   by_trace_ts_id:\n    cql: SELECT * FROM span WHERE trace_id = ? AND ts_uuid = ? AND id = ?\n    fields: samerow\n   by_annotation:\n    cql: SELECT trace_id, ts, id FROM span WHERE l_service = ? AND annotation_query LIKE ? ALLOW FILTERING\n    fields: samerow\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/span_by_service-stress.yaml",
    "content": "###\n### Stress test for stress_zipkin2.span_by_service table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=span_by_service-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=span_by_service-stress.yaml ops\\(insert=1,select=1,select_spans=1\\)  duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: span_by_service\n\n\n### Column Distribution Specifications ###\n\ncolumnspec:\n  - name: service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: span\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   select:\n    cql: SELECT DISTINCT service FROM span_by_service\n    fields: samerow\n   select_spans:\n    cql: SELECT span FROM span_by_service WHERE service = ? LIMIT 10000\n    fields: samerow\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/trace_by_service_remote_service-stress.yaml",
    "content": "###\n### Stress test for stress_zipkin2.trace_by_service_remote_service table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=trace_by_service_remote_service-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=trace_by_service_remote_service-stress.yaml ops\\(insert=1,select=1\\)  duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: trace_by_service_remote_service\n\n\n### Column Distribution Specifications ###\n\ncolumnspec:\n  - name: service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: remote_service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: bucket\n    size: fixed(12)\n    population: fixed(123456789012)\n\n  - name: ts\n    size: fixed(12)\n    population: uniform(1..10k)\n\n  - name: trace_id\n    size: fixed(32)\n    population: uniform(1..10k)\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   select:\n    cql: SELECT * FROM trace_by_service_remote_service WHERE service = ? AND remote_service = ? AND bucket = ? LIMIT 1\n    fields: samerow\n\n# TODO: adrian doesn't know how to make a timestamp range query from test data\n# search by timestamp range\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/trace_by_service_span-stress.yaml",
    "content": "###\n### Stress test for stress_zipkin2.trace_by_service_span table\n###\n### Stress testing is done using the `cassandra-stress` tool\n###\n### For example\n###  cqlsh -f zipkin2-test-schema.cql\n###  cassandra-stress  user profile=trace_by_service_span-stress.yaml ops\\(insert=1\\) no-warmup duration=1m  -rate threads=4 throttle=50/s\n###\n### after a benchmark has been run with only writes, a mixed read-write benchmark can be run with\n###  cassandra-stress  user profile=trace_by_service_span-stress.yaml ops\\(insert=1,select=1,by_duration=1\\)  duration=1m  -rate threads=4 throttle=50/s\n\n# Keyspace Name\nkeyspace: stress_zipkin2\n\n# Table name\ntable: trace_by_service_span\n\n\n### Column Distribution Specifications ###\n\ncolumnspec:\n  - name: service\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: span\n    size: uniform(5..20)\n    population: uniform(1..100)\n\n  - name: bucket\n    size: fixed(12)\n    population: fixed(123456789012)\n\n  - name: ts\n    size: fixed(12)\n    population: uniform(1..10k)\n\n  - name: trace_id\n    size: fixed(32)\n    population: uniform(1..10k)\n\n  - name: duration\n    size: fixed(12)\n    population: uniform(1..10k)\n\n\n\n### Batch Ratio Distribution Specifications ###\n\ninsert:\n  partitions: fixed(1)            # 1 partition key at a time inserts to model a message being generated\n  select:  fixed(1)/1000\n  batchtype: UNLOGGED             # Unlogged batches\n\n\n#\n# A set of basic queries\n#\nqueries:\n   select:\n    cql: SELECT * FROM trace_by_service_span WHERE service = ? AND span = ? AND bucket = ? LIMIT 1\n    fields: samerow\n   by_duration:\n    cql: SELECT * FROM trace_by_service_span WHERE service = ? AND span = ? AND bucket = ? AND duration < ? LIMIT 1\n    fields: samerow\n\n# TODO: adrian doesn't know how to make a timestamp range query from test data\n# search by timestamp range,\n# search by timestamp range and duration\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/zipkin2-schema-indexes-original.cql",
    "content": "ALTER TABLE zipkin2.span ADD l_service text;\nALTER TABLE zipkin2.span ADD annotation_query text; //-- can't do SASI on set<text>: ░-joined until CASSANDRA-11182\n\nCREATE TABLE IF NOT EXISTS zipkin2.trace_by_service_span (\n    service       text,             //-- service name\n    span          text,             //-- span name, or blank for queries without span name\n    bucket        int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts            timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id      text,             //-- trace ID\n    duration      bigint,           //-- span duration, in milliseconds\n    PRIMARY KEY ((service, span, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a service, or service and span. span column may be blank (when only looking up by service). bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision. duration column is span duration, rounded up to tens of milliseconds (or hundredths of seconds)';\n\nCREATE TABLE IF NOT EXISTS zipkin2.span_by_service (\n    service text,\n    span    text,\n    PRIMARY KEY (service, span)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up span names by a service name.';\n\nDROP INDEX IF EXISTS zipkin2.span_annotation_query_idx;\n\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.span (annotation_query) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {\n    'mode': 'PREFIX',\n    'analyzed': 'true',\n    'analyzer_class':'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer',\n    'delimiter': '░'};\n\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.span (l_service) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\n\nCREATE CUSTOM INDEX IF NOT EXISTS ON zipkin2.trace_by_service_span (duration) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\n"
  },
  {
    "path": "zipkin-storage/cassandra/src/test/resources/zipkin2-test-schema.cql",
    "content": "CREATE KEYSPACE IF NOT EXISTS stress_zipkin2 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = false;\n\n//-- same schema but remove all UDTs and collections (as cassandra-stress doesn't support them)\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.span (\n    trace_id            text, // when strictTraceId=false, only contains right-most 16 chars\n    ts_uuid             timeuuid,\n    id                  text,\n    trace_id_high       text, // when strictTraceId=false, contains left-most 16 chars if present\n    parent_id           text,\n    kind                text,\n    span                text, // span.name\n    ts                  bigint,\n    duration            bigint,\n    shared              boolean,\n    debug               boolean,\n    l_service           text,\n    annotation_query    text, //-- can't do SASI on set<text>: ░-joined until CASSANDRA-11182\n    PRIMARY KEY (trace_id, ts_uuid, id)\n)\n    WITH CLUSTERING ORDER BY (ts_uuid DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  604800\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0.0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Primary table for holding trace data';\nCREATE CUSTOM INDEX IF NOT EXISTS ON stress_zipkin2.span (l_service) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\nCREATE CUSTOM INDEX IF NOT EXISTS ON stress_zipkin2.span (annotation_query) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {\n    'mode': 'PREFIX',\n    'analyzed': 'true',\n    'analyzer_class':'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer',\n    'delimiter': '░'};\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.trace_by_service_span (\n    service       text,             //-- service name\n    span          text,             //-- span name, or blank for queries without span name\n    bucket        int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts            timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id      text,             //-- trace ID\n    duration      bigint,           //-- span duration, in milliseconds\n    PRIMARY KEY ((service, span, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a service, or service and span. span column may be blank (when only looking up by service). bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision. duration column is span duration, rounded up to tens of milliseconds (or hundredths of seconds)';\nCREATE CUSTOM INDEX IF NOT EXISTS ON stress_zipkin2.trace_by_service_span (duration) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n   WITH OPTIONS = {'mode': 'PREFIX'};\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.trace_by_service_remote_service (\n    service         text,             //-- service name\n    remote_service  text,             //-- remote servie name\n    bucket          int,              //-- time bucket, calculated as ts/interval (in microseconds), for some pre-configured interval like 1 day.\n    ts              timeuuid,         //-- start timestamp of the span, truncated to millisecond precision\n    trace_id        text,             //-- trace ID\n    PRIMARY KEY ((service, remote_service, bucket), ts)\n)\n   WITH CLUSTERING ORDER BY (ts DESC)\n    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up a trace by a remote service. bucket column adds time bucketing to the partition key, values are microseconds rounded to a pre-configured interval (typically one day). ts column is start timestamp of the span as time-uuid, truncated to millisecond precision.';\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.span_by_service (\n    service text,\n    span    text,\n    PRIMARY KEY (service, span)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up span names by a service name. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.remote_service_by_service (\n    service text,\n    remote_service text,\n    PRIMARY KEY (service, remote_service)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up remote service names by a service name. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n\nCREATE TABLE IF NOT EXISTS stress_zipkin2.autocomplete_tags (\n    key     text,\n    value    text,\n    PRIMARY KEY (key, value)\n)\n    WITH compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.2'}\n    AND caching = {'rows_per_partition': 'ALL'}\n    AND default_time_to_live =  259200\n    AND gc_grace_seconds = 3600\n    AND read_repair_chance = 0\n    AND dclocal_read_repair_chance = 0\n    AND speculative_retry = '95percentile'\n    AND comment = 'Secondary table for looking up span tag values for auto-complete purposes. To compensate for hot partitions, we deduplicate write client side, use LeveledCompactionStrategy with a low threshold and add row caching.';\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/README.md",
    "content": "# storage-elasticsearch\n\nThis is a plugin to the Elasticsearch storage component, which uses\nHTTP by way of [Armeria](https://github.com/line/armeria) and\n[Jackson](https://github.com/FasterXML/jackson). This uses Elasticsearch 5+\nfeatures, but is tested against Elasticsearch 7-8.x and OpenSearch 2.x.\n\n## Multiple hosts\nMost users will supply a DNS name that's mapped to multiple A or AAAA\nrecords. For example, `http://elasticsearch:9200` will use normal host\nlookups to get the list of IP addresses, though you can alternatively supply\na list of http base urls. In either case, all the resolved IP addresses\nfrom all provided hosts will be iterated over round-robin, with requests made\nonly to healthy addresses.\n\nHere are some examples:\n\n* `http://1.1.1.1:9200,http://2.2.2.2:19200`\n* `http://1.1.1.1:9200,http://[2001:db8::c001]:9200`\n* `http://elasticsearch:9200,http://1.2.3.4:9200`\n* `http://elasticsearch-1:9200,http://elasticsearch-2:9200`\n\n## Format\nSpans are stored in version 2 format, which is the same as the [v2 POST endpoint](https://zipkin.io/zipkin-api/#/default/post_spans)\nwith one difference described below. We add a \"timestamp_millis\" field\nto aid in integration with other tools.\n\n### Timestamps\nZipkin's timestamps are in epoch microseconds, which is not a supported date type in Elasticsearch / OpenSearch.\nIn consideration of tools like like Kibana, this component adds \"timestamp_millis\" when writing\nspans. This is mapped to the Elasticsearch / OpenSearch date type, so can be used to any date-based queries.\n\n## Indexes\nSpans are stored into daily indices, for example spans with a timestamp\nfalling on 2016/03/19 will be stored in the index named 'zipkin:span-2016-03-19'\nor 'zipkin-span-2016-03-19' if using Elasticsearch version 7 or higher.\nThere is no support for TTL through this SpanStore. It is recommended\ninstead to use [Elastic Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/current/about.html)\nto remove indices older than the point you are interested in.\n\n### Customizing daily index format\nThe daily index format can be adjusted in two ways. You can change the\nindex prefix from 'zipkin' to something else. You can also change\nthe date separator from '-' to something else.\n`ElasticsearchStorage.Builder.index` and `ElasticsearchStorage.Builder.dateSeparator`\ncontrol the daily index format.\n\nFor example, using Elasticsearch 7+, spans with a timestamp falling on\n2016/03/19 end up in the index 'zipkin-span-2016-03-19'. When the date\nseparator is '.', the index would be 'zipkin-span-2016.03.19'.\n\n### String Mapping\nThe Zipkin api implies aggregation and exact match (keyword) on string\nfields named `traceId` and `name` and `serviceName`. Indexing on these\nfields is limited to 256 characters eventhough storage is currently\nunbounded.\n\n### Query indexing\nTo support the zipkin query api, a special index field named `_q` is\nadded to documents, containing annotation values and tag entry pairs.\nEx: the tag `\"error\": \"500\"` results in `\"_q\":[\"error\", \"error=500\"]`.\nThe values in `q` are limited to 256 characters and searched as keywords.\n\nYou can check these manually like so:\n```bash\n$ curl -s 'localhost:9200/zipkin*span-2017-08-11/_search?q=_q:error=500'\n```\n\nThe reason for special casing is around dotted name constraints. Tags\nare stored as a dictionary. Some keys include inconsistent number of dots\n(ex \"error\" and \"error.message\"). Elasticsearch / OpenSearch cannot index\nthese as it inteprets them as fields, and dots in fields imply an object \npath.\n\n### Trace Identifiers\nUnless `ElasticsearchStorage.Builder.strictTraceId` is set to false,\ntrace identifiers are unanalyzed keywords (exact string match). This\nmeans that trace IDs should be written fixed length as either 16 or 32\nlowercase hex characters, corresponding to 64 or 128 bit length. If\nwriting a custom collector in a different language, make sure you trace\nidentifiers the same way.\n\n#### Migrating from 64 to 128-bit trace IDs\nWhen [migrating from 64 to 128-bit trace IDs](../../zipkin-server/README.md#migrating-from-64-to-128-bit-trace-ids),\n`ElasticsearchStorage.Builder.strictTraceId` will be false, and traceId\nfields will be tokenized to support mixed lookup. This setting should\nonly be used temporarily, but is explained below.\n\nThe index template tokenizes trace identifiers to match on either 64-bit\nor 128-bit length. This allows span lookup by 64-bit trace ID to include\nspans reported with 128-bit variants of the same id. This allows interop\nwith tools who only support 64-bit ids, and buys time for applications\nto upgrade to 128-bit instrumentation.\n\nFor example, application A starts a trace with a 128-bit `traceId`\n\"48485a3953bb61246b221d5bc9e6496c\". The next hop, application B, doesn't\nyet support 128-bit ids, B truncates `traceId` to \"6b221d5bc9e6496c\".\nWhen `SpanStore.getTrace(toLong(\"6b221d5bc9e6496c\"))` executes, it\nis able to retrieve spans with the longer `traceId`, due to tokenization\nsetup in the index template.\n\nTo see this in action, you can run a test command like so against one of\nyour indexes:\n\n```bash\n# the output below shows which tokens will match on the trace id supplied.\n$ curl -s 'localhost:9200/zipkin*span-2017-08-22/_analyze' -d '{\n      \"text\": \"48485a3953bb61246b221d5bc9e6496c\",\n      \"analyzer\": \"traceId_analyzer\"\n  }'|jq '.tokens|.[]|.token'\n  \"48485a3953bb61246b221d5bc9e6496c\"\n  \"6b221d5bc9e6496c\"\n```\n\n### Disabling indexing\nIndexing is a good default, but some sites who don't use Zipkin UI's\n\"Find a Trace\" screen may want to disable indexing. This means templates\nwill opt-out of analyzing any data in `span`, except `traceId`. This\nalso means the special fields `_q` and `timestamp_millis` will neither\nbe written, nor analyzed.\n\n[Disabling search](../../README.md#disabling-search) disables indexing.\n\n### Composable Index Template\nElasticsearch 7.8 introduces [composable templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html) and\ndeprecates [legacy/v1 templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html) used in version prior (fully supported by OpenSearch).\nMerging of multiple templates with matching index patterns is no longer allowed, and Elasticsearch / OpenSearch will return error on PUT of the second template\nwith matching index pattern and priority. Templates with matching index patterns are required to have different priorities, and Elasticsearch / OpenSearch will\nonly use the template with the highest priority. This also means that [secondary template](https://gist.github.com/codefromthecrypt/1af1259102e7a2da1b3c9103565165d7)\nis no longer achievable.\n\nBy default, Zipkin will use legacy template during initialization, but you can opt to use composable template by\nproviding `ES_TEMPLATE_PRIORITY` environment variable.\n\n## Customizing the ingest pipeline\n\n### Elasticsearch\n\nYou can setup an [ingest pipeline](https://www.elastic.co/guide/en/elasticsearch/reference/master/pipeline.html) to perform custom processing.\n\n### OpenSearch\n\nYou can setup an [ingest pipeline](https://opensearch.org/docs/latest/ingest-pipelines/) to perform custom processing.\n\n### Setting up ingest pipeline\n\nHere's an example, which you'd setup prior to configuring Zipkin to use\nit via `ElasticsearchStorage.Builder.pipeline`\n\n\n```\nPUT _ingest/pipeline/zipkin\n{\n  \"description\" : \"add collector_timestamp_millis\",\n  \"processors\" : [\n    {\n      \"set\" : {\n        \"field\": \"collector_timestamp_millis\",\n        \"value\": \"{{_ingest.timestamp}}\"\n      }\n    }\n  ]\n}\n```\n\n## Tuning\n\n### Autocomplete indexing\nRedundant requests to store autocomplete values are ignored for an hour\nto reduce load. This is implemented by\n[DelayLimiter](../../zipkin/src/main/java/zipkin2/internal/DelayLimiter.java)\n\n## Data retention\nZipkin-server does not handle retention management of the trace data. Use the tools recommended by to manage data retention, or your cluster will grow indefinitely!\n\n### Elasticsearch\n * [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/current/index.html)\n * [Index Lifecycle Management](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/index-lifecycle-management.html)\n\n### OpenSearch\n * [Curator](https://github.com/flant/curator-opensearch)\n * [Index Lifecycle Management](https://opensearch.org/docs/latest/im-plugin/ism/index/)"
  },
  {
    "path": "zipkin-storage/elasticsearch/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-storage-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-storage-elasticsearch</artifactId>\n  <name>Storage: Elasticsearch / OpenSearch (V2)</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n\n    <!-- error-prone doesn't like generated code and it wants you to depend on guava! -->\n    <errorprone.args>-XepDisableWarningsInGeneratedCode -Xep:AutoValueImmutableFields:OFF -Xep:ExtendsAutoValue:OFF -Xep:AutoValueSubclassLeaked:OFF</errorprone.args>\n  </properties>\n\n  <!-- Avoid CVEs in armeria deps -->\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>io.netty</groupId>\n        <artifactId>netty-bom</artifactId>\n        <version>${netty.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n\n  <dependencies>\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria</artifactId>\n      <version>${armeria.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>com.google.auto.value</groupId>\n      <artifactId>auto-value-annotations</artifactId>\n      <version>${auto-value.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.google.auto.value</groupId>\n      <artifactId>auto-value</artifactId>\n      <version>${auto-value.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.awaitility</groupId>\n      <artifactId>awaitility</artifactId>\n      <version>${awaitility.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.linecorp.armeria</groupId>\n      <artifactId>armeria-junit5</artifactId>\n      <version>${armeria.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/BaseVersion.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage zipkin2.elasticsearch;\n\nimport static zipkin2.elasticsearch.internal.JsonReaders.enterPath;\n\nimport java.io.IOException;\nimport java.util.function.Supplier;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpMethod;\n\nimport zipkin2.elasticsearch.internal.client.HttpCall;\n\n/**\n * Base version for both Elasticsearch and OpenSearch distributions.\n */\npublic abstract class BaseVersion {\n  final int major, minor;\n\n  BaseVersion(int major, int minor) {\n    this.major = major;\n    this.minor = minor;\n  }\n\n  /**\n   * Gets the version for particular distribution, returns either {@link ElasticsearchVersion}\n   * or {@link OpensearchVersion} instance.\n   * @param http the HTTP client\n   * @return either {@link ElasticsearchVersion} or {@link OpensearchVersion} instance\n   * @throws IOException in case of I/O errors\n   */\n  static BaseVersion get(HttpCall.Factory http) throws IOException {\n    return Parser.INSTANCE.get(http);\n  }\n\n  /**\n   * Does this version of Elasticsearch / OpenSearch still support mapping types?\n   * @return \"true\" if mapping types are supported, \"false\" otherwise\n   */\n  public abstract boolean supportsTypes();\n  \n  enum Parser implements HttpCall.BodyConverter<BaseVersion> {\n    INSTANCE;\n\n    final Pattern REGEX = Pattern.compile(\"(\\\\d+)\\\\.(\\\\d+).*\");\n\n    BaseVersion get(HttpCall.Factory callFactory) throws IOException {\n      AggregatedHttpRequest getNode = AggregatedHttpRequest.of(HttpMethod.GET, \"/\");\n      BaseVersion version = callFactory.newCall(getNode, this, \"get-node\").execute();\n      if (version == null) {\n        throw new IllegalArgumentException(\"No content reading Elasticsearch/OpenSearch version\");\n      }\n      return version;\n    }\n\n    @Override\n    public BaseVersion convert(JsonParser parser, Supplier<String> contentString) {\n      String version = null;\n      String distribution = null;\n      try {\n        if (enterPath(parser, \"version\") != null) {\n          while (parser.nextToken() != null) {\n            if (parser.currentToken() == JsonToken.VALUE_STRING) {\n              if (parser.currentName() == \"distribution\") {\n                distribution = parser.getText();\n              } else if (parser.currentName() == \"number\") {\n                version = parser.getText();\n              }\n            }\n          }\n        }\n      } catch (RuntimeException | IOException possiblyParseException) {\n        // EmptyCatch ignored\n      }\n\n      if (version == null) {\n        throw new IllegalArgumentException(\n          \".version.number not found in response: \" + contentString.get());\n      }\n\n      Matcher matcher = REGEX.matcher(version);\n      if (!matcher.matches()) {\n        throw new IllegalArgumentException(\"Invalid .version.number: \" + version);\n      }\n\n      try {\n        int major = Integer.parseInt(matcher.group(1));\n        int minor = Integer.parseInt(matcher.group(2));\n        if (\"opensearch\".equalsIgnoreCase(distribution)) {\n          return new OpensearchVersion(major, minor);\n        } else {\n          return new ElasticsearchVersion(major, minor);\n        }\n      } catch (NumberFormatException e) {\n        throw new IllegalArgumentException(\"Invalid .version.number: \" + version\n          + \", for .version.distribution:\" + distribution);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/BodyConverters.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.elasticsearch.internal.JsonSerializers;\nimport zipkin2.elasticsearch.internal.client.HttpCall.BodyConverter;\nimport zipkin2.elasticsearch.internal.client.SearchResultConverter;\nimport zipkin2.internal.DependencyLinker;\n\nimport static zipkin2.elasticsearch.internal.JsonReaders.collectValuesNamed;\n\nfinal class BodyConverters {\n  static final BodyConverter<Object> NULL = (parser, contentString) -> null;\n  static final BodyConverter<List<String>> KEYS =\n    (parser, contentString) -> collectValuesNamed(parser, \"key\");\n  static final BodyConverter<List<Span>> SPANS =\n    SearchResultConverter.create(JsonSerializers.SPAN_PARSER);\n  static final BodyConverter<List<DependencyLink>> DEPENDENCY_LINKS =\n    new SearchResultConverter<DependencyLink>(JsonSerializers.DEPENDENCY_LINK_PARSER) {\n      @Override\n      public List<DependencyLink> convert(JsonParser parser, Supplier<String> contentString)\n        throws IOException {\n        List<DependencyLink> result = super.convert(parser, contentString);\n        return result.isEmpty() ? result : DependencyLinker.merge(result);\n      }\n    };\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchAutocompleteTags.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.elasticsearch.internal.IndexNameFormatter;\nimport zipkin2.elasticsearch.internal.client.Aggregation;\nimport zipkin2.elasticsearch.internal.client.SearchCallFactory;\nimport zipkin2.elasticsearch.internal.client.SearchRequest;\nimport zipkin2.storage.AutocompleteTags;\n\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_AUTOCOMPLETE;\n\nfinal class ElasticsearchAutocompleteTags implements AutocompleteTags {\n\n  final boolean enabled;\n  final IndexNameFormatter indexNameFormatter;\n  final SearchCallFactory search;\n  final int namesLookback;\n  final Call<List<String>> keysCall;\n\n  ElasticsearchAutocompleteTags(ElasticsearchStorage es) {\n    this.search = new SearchCallFactory(es.http());\n    this.indexNameFormatter = es.indexNameFormatter();\n    this.enabled = es.searchEnabled() && !es.autocompleteKeys().isEmpty();\n    this.namesLookback = es.namesLookback();\n    this.keysCall = Call.create(es.autocompleteKeys());\n  }\n\n  @Override public Call<List<String>> getKeys() {\n    if (!enabled) return Call.emptyList();\n    return keysCall.clone();\n  }\n\n  @Override public Call<List<String>> getValues(String key) {\n    if (key == null) throw new NullPointerException(\"key == null\");\n    if (key.isEmpty()) throw new IllegalArgumentException(\"key was empty\");\n    if (!enabled) return Call.emptyList();\n\n    long endMillis = System.currentTimeMillis();\n    long beginMillis = endMillis - namesLookback;\n    List<String> indices =\n      indexNameFormatter.formatTypeAndRange(TYPE_AUTOCOMPLETE, beginMillis, endMillis);\n\n    if (indices.isEmpty()) return Call.emptyList();\n\n    SearchRequest.Filters filters =\n      new SearchRequest.Filters().addTerm(\"tagKey\", key);\n\n    SearchRequest request = SearchRequest.create(indices)\n      .filters(filters)\n      .addAggregation(Aggregation.terms(\"tagValue\", Integer.MAX_VALUE));\n    return search.newCall(request, BodyConverters.KEYS);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport zipkin2.Call;\nimport zipkin2.Span;\nimport zipkin2.elasticsearch.internal.BulkCallBuilder;\nimport zipkin2.elasticsearch.internal.BulkIndexWriter;\nimport zipkin2.elasticsearch.internal.IndexNameFormatter;\nimport zipkin2.internal.DelayLimiter;\nimport zipkin2.storage.SpanConsumer;\n\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_AUTOCOMPLETE;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;\nimport static zipkin2.internal.RecyclableBuffers.SHORT_STRING_LENGTH;\n\nclass ElasticsearchSpanConsumer implements SpanConsumer { // not final for testing\n  final ElasticsearchStorage es;\n  final Set<String> autocompleteKeys;\n  final IndexNameFormatter indexNameFormatter;\n  final char indexTypeDelimiter;\n  final boolean searchEnabled;\n  final DelayLimiter<AutocompleteContext> delayLimiter;\n\n  ElasticsearchSpanConsumer(ElasticsearchStorage es) {\n    this.es = es;\n    this.autocompleteKeys = new LinkedHashSet<>(es.autocompleteKeys());\n    this.indexNameFormatter = es.indexNameFormatter();\n    this.indexTypeDelimiter = es.indexTypeDelimiter();\n    this.searchEnabled = es.searchEnabled();\n    this.delayLimiter = DelayLimiter.newBuilder()\n      .ttl(es.autocompleteTtl(), TimeUnit.MILLISECONDS)\n      .cardinality(es.autocompleteCardinality()).build();\n  }\n\n  String formatTypeAndTimestampForInsert(String type, long timestampMillis) {\n    return indexNameFormatter\n      .formatTypeAndTimestampForInsert(type, indexTypeDelimiter, timestampMillis);\n  }\n\n  @Override public Call<Void> accept(List<Span> spans) {\n    if (spans.isEmpty()) return Call.create(null);\n    BulkSpanIndexer indexer = new BulkSpanIndexer(this);\n    indexSpans(indexer, spans);\n    return indexer.newCall();\n  }\n\n  void indexSpans(BulkSpanIndexer indexer, List<Span> spans) {\n    for (Span span : spans) {\n      final long indexTimestamp; // which index to store this span into\n      if (span.timestampAsLong() != 0L) {\n        indexTimestamp = span.timestampAsLong() / 1000L;\n      } else if (!span.annotations().isEmpty()) {\n        // guessTimestamp is made for determining the span's authoritative timestamp. When choosing\n        // the index bucket, any annotation is better than using current time.\n        indexTimestamp = span.annotations().get(0).timestamp() / 1000L;\n      } else {\n        indexTimestamp = System.currentTimeMillis();\n      }\n      indexer.add(indexTimestamp, span);\n      if (searchEnabled && !span.tags().isEmpty()) {\n        indexer.addAutocompleteValues(indexTimestamp, span);\n      }\n    }\n  }\n\n  /** Mutable type used for each call to store spans */\n  static final class BulkSpanIndexer {\n    final BulkCallBuilder bulkCallBuilder;\n    final ElasticsearchSpanConsumer consumer;\n    final List<AutocompleteContext> pendingAutocompleteContexts = new ArrayList<>();\n    final BulkIndexWriter<Span> spanWriter;\n\n    BulkSpanIndexer(ElasticsearchSpanConsumer consumer) {\n      this.bulkCallBuilder = new BulkCallBuilder(consumer.es, consumer.es.version(), \"index-span\");\n      this.consumer = consumer;\n      this.spanWriter =\n        consumer.searchEnabled ? BulkIndexWriter.SPAN : BulkIndexWriter.SPAN_SEARCH_DISABLED;\n    }\n\n    void add(long indexTimestamp, Span span) {\n      String index = consumer.formatTypeAndTimestampForInsert(TYPE_SPAN, indexTimestamp);\n      bulkCallBuilder.index(index, TYPE_SPAN, span, spanWriter);\n    }\n\n    void addAutocompleteValues(long indexTimestamp, Span span) {\n      String idx = consumer.formatTypeAndTimestampForInsert(TYPE_AUTOCOMPLETE, indexTimestamp);\n      for (Map.Entry<String, String> tag : span.tags().entrySet()) {\n        int length = tag.getKey().length() + tag.getValue().length() + 1;\n        if (length > SHORT_STRING_LENGTH) continue;\n\n        // If the autocomplete whitelist doesn't contain the key, skip storing its value\n        if (!consumer.autocompleteKeys.contains(tag.getKey())) continue;\n\n        AutocompleteContext context =\n          new AutocompleteContext(indexTimestamp, tag.getKey(), tag.getValue());\n        if (!consumer.delayLimiter.shouldInvoke(context)) continue;\n        pendingAutocompleteContexts.add(context);\n\n        bulkCallBuilder.index(idx, TYPE_AUTOCOMPLETE, tag, BulkIndexWriter.AUTOCOMPLETE);\n      }\n    }\n\n    Call<Void> newCall() {\n      Call<Void> storeCall = bulkCallBuilder.build();\n      if (pendingAutocompleteContexts.isEmpty()) return storeCall;\n      return storeCall.handleError((error, callback) -> {\n        for (AutocompleteContext context : pendingAutocompleteContexts) {\n          consumer.delayLimiter.invalidate(context);\n        }\n        callback.onError(error);\n      });\n    }\n  }\n\n  static final class AutocompleteContext {\n    final long timestamp;\n    final String key, value;\n\n    AutocompleteContext(long timestamp, String key, String value) {\n      this.timestamp = timestamp;\n      this.key = key;\n      this.value = value;\n    }\n\n    @Override public boolean equals(Object o) {\n      if (o == this) return true;\n      if (!(o instanceof AutocompleteContext)) return false;\n      AutocompleteContext that = (AutocompleteContext) o;\n      return timestamp == that.timestamp && key.equals(that.key) && value.equals(that.value);\n    }\n\n    @Override public int hashCode() {\n      int h$ = 1;\n      h$ *= 1000003;\n      h$ ^= (int) (h$ ^ ((timestamp >>> 32) ^ timestamp));\n      h$ *= 1000003;\n      h$ ^= key.hashCode();\n      h$ *= 1000003;\n      h$ ^= value.hashCode();\n      return h$;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchSpanStore.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.elasticsearch.internal.IndexNameFormatter;\nimport zipkin2.elasticsearch.internal.client.Aggregation;\nimport zipkin2.elasticsearch.internal.client.HttpCall;\nimport zipkin2.elasticsearch.internal.client.SearchCallFactory;\nimport zipkin2.elasticsearch.internal.client.SearchRequest;\nimport zipkin2.storage.GroupByTraceId;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StrictTraceId;\nimport zipkin2.storage.Traces;\n\nimport static java.util.Arrays.asList;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_DEPENDENCY;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;\n\nfinal class ElasticsearchSpanStore implements SpanStore, Traces, ServiceAndSpanNames {\n\n  /** To not produce unnecessarily long queries, we don't look back further than first ES support */\n  static final long EARLIEST_MS = 1456790400000L; // March 2016\n\n  final SearchCallFactory search;\n  final Call.Mapper<List<Span>, List<List<Span>>> groupByTraceId;\n  final String[] allSpanIndices;\n  final IndexNameFormatter indexNameFormatter;\n  final boolean strictTraceId, searchEnabled;\n  final int namesLookback;\n\n  ElasticsearchSpanStore(ElasticsearchStorage es) {\n    this.search = new SearchCallFactory(es.http());\n    this.groupByTraceId = GroupByTraceId.create(es.strictTraceId());\n    this.allSpanIndices = new String[] {es.indexNameFormatter().formatType(TYPE_SPAN)};\n    this.indexNameFormatter = es.indexNameFormatter();\n    this.strictTraceId = es.strictTraceId();\n    this.searchEnabled = es.searchEnabled();\n    this.namesLookback = es.namesLookback();\n  }\n\n  @Override\n  public Call<List<List<Span>>> getTraces(QueryRequest request) {\n    if (!searchEnabled) return Call.emptyList();\n\n    long endMillis = request.endTs();\n    long beginMillis = Math.max(endMillis - request.lookback(), EARLIEST_MS);\n\n    SearchRequest.Filters filters = new SearchRequest.Filters();\n    filters.addRange(\"timestamp_millis\", beginMillis, endMillis);\n    if (request.serviceName() != null) {\n      filters.addTerm(\"localEndpoint.serviceName\", request.serviceName());\n    }\n\n    if (request.remoteServiceName() != null) {\n      filters.addTerm(\"remoteEndpoint.serviceName\", request.remoteServiceName());\n    }\n\n    if (request.spanName() != null) {\n      filters.addTerm(\"name\", request.spanName());\n    }\n\n    for (Map.Entry<String, String> kv : request.annotationQuery().entrySet()) {\n      if (kv.getValue().isEmpty()) {\n        filters.addTerm(\"_q\", kv.getKey());\n      } else {\n        filters.addTerm(\"_q\", kv.getKey() + \"=\" + kv.getValue());\n      }\n    }\n\n    if (request.minDuration() != null) {\n      filters.addRange(\"duration\", request.minDuration(), request.maxDuration());\n    }\n\n    // We need to filter to traces that contain at least one span that matches the request,\n    // but the zipkin API is supposed to order traces by first span, regardless of if it was\n    // filtered or not. This is not possible without either multiple, heavyweight queries\n    // or complex multiple indexing, defeating much of the elegance of using elasticsearch for this.\n    // So we fudge and order on the first span among the filtered spans - in practice, there should\n    // be no significant difference in user experience since span start times are usually very\n    // close to each other in human time.\n    Aggregation traceIdTimestamp =\n      Aggregation.terms(\"traceId\", request.limit())\n        .addSubAggregation(Aggregation.min(\"timestamp_millis\"))\n        .orderBy(\"timestamp_millis\", \"desc\");\n\n    List<String> indices = indexNameFormatter.formatTypeAndRange(TYPE_SPAN, beginMillis, endMillis);\n    if (indices.isEmpty()) return Call.emptyList();\n\n    SearchRequest esRequest =\n      SearchRequest.create(indices).filters(filters).addAggregation(traceIdTimestamp);\n\n    HttpCall<List<String>> traceIdsCall = search.newCall(esRequest, BodyConverters.KEYS);\n\n    Call<List<List<Span>>> result =\n      traceIdsCall.flatMap(new GetSpansByTraceId(search, indices)).map(groupByTraceId);\n    // Elasticsearch lookup by trace ID is by the full 128-bit length, but there's still a chance of\n    // clash on lower-64 bit. When strict trace ID is enabled, we only filter client-side on clash.\n    return strictTraceId ? result.map(StrictTraceId.filterTraces(request)) : result;\n  }\n\n  @Override\n  public Call<List<Span>> getTrace(String traceId) {\n    // make sure we have a 16 or 32 character trace ID\n    traceId = Span.normalizeTraceId(traceId);\n\n    // Unless we are strict, truncate the trace ID to 64bit (encoded as 16 characters)\n    if (!strictTraceId && traceId.length() == 32) traceId = traceId.substring(16);\n\n    SearchRequest request = SearchRequest.create(asList(allSpanIndices)).term(\"traceId\", traceId);\n    return search.newCall(request, BodyConverters.SPANS);\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n    Set<String> normalizedTraceIds = new LinkedHashSet<>();\n    for (String traceId : traceIds) {\n      // make sure we have a 16 or 32 character trace ID\n      traceId = Span.normalizeTraceId(traceId);\n\n      // Unless we are strict, truncate the trace ID to 64bit (encoded as 16 characters)\n      if (!strictTraceId && traceId.length() == 32) traceId = traceId.substring(16);\n\n      normalizedTraceIds.add(traceId);\n    }\n\n    if (normalizedTraceIds.isEmpty()) return Call.emptyList();\n    SearchRequest request =\n      SearchRequest.create(asList(allSpanIndices)).terms(\"traceId\", normalizedTraceIds);\n    return search.newCall(request, BodyConverters.SPANS).map(groupByTraceId);\n  }\n\n  @Override public Call<List<String>> getServiceNames() {\n    if (!searchEnabled) return Call.emptyList();\n\n    long endMillis = System.currentTimeMillis();\n    long beginMillis = endMillis - namesLookback;\n\n    List<String> indices = indexNameFormatter.formatTypeAndRange(TYPE_SPAN, beginMillis, endMillis);\n    if (indices.isEmpty()) return Call.emptyList();\n\n    SearchRequest request = SearchRequest.create(indices)\n      .filters(new SearchRequest.Filters().addRange(\"timestamp_millis\", beginMillis, endMillis))\n      .addAggregation(Aggregation.terms(\"localEndpoint.serviceName\", Integer.MAX_VALUE));\n    return search.newCall(request, BodyConverters.KEYS);\n  }\n\n  @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {\n    return aggregatedFieldByServiceName(serviceName, \"remoteEndpoint.serviceName\");\n  }\n\n  @Override public Call<List<String>> getSpanNames(String serviceName) {\n    return aggregatedFieldByServiceName(serviceName, \"name\");\n  }\n\n  Call<List<String>> aggregatedFieldByServiceName(String serviceName, String term) {\n    if (serviceName.isEmpty() || !searchEnabled) return Call.emptyList();\n\n    long endMillis = System.currentTimeMillis();\n    long beginMillis = endMillis - namesLookback;\n\n    List<String> indices = indexNameFormatter.formatTypeAndRange(TYPE_SPAN, beginMillis, endMillis);\n    if (indices.isEmpty()) return Call.emptyList();\n\n    // A span name is only valid on a local endpoint, as a span name is defined locally\n    SearchRequest.Filters filters = new SearchRequest.Filters()\n      .addRange(\"timestamp_millis\", beginMillis, endMillis)\n      .addTerm(\"localEndpoint.serviceName\", serviceName.toLowerCase(Locale.ROOT));\n\n    SearchRequest request = SearchRequest.create(indices).filters(filters)\n      .addAggregation(Aggregation.terms(term, Integer.MAX_VALUE));\n\n    return search.newCall(request, BodyConverters.KEYS);\n  }\n\n  @Override\n  public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n    if (endTs <= 0) throw new IllegalArgumentException(\"endTs <= 0\");\n    if (lookback <= 0) throw new IllegalArgumentException(\"lookback <= 0\");\n\n    long beginMillis = Math.max(endTs - lookback, EARLIEST_MS);\n\n    // We just return all dependencies in the days that fall within endTs and lookback as\n    // dependency links themselves don't have timestamps.\n    List<String> indices =\n      indexNameFormatter.formatTypeAndRange(TYPE_DEPENDENCY, beginMillis, endTs);\n    if (indices.isEmpty()) return Call.emptyList();\n\n    return search.newCall(SearchRequest.create(indices), BodyConverters.DEPENDENCY_LINKS);\n  }\n\n  static final class GetSpansByTraceId implements Call.FlatMapper<List<String>, List<Span>> {\n    final SearchCallFactory search;\n    final List<String> indices;\n\n    GetSpansByTraceId(SearchCallFactory search, List<String> indices) {\n      this.search = search;\n      this.indices = indices;\n    }\n\n    @Override\n    public Call<List<Span>> map(List<String> input) {\n      if (input.isEmpty()) return Call.emptyList();\n\n      SearchRequest getTraces = SearchRequest.create(indices).terms(\"traceId\", input);\n      return search.newCall(getTraces, BodyConverters.SPANS);\n    }\n\n    @Override\n    public String toString() {\n      return \"GetSpansByTraceId{indices=\" + indices + \"}\";\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchSpecificTemplates.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage zipkin2.elasticsearch;\n\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V5_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V6_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V6_7;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V7_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V7_8;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V9_0;\n\nimport zipkin2.internal.Nullable;\n\nfinal class ElasticsearchSpecificTemplates extends VersionSpecificTemplates<ElasticsearchVersion> {\n  static class DistributionTemplate extends DistributionSpecificTemplates {\n    private final ElasticsearchVersion version;\n\n    DistributionTemplate(ElasticsearchVersion version) {\n        this.version = version;\n    }\n    \n    @Override String indexTemplatesUrl(String indexPrefix, String type, @Nullable Integer templatePriority) {\n      if (version.compareTo(V7_8) >= 0 && templatePriority != null) {\n        return \"/_index_template/\" + indexPrefix + type + \"_template\";\n      }\n      if (version.compareTo(V6_7) >= 0 && version.compareTo(V7_0) < 0) {\n        // because deprecation warning on 6 to prepare for 7:\n        //\n        // [types removal] The parameter include_type_name should be explicitly specified in get\n        // template requests to prepare for 7.0. In 7.0 include_type_name will default to 'false',\n        // which means responses will omit the type name in mapping definitions.\n        //\n        // The parameter include_type_name was added in 6.7. Using this with ES older than\n        // 6.7 will result in unrecognized parameter: [include_type_name].\n        return \"/_template/\" + indexPrefix + type + \"_template?include_type_name=true\";\n      }\n\n      return \"/_template/\" + indexPrefix + type + \"_template\";\n    }\n\n    @Override char indexTypeDelimiter() {\n      return ElasticsearchSpecificTemplates.indexTypeDelimiter(version);\n    }\n\n    @Override\n    IndexTemplates get(String indexPrefix, int indexReplicas, int indexShards,\n      boolean searchEnabled, boolean strictTraceId, Integer templatePriority) {\n      return new ElasticsearchSpecificTemplates(indexPrefix, indexReplicas, indexShards, \n        searchEnabled, strictTraceId, templatePriority).get(version);\n    }\n  }\n\n  ElasticsearchSpecificTemplates(String indexPrefix, int indexReplicas, int indexShards,\n    boolean searchEnabled, boolean strictTraceId, Integer templatePriority) {\n    super(indexPrefix, indexReplicas,indexShards, searchEnabled, strictTraceId, templatePriority); \n  }\n\n  @Override String indexPattern(String type, ElasticsearchVersion version) {\n    return '\"'\n      + (version.compareTo(V6_0) < 0 ? \"template\" : \"index_patterns\")\n      + \"\\\": \\\"\"\n      + indexPrefix\n      + indexTypeDelimiter(version)\n      + type\n      + \"-*\"\n      + \"\\\"\";\n  }\n  \n  @Override boolean useComposableTemplate(ElasticsearchVersion version) {\n    return (version.compareTo(V7_8) >= 0 && templatePriority != null);\n  }\n\n  /**\n   * This returns a delimiter based on what's supported by the Elasticsearch version.\n   *\n   * <p>Starting in Elasticsearch 7.x, colons are no longer allowed in index names. This logic will\n   * make sure the pattern in our index template doesn't use them either.\n   *\n   * <p>See https://github.com/openzipkin/zipkin/issues/2219\n   */\n  static char indexTypeDelimiter(ElasticsearchVersion version) {\n    return version.compareTo(V7_0) < 0 ? ':' : '-';\n  }\n\n  @Override String maybeWrap(String type, ElasticsearchVersion version, String json) {\n    // ES 7.x defaults include_type_name to false https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#_literal_include_type_name_literal_now_defaults_to_literal_false_literal\n    if (version.compareTo(V7_0) >= 0) return json;\n    return \"    \\\"\" + type + \"\\\": {\\n  \" + json.replace(\"\\n\", \"\\n  \") + \"  }\\n\";\n  }\n\n  @Override IndexTemplates get(ElasticsearchVersion version) {\n    if (version.compareTo(V5_0) < 0 || version.compareTo(V9_0) >= 0) {\n      throw new IllegalArgumentException(\n        \"Elasticsearch versions 5-8.x are supported, was: \" + version);\n    }\n    return IndexTemplates.newBuilder()\n      .version(version)\n      .indexTypeDelimiter(indexTypeDelimiter(version))\n      .span(spanIndexTemplate(version))\n      .dependency(dependencyTemplate(version))\n      .autocomplete(autocompleteTemplate(version))\n      .build();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.google.auto.value.AutoValue;\nimport com.google.auto.value.extension.memoized.Memoized;\nimport com.linecorp.armeria.client.ResponseTimeoutException;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroup;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpMethod;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Supplier;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.elasticsearch.internal.IndexNameFormatter;\nimport zipkin2.elasticsearch.internal.Internal;\nimport zipkin2.elasticsearch.internal.client.HttpCall;\nimport zipkin2.elasticsearch.internal.client.HttpCall.BodyConverter;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.AutocompleteTags;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.Traces;\n\nimport static com.linecorp.armeria.common.HttpMethod.GET;\nimport static zipkin2.elasticsearch.EnsureIndexTemplate.ensureIndexTemplate;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_AUTOCOMPLETE;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_DEPENDENCY;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;\nimport static zipkin2.elasticsearch.internal.JsonReaders.enterPath;\n\n@AutoValue\npublic abstract class ElasticsearchStorage extends zipkin2.storage.StorageComponent {\n  /**\n   * This defers creation of an {@link WebClient}. This is needed because routinely, I/O occurs in\n   * constructors and this can delay or cause startup to crash. For example, an underlying {@link\n   * EndpointGroup} could be delayed due to DNS, implicit api calls or health checks.\n   */\n  public interface LazyHttpClient extends Supplier<WebClient>, Closeable {\n    /**\n     * Lazily creates an instance of the http client configured to the correct elasticsearch host or\n     * cluster. The same value should always be returned.\n     */\n    @Override WebClient get();\n\n    @Override default void close() {\n    }\n\n    /** This should return the initial endpoints in a single-string without resolving them. */\n    @Override String toString();\n  }\n\n  /** The lazy http client supplier will be closed on {@link #close()} */\n  public static Builder newBuilder(LazyHttpClient lazyHttpClient) {\n    return new $AutoValue_ElasticsearchStorage.Builder()\n      .lazyHttpClient(lazyHttpClient)\n      .strictTraceId(true)\n      .searchEnabled(true)\n      .index(\"zipkin\")\n      .dateSeparator('-')\n      .indexShards(5)\n      .indexReplicas(1)\n      .ensureTemplates(true)\n      .namesLookback(86400000)\n      .flushOnWrites(false)\n      .autocompleteKeys(List.of())\n      .autocompleteTtl((int) TimeUnit.HOURS.toMillis(1))\n      .autocompleteCardinality(5 * 4000); // Ex. 5 site tags with cardinality 4000 each\n  }\n\n  abstract Builder toBuilder();\n\n  @AutoValue.Builder\n  public abstract static class Builder extends StorageComponent.Builder {\n\n    /**\n     * Only valid when the destination is Elasticsearch 5.x. Indicates the ingest pipeline used\n     * before spans are indexed. No default.\n     *\n     * <p>See https://www.elastic.co/guide/en/elasticsearch/reference/master/pipeline.html\n     */\n    public abstract Builder pipeline(String pipeline);\n\n    /**\n     * Only return span and service names where all {@link zipkin2.Span#timestamp()} are at or after\n     * (now - lookback) in milliseconds. Defaults to 1 day (86400000).\n     */\n    public abstract Builder namesLookback(int namesLookback);\n\n    /**\n     * Internal and visible only for testing.\n     *\n     * <p>See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html\n     */\n    public abstract Builder flushOnWrites(boolean flushOnWrites);\n\n    /** The index prefix to use when generating daily index names. Defaults to zipkin. */\n    public final Builder index(String index) {\n      indexNameFormatterBuilder().index(index);\n      return this;\n    }\n\n    /**\n     * The date separator to use when generating daily index names. Defaults to '-'.\n     *\n     * <p>By default, spans with a timestamp falling on 2016/03/19 end up in the index\n     * 'zipkin-span-2016-03-19'. When the date separator is '.', the index would be\n     * 'zipkin-span-2016.03.19'. If the date separator is 0, there is no delimiter. Ex the index\n     * would be 'zipkin-span-20160319'\n     */\n    public final Builder dateSeparator(char dateSeparator) {\n      indexNameFormatterBuilder().dateSeparator(dateSeparator);\n      return this;\n    }\n\n    /**\n     * The number of shards to split the index into. Each shard and its replicas are assigned to a\n     * machine in the cluster. Increasing the number of shards and machines in the cluster will\n     * improve read and write performance. Number of shards cannot be changed for existing indices,\n     * but new daily indices will pick up changes to the setting. Defaults to 5.\n     *\n     * <p>Corresponds to <a\n     * href=\"https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html\">index.number_of_shards</a>\n     */\n    public abstract Builder indexShards(int indexShards);\n\n    /**\n     * The number of replica copies of each shard in the index. Each shard and its replicas are\n     * assigned to a machine in the cluster. Increasing the number of replicas and machines in the\n     * cluster will improve read performance, but not write performance. Number of replicas can be\n     * changed for existing indices. Defaults to 1. It is highly discouraged to set this to 0 as it\n     * would mean a machine failure results in data loss.\n     *\n     * <p>Corresponds to <a\n     * href=\"https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html\">index.number_of_replicas</a>\n     */\n    public abstract Builder indexReplicas(int indexReplicas);\n\n    /** False disables automatic index template installation. */\n    public abstract Builder ensureTemplates(boolean ensureTemplates);\n\n    /**\n     * Only valid when the destination is Elasticsearch >= 7.8. Indicates the index template\n     * priority in case of multiple matching templates. The template with the highest priority is\n     * used. Defaults to 0.\n     *\n     * <p>See https://www.elastic.co/guide/en/elasticsearch/reference/7.8/_index_template_and_settings_priority.html\n     */\n    public abstract Builder templatePriority(@Nullable Integer templatePriority);\n\n    /** {@inheritDoc} */\n    @Override public abstract Builder strictTraceId(boolean strictTraceId);\n\n    /** {@inheritDoc} */\n    @Override public abstract Builder searchEnabled(boolean searchEnabled);\n\n    /** {@inheritDoc} */\n    @Override public abstract Builder autocompleteKeys(List<String> autocompleteKeys);\n\n    /** {@inheritDoc} */\n    @Override public abstract Builder autocompleteTtl(int autocompleteTtl);\n\n    /** {@inheritDoc} */\n    @Override public abstract Builder autocompleteCardinality(int autocompleteCardinality);\n\n    @Override public abstract ElasticsearchStorage build();\n\n    abstract Builder lazyHttpClient(LazyHttpClient lazyHttpClient);\n\n    abstract IndexNameFormatter.Builder indexNameFormatterBuilder();\n\n    Builder() {\n    }\n  }\n\n  abstract LazyHttpClient lazyHttpClient();\n\n  @Nullable public abstract String pipeline();\n\n  public abstract boolean flushOnWrites();\n\n  public abstract boolean strictTraceId();\n\n  abstract boolean searchEnabled();\n\n  abstract List<String> autocompleteKeys();\n\n  abstract int autocompleteTtl();\n\n  abstract int autocompleteCardinality();\n\n  abstract int indexShards();\n\n  abstract int indexReplicas();\n\n  public abstract IndexNameFormatter indexNameFormatter();\n\n  abstract boolean ensureTemplates();\n\n  public abstract int namesLookback();\n\n  @Nullable abstract Integer templatePriority();\n\n  @Override public SpanStore spanStore() {\n    ensureIndexTemplates();\n    return new ElasticsearchSpanStore(this);\n  }\n\n  @Override public Traces traces() {\n    return (Traces) spanStore();\n  }\n\n  @Override public ServiceAndSpanNames serviceAndSpanNames() {\n    return (ServiceAndSpanNames) spanStore();\n  }\n\n  @Override public AutocompleteTags autocompleteTags() {\n    ensureIndexTemplates();\n    return new ElasticsearchAutocompleteTags(this);\n  }\n\n  @Override public SpanConsumer spanConsumer() {\n    ensureIndexTemplates();\n    return new ElasticsearchSpanConsumer(this);\n  }\n\n  /** Returns the Elasticsearch / OpenSearch version of the connected cluster. Internal use only */\n  @Memoized public BaseVersion version() {\n    try {\n      return BaseVersion.get(http());\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  char indexTypeDelimiter() {\n    return VersionSpecificTemplates.forVersion(version()).indexTypeDelimiter();\n  }\n\n  /** This is an internal blocking call, only used in tests. */\n  public void clear() throws IOException {\n    Set<String> toClear = new LinkedHashSet<>();\n    toClear.add(indexNameFormatter().formatType(TYPE_SPAN));\n    toClear.add(indexNameFormatter().formatType(TYPE_DEPENDENCY));\n    // Note: Elasticsearch 8.x requires this config to clear with wildcards:\n    // action.destructive_requires_name: false\n    for (String index : toClear) clear(index);\n  }\n\n  void clear(String index) throws IOException {\n    String url = '/' + index;\n    AggregatedHttpRequest delete = AggregatedHttpRequest.of(HttpMethod.DELETE, url);\n    http().newCall(delete, BodyConverters.NULL, \"delete-index\").execute();\n  }\n\n  /**\n   * Internal code and api responses coerce to {@link RejectedExecutionException} when work is\n   * rejected. We also classify {@link ResponseTimeoutException} as a capacity related exception\n   * even though capacity is not the only reason (timeout could also result from a misconfiguration\n   * or a network problem).\n   */\n  @Override public boolean isOverCapacity(Throwable e) {\n    return e instanceof RejectedExecutionException || e instanceof ResponseTimeoutException;\n  }\n\n  /** This is blocking so that we can determine if the cluster is healthy or not */\n  @Override public CheckResult check() {\n    return ensureIndexTemplatesAndClusterReady(indexNameFormatter().formatType(TYPE_SPAN));\n  }\n\n  /**\n   * This allows the health check to display problems, such as access, installing the index\n   * template. It also helps reduce traffic sent to nodes still initializing (when guarded on the\n   * check result). Finally, this reads the cluster health of the index as it can go down after the\n   * one-time initialization passes.\n   */\n  CheckResult ensureIndexTemplatesAndClusterReady(String index) {\n    try {\n      version(); // ensure the version is available (even if we already cached it)\n      ensureIndexTemplates(); // called only once, so we have to double-check health\n      AggregatedHttpRequest request = AggregatedHttpRequest.of(GET, \"/_cluster/health/\" + index);\n      CheckResult result = http().newCall(request, READ_STATUS, \"get-cluster-health\").execute();\n      if (result == null) throw new IllegalArgumentException(\"No content reading cluster health\");\n      return result;\n    } catch (Throwable e) {\n      Call.propagateIfFatal(e);\n      // Wrapping interferes with humans intended to read this message:\n      //\n      // Unwrap the marker exception as the health check is not relevant for the throttle component.\n      // Unwrap any IOException from the first call to ensureIndexTemplates()\n      if (e instanceof RejectedExecutionException || e instanceof UncheckedIOException) {\n        return CheckResult.failed(e.getCause());\n      }\n      return CheckResult.failed(e);\n    }\n  }\n\n  volatile boolean ensuredTemplates;\n\n  // synchronized since we don't want overlapping calls to apply the index templates\n  void ensureIndexTemplates() {\n    if (ensuredTemplates) return;\n    if (!ensureTemplates()) ensuredTemplates = true;\n    synchronized (this) {\n      if (ensuredTemplates) return;\n      doEnsureIndexTemplates();\n      ensuredTemplates = true;\n    }\n  }\n\n  IndexTemplates doEnsureIndexTemplates() {\n    try {\n      HttpCall.Factory http = http();\n      IndexTemplates templates = versionSpecificTemplates(version());\n      ensureIndexTemplate(http, buildUrl(templates, TYPE_SPAN), templates.span());\n      ensureIndexTemplate(http, buildUrl(templates, TYPE_DEPENDENCY), templates.dependency());\n      ensureIndexTemplate(http, buildUrl(templates, TYPE_AUTOCOMPLETE), templates.autocomplete());\n      return templates;\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  IndexTemplates versionSpecificTemplates(BaseVersion version) {\n    return VersionSpecificTemplates.forVersion(version).get(\n      indexNameFormatter().index(),\n      indexReplicas(),\n      indexShards(),\n      searchEnabled(),\n      strictTraceId(),\n      templatePriority()\n    );\n  }\n\n  String buildUrl(IndexTemplates templates, String type) {\n    String indexPrefix = indexNameFormatter().index() + templates.indexTypeDelimiter();\n    return VersionSpecificTemplates.forVersion(version()).indexTemplatesUrl(indexPrefix, type, templatePriority());\n  }\n\n  @Override public final String toString() {\n    return \"ElasticsearchStorage{initialEndpoints=\" + lazyHttpClient()\n      + \", index=\" + indexNameFormatter().index() + \"}\";\n  }\n\n  static {\n    Internal.instance = new Internal() {\n      @Override public HttpCall.Factory http(ElasticsearchStorage storage) {\n        return storage.http();\n      }\n    };\n  }\n\n  @Memoized HttpCall.Factory http() {\n    return new HttpCall.Factory(lazyHttpClient().get());\n  }\n\n  @Override public void close() {\n    lazyHttpClient().close();\n  }\n\n  ElasticsearchStorage() {\n  }\n\n  static final BodyConverter<CheckResult> READ_STATUS = new BodyConverter<CheckResult>() {\n    @Override public CheckResult convert(JsonParser parser, Supplier<String> contentString)\n      throws IOException {\n      JsonParser status = enterPath(parser, \"status\");\n      if (status == null) {\n        throw new IllegalArgumentException(\"Health status couldn't be read \" + contentString.get());\n      }\n      if (\"RED\".equalsIgnoreCase(status.getText())) {\n        return CheckResult.failed(new IllegalStateException(\"Health status is RED\"));\n      }\n      return CheckResult.OK;\n    }\n\n    @Override public String toString() {\n      return \"ReadStatus\";\n    }\n  };\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchVersion.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.util.Objects;\n\n/** Helps avoid problems comparing versions by number. Ex 7.10 should be > 7.9 */\npublic final class ElasticsearchVersion extends BaseVersion implements Comparable<ElasticsearchVersion> {\n  public static final ElasticsearchVersion V5_0 = new ElasticsearchVersion(5, 0);\n  public static final ElasticsearchVersion V6_0 = new ElasticsearchVersion(6, 0);\n  public static final ElasticsearchVersion V6_7 = new ElasticsearchVersion(6, 7);\n  public static final ElasticsearchVersion V7_0 = new ElasticsearchVersion(7, 0);\n  public static final ElasticsearchVersion V7_8 = new ElasticsearchVersion(7, 8);\n  public static final ElasticsearchVersion V9_0 = new ElasticsearchVersion(9, 0);\n\n  ElasticsearchVersion(int major, int minor) {\n    super(major, minor);\n  }\n\n  @Override public boolean supportsTypes() {\n    return compareTo(V7_0) < 0;\n  }\n\n  @Override public int compareTo(ElasticsearchVersion other) {\n    if (major < other.major) return -1;\n    if (major > other.major) return 1;\n    return Integer.compare(minor, other.minor);\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof ElasticsearchVersion)) return false;\n    ElasticsearchVersion that = (ElasticsearchVersion) o;\n    return this.major == that.major && this.minor == that.minor;\n  }\n\n  @Override public int hashCode() {\n    return Objects.hash(major, minor);\n  }\n\n  @Override public String toString() {\n    return major + \".\" + minor;\n  }\n\n\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/EnsureIndexTemplate.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport zipkin2.elasticsearch.internal.client.HttpCall;\n\n/** Ensures the index template exists and saves off the version */\nfinal class EnsureIndexTemplate {\n\n  /**\n   * This is a blocking call, used inside a lazy. That's because no writes should occur until the\n   * template is available.\n   */\n  static void ensureIndexTemplate(HttpCall.Factory callFactory, String templateUrl,\n      String indexTemplate) throws IOException {\n    AggregatedHttpRequest getTemplate = AggregatedHttpRequest.of(HttpMethod.GET, templateUrl);\n    try {\n      callFactory.newCall(getTemplate, BodyConverters.NULL, \"get-template\").execute();\n    } catch (FileNotFoundException e) { // TODO: handle 404 slightly more nicely\n      AggregatedHttpRequest updateTemplate = AggregatedHttpRequest.of(\n        RequestHeaders.of(\n          HttpMethod.PUT, templateUrl, HttpHeaderNames.CONTENT_TYPE, MediaType.JSON_UTF_8),\n        HttpData.ofUtf8(indexTemplate));\n      callFactory.newCall(updateTemplate, BodyConverters.NULL, \"update-template\").execute();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/IndexTemplates.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.google.auto.value.AutoValue;\n\n@AutoValue\nabstract class IndexTemplates {\n  static Builder newBuilder() {\n    return new AutoValue_IndexTemplates.Builder();\n  }\n\n  abstract BaseVersion version();\n\n  abstract char indexTypeDelimiter();\n\n  abstract String span();\n\n  abstract String dependency();\n\n  abstract String autocomplete();\n\n  @AutoValue.Builder\n  interface Builder {\n    Builder version(BaseVersion version);\n\n    Builder indexTypeDelimiter(char indexTypeDelimiter);\n\n    Builder span(String span);\n\n    Builder dependency(String dependency);\n\n    Builder autocomplete(String autocomplete);\n\n    IndexTemplates build();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/OpensearchSpecificTemplates.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage zipkin2.elasticsearch;\n\nimport static zipkin2.elasticsearch.OpensearchVersion.V1_0;\nimport static zipkin2.elasticsearch.OpensearchVersion.V4_0;\n\nimport zipkin2.internal.Nullable;\n\nfinal class OpensearchSpecificTemplates extends VersionSpecificTemplates<OpensearchVersion> {\n  static class DistributionTemplate extends DistributionSpecificTemplates {\n    private final OpensearchVersion version;\n\n    DistributionTemplate(OpensearchVersion version) {\n      this.version = version;\n    }\n    \n    @Override String indexTemplatesUrl(String indexPrefix, String type, @Nullable Integer templatePriority) {\n      if (version.compareTo(V1_0) >= 0 && templatePriority != null) {\n        return \"/_index_template/\" + indexPrefix + type + \"_template\";\n      }\n\n      return \"/_template/\" + indexPrefix + type + \"_template\";\n    }\n\n    @Override char indexTypeDelimiter() {\n      return OpensearchSpecificTemplates.indexTypeDelimiter(version);\n    }\n\n    @Override\n    IndexTemplates get(String indexPrefix, int indexReplicas, int indexShards,\n      boolean searchEnabled, boolean strictTraceId, Integer templatePriority) {\n      return new OpensearchSpecificTemplates(indexPrefix, indexReplicas, indexShards, \n        searchEnabled, strictTraceId, templatePriority).get(version);\n    }\n  }\n\n  OpensearchSpecificTemplates(String indexPrefix, int indexReplicas, int indexShards,\n    boolean searchEnabled, boolean strictTraceId, Integer templatePriority) {\n    super(indexPrefix, indexReplicas,indexShards, searchEnabled, strictTraceId, templatePriority); \n  }\n\n  @Override String indexPattern(String type, OpensearchVersion version) {\n    return '\"'\n      + \"index_patterns\"\n      + \"\\\": \\\"\"\n      + indexPrefix\n      + indexTypeDelimiter(version)\n      + type\n      + \"-*\"\n      + \"\\\"\";\n  }\n  \n  static char indexTypeDelimiter(OpensearchVersion version) {\n    return '-';\n  }\n  \n  @Override boolean useComposableTemplate(OpensearchVersion version) {\n    return (templatePriority != null);\n  }\n  \n  @Override String maybeWrap(String type, OpensearchVersion version, String json) {\n    return json;\n  }\n\n  @Override IndexTemplates get(OpensearchVersion version) {\n    if (version.compareTo(V1_0) < 0 || version.compareTo(V4_0) >= 0) {\n      throw new IllegalArgumentException(\n        \"OpenSearch versions 1-3.x are supported, was: \" + version);\n    }\n    return IndexTemplates.newBuilder()\n      .version(version)\n      .indexTypeDelimiter(indexTypeDelimiter(version))\n      .span(spanIndexTemplate(version))\n      .dependency(dependencyTemplate(version))\n      .autocomplete(autocompleteTemplate(version))\n      .build();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/OpensearchVersion.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.util.Objects;\n\n/** Helps avoid problems comparing versions by number. Ex 2.10 should be > 2.9 */\npublic final class OpensearchVersion extends BaseVersion implements Comparable<OpensearchVersion> {\n  public static final OpensearchVersion V1_0 = new OpensearchVersion(1, 0);\n  public static final OpensearchVersion V2_0 = new OpensearchVersion(2, 0);\n  public static final OpensearchVersion V3_0 = new OpensearchVersion(2, 0);\n  public static final OpensearchVersion V4_0 = new OpensearchVersion(4, 0);\n\n  OpensearchVersion(int major, int minor) {\n    super(major, minor);\n  }\n\n  @Override public boolean supportsTypes() {\n    return compareTo(V2_0) < 0;\n  }\n\n  @Override public int compareTo(OpensearchVersion other) {\n    if (major < other.major) return -1;\n    if (major > other.major) return 1;\n    return Integer.compare(minor, other.minor);\n  }\n\n  @Override public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof OpensearchVersion)) return false;\n    OpensearchVersion that = (OpensearchVersion) o;\n    return this.major == that.major && this.minor == that.minor;\n  }\n\n  @Override public int hashCode() {\n    return Objects.hash(major, minor);\n  }\n\n  @Override public String toString() {\n    return major + \".\" + minor;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/VersionSpecificTemplates.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport zipkin2.internal.Nullable;\n\n/** Returns version-specific index templates */\n// TODO: make a main class that spits out the index template using ENV variables for the server,\n// a parameter for the version, and a parameter for the index type. Ex.\n// java -cp zipkin-storage-elasticsearch.jar zipkin2.elasticsearch.VersionSpecificTemplates 6.7 span\nabstract class VersionSpecificTemplates<V extends BaseVersion> {\n  /** Maximum character length constraint of most names, IP literals and IDs. */\n  static final int SHORT_STRING_LENGTH = 256;\n  static final String TYPE_AUTOCOMPLETE = \"autocomplete\";\n  static final String TYPE_SPAN = \"span\";\n  static final String TYPE_DEPENDENCY = \"dependency\";\n\n  /**\n   * In Zipkin search, we do exact match only (keyword). Norms is about scoring. We don't use that\n   * in our API, and disable it to reduce disk storage needed.\n   */\n  static final String KEYWORD = \"{ \\\"type\\\": \\\"keyword\\\", \\\"norms\\\": false }\";\n\n  final String indexPrefix;\n  final int indexReplicas, indexShards;\n  final boolean searchEnabled, strictTraceId;\n  final Integer templatePriority;\n\n  VersionSpecificTemplates(String indexPrefix, int indexReplicas, int indexShards,\n    boolean searchEnabled, boolean strictTraceId, Integer templatePriority) {\n    this.indexPrefix = indexPrefix;\n    this.indexReplicas = indexReplicas;\n    this.indexShards = indexShards;\n    this.searchEnabled = searchEnabled;\n    this.strictTraceId = strictTraceId;\n    this.templatePriority = templatePriority;\n  }\n\n  String indexProperties(V version) {\n    // 6.x _all disabled https://www.elastic.co/guide/en/elasticsearch/reference/6.7/breaking-changes-6.0.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default\n    // 7.x _default disallowed https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#_the_literal__default__literal_mapping_is_no_longer_allowed\n    String result = \"    \\\"index.number_of_shards\\\": \" + indexShards + \",\\n\"\n      + \"    \\\"index.number_of_replicas\\\": \" + indexReplicas + \",\\n\"\n      + \"    \\\"index.requests.cache.enable\\\": true\";\n    return result + \"\\n\";\n  }\n\n  String indexTemplate(V version) {\n    if (useComposableTemplate(version)) {\n      return \"\\\"template\\\": {\\n\";\n    }\n\n    return \"\";\n  }\n\n  String indexTemplateClosing(V version) {\n    if (useComposableTemplate(version)) {\n      return \"},\\n\";\n    }\n\n    return \"\";\n  }\n\n  String templatePriority(V version) {\n    if (useComposableTemplate(version)) {\n      return \"\\\"priority\\\": \" + templatePriority + \"\\n\";\n    }\n\n    return \"\";\n  }\n\n  String beginTemplate(String type, V version) {\n    return \"{\\n\"\n      + \"  \" + indexPattern(type, version) + \",\\n\"\n      + indexTemplate(version)\n      + \"  \\\"settings\\\": {\\n\"\n      + indexProperties(version);\n  }\n\n  String endTemplate(V version) {\n    return indexTemplateClosing(version)\n      + templatePriority(version)\n      + \"}\";\n  }\n\n  /** Templatized due to version differences. Only fields used in search are declared */\n  String spanIndexTemplate(V version) {\n    String result = beginTemplate(TYPE_SPAN, version);\n\n    String traceIdMapping = KEYWORD;\n    if (!strictTraceId) {\n      // Supporting mixed trace ID length is expensive due to needing a special analyzer and\n      // \"fielddata\" which consumes a lot of heap. Sites should only turn off strict trace ID when\n      // in a transition, and keep trace ID length transitions as short time as possible.\n      traceIdMapping =\n        \"{ \\\"type\\\": \\\"text\\\", \\\"fielddata\\\": \\\"true\\\", \\\"analyzer\\\": \\\"traceId_analyzer\\\" }\";\n      result += (\"\"\"\n        ,\n            \"analysis\": {\n              \"analyzer\": {\n                \"traceId_analyzer\": {\n                  \"type\": \"custom\",\n                  \"tokenizer\": \"keyword\",\n                  \"filter\": \"traceId_filter\"\n                }\n              },\n              \"filter\": {\n                \"traceId_filter\": {\n                  \"type\": \"pattern_capture\",\n                  \"patterns\": [\"([0-9a-f]{1,16})$\"],\n                  \"preserve_original\": true\n                }\n              }\n            }\n        \"\"\");\n    }\n\n    result += \"  },\\n\";\n\n    if (searchEnabled) {\n      return result\n        + (\"  \\\"mappings\\\": {\\n\"\n        + maybeWrap(TYPE_SPAN, version, \"    \\\"_source\\\": {\\\"excludes\\\": [\\\"_q\\\"] },\\n\"\n        + \"    \\\"dynamic_templates\\\": [\\n\"\n        + \"      {\\n\"\n        + \"        \\\"strings\\\": {\\n\"\n        + \"          \\\"mapping\\\": {\\n\"\n        + \"            \\\"type\\\": \\\"keyword\\\",\\\"norms\\\": false,\"\n        + \" \\\"ignore_above\\\": \" + SHORT_STRING_LENGTH + \"\\n\"\n        + \"          },\\n\"\n        + \"          \\\"match_mapping_type\\\": \\\"string\\\",\\n\"\n        + \"          \\\"match\\\": \\\"*\\\"\\n\"\n        + \"        }\\n\"\n        + \"      }\\n\"\n        + \"    ],\\n\"\n        + \"    \\\"properties\\\": {\\n\"\n        + \"      \\\"traceId\\\": \" + traceIdMapping + \",\\n\"\n        + \"      \\\"name\\\": \" + KEYWORD + \",\\n\"\n        + \"      \\\"localEndpoint\\\": {\\n\"\n        + \"        \\\"type\\\": \\\"object\\\",\\n\"\n        + \"        \\\"dynamic\\\": false,\\n\"\n        + \"        \\\"properties\\\": { \\\"serviceName\\\": \" + KEYWORD + \" }\\n\"\n        + \"      },\\n\"\n        + \"      \\\"remoteEndpoint\\\": {\\n\"\n        + \"        \\\"type\\\": \\\"object\\\",\\n\"\n        + \"        \\\"dynamic\\\": false,\\n\"\n        + \"        \\\"properties\\\": { \\\"serviceName\\\": \" + KEYWORD + \" }\\n\"\n        + \"      },\\n\"\n        + \"      \\\"timestamp_millis\\\": {\\n\"\n        + \"        \\\"type\\\":   \\\"date\\\",\\n\"\n        + \"        \\\"format\\\": \\\"epoch_millis\\\"\\n\"\n        + \"      },\\n\"\n        + \"      \\\"duration\\\": { \\\"type\\\": \\\"long\\\" },\\n\"\n        + \"      \\\"annotations\\\": { \\\"enabled\\\": false },\\n\"\n        + \"      \\\"tags\\\": { \\\"enabled\\\": false },\\n\"\n        + \"      \\\"_q\\\": \" + KEYWORD + \"\\n\"\n        + \"    }\\n\")\n        + \"  }\\n\"\n        + endTemplate(version));\n    }\n    return result\n      + (\"  \\\"mappings\\\": {\\n\"\n      + maybeWrap(TYPE_SPAN, version, \"    \\\"properties\\\": {\\n\"\n      + \"      \\\"traceId\\\": \" + traceIdMapping + \",\\n\"\n      + \"      \\\"annotations\\\": { \\\"enabled\\\": false },\\n\"\n      + \"      \\\"tags\\\": { \\\"enabled\\\": false }\\n\"\n      + \"    }\\n\")\n      + \"  }\\n\"\n      + endTemplate(version));\n  }\n\n  /** Templatized due to version differences. Only fields used in search are declared */\n  String dependencyTemplate(V version) {\n    return beginTemplate(TYPE_DEPENDENCY, version)\n      + \"  },\\n\"\n      + \"  \\\"mappings\\\": {\\n\"\n      + maybeWrap(TYPE_DEPENDENCY, version, \"    \\\"enabled\\\": false\\n\")\n      + \"  }\\n\"\n      + endTemplate(version);\n  }\n\n  // The key filed of a autocompleteKeys is intentionally names as tagKey since it clashes with the\n  // BodyConverters KEY\n  String autocompleteTemplate(V version) {\n    return beginTemplate(TYPE_AUTOCOMPLETE, version)\n      + \"  },\\n\"\n      + \"  \\\"mappings\\\": {\\n\"\n      + maybeWrap(TYPE_AUTOCOMPLETE, version, \"    \\\"enabled\\\": true,\\n\"\n      + \"    \\\"properties\\\": {\\n\"\n      + \"      \\\"tagKey\\\": \" + KEYWORD + \",\\n\"\n      + \"      \\\"tagValue\\\": \" + KEYWORD + \"\\n\"\n      + \"    }\\n\")\n      + \"  }\\n\"\n      + endTemplate(version);\n  }\n\n  /**\n   * Returns index pattern\n   * @param type type \n   * @param version distribution version\n   * @return index pattern\n   */\n  abstract String indexPattern(String type, V version);\n\n  /**\n   * Returns index templates\n   * @param version distribution version\n   * @return index templates\n   */\n  abstract IndexTemplates get(V version);\n\n  /**\n   * Should composable templates be used or not\n   * @param version distribution version\n   * @return {@code true} if composable templates should be used,\n   * {@code false} otherwise\n   */\n  abstract boolean useComposableTemplate(V version);\n\n  /**\n   * Wraps the JSON payload if needed\n   * @param type type\n   * @param version distribution version\n   * @param json JSON payload\n   * @return wrapped JSON payload if needed\n   */\n  abstract String maybeWrap(String type, V version, String json);\n \n  /**\n   * Returns distribution specific templates (index templates URL, index \n   * type delimiter, {@link IndexTemplates});\n   */\n  abstract static class DistributionSpecificTemplates {\n    /**\n     * Returns distribution specific index templates URL\n     * @param indexPrefix index prefix\n     * @param type type\n     * @param templatePriority index template priority\n     * @return index templates URL \n     */\n    abstract String indexTemplatesUrl(String indexPrefix, String type, @Nullable Integer templatePriority); \n\n    /**\n     * Returns distribution specific index type delimiter\n     * @return index type delimiter\n     */\n    abstract char indexTypeDelimiter();\n\n    /**\n     * Returns distribution specific index templates\n     * @param indexPrefix index prefix\n     * @param indexReplicas number of replicas\n     * @param indexShards number of shards\n     * @param searchEnabled search is enabled or disabled\n     * @param strictTraceId strict trace ID\n     * @param templatePriority index template priority\n     * @return index templates\n     */\n    abstract IndexTemplates get(String indexPrefix, int indexReplicas, int indexShards,\n      boolean searchEnabled, boolean strictTraceId, Integer templatePriority);\n  }\n\n  /**\n   * Creates a new {@link DistributionSpecificTemplates} instance based on the distribution\n   * @param version distribution version\n   * @return {@link OpensearchSpecificTemplates} or {@link ElasticsearchSpecificTemplates} instance\n   */\n  static DistributionSpecificTemplates forVersion(BaseVersion version) {\n    if (version instanceof ElasticsearchVersion) {\n      return new ElasticsearchSpecificTemplates.DistributionTemplate((ElasticsearchVersion) version);\n    } else if (version instanceof OpensearchVersion) {\n      return new OpensearchSpecificTemplates.DistributionTemplate((OpensearchVersion) version);\n    } else {\n      throw new IllegalArgumentException(\"The distribution version is not supported: \" + version);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkCallBuilder.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.google.auto.value.AutoValue;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpRequestWriter;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.RequestContext;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.util.Exceptions;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.buffer.ByteBufOutputStream;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport io.netty.handler.codec.http.QueryStringEncoder;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.function.Supplier;\n\nimport zipkin2.elasticsearch.BaseVersion;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.internal.client.HttpCall;\nimport zipkin2.elasticsearch.internal.client.HttpCall.BodyConverter;\n\nimport static zipkin2.Call.propagateIfFatal;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.OBJECT_MAPPER;\nimport static zipkin2.elasticsearch.internal.client.HttpCall.maybeRootCauseReason;\n\n// See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html\n// exposed to re-use for testing writes of dependency links\npublic final class BulkCallBuilder {\n  // This mapper is invoked under the assumption that bulk requests return errors even when the http\n  // status is success. The status codes expected to be returned were undocumented as of version 7.2\n  // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html\n  static final BodyConverter<Void> CHECK_FOR_ERRORS = new BodyConverter<Void>() {\n    @Override public Void convert(JsonParser parser, Supplier<String> contentString) {\n      RuntimeException toThrow = null;\n      try {\n        JsonNode root = OBJECT_MAPPER.readTree(parser);\n        // only throw when we know it is an error\n        if (!root.at(\"/errors\").booleanValue() && !root.at(\"/error\").isObject()) return null;\n\n        String message = maybeRootCauseReason(root);\n        if (message == null) message = contentString.get();\n        Number status = root.findPath(\"status\").numberValue();\n        if (status != null && status.intValue() == 429) {\n          toThrow = new RejectedExecutionException(message);\n        } else {\n          toThrow = new RuntimeException(message);\n        }\n      } catch (RuntimeException | IOException possiblyParseException) { // All use of jackson throws\n      }\n      if (toThrow != null) throw toThrow;\n      return null;\n    }\n\n    @Override public String toString() {\n      return \"CheckForErrors\";\n    }\n  };\n\n  final String tag;\n  final boolean shouldAddType;\n  final HttpCall.Factory http;\n  final String pipeline;\n  final boolean waitForRefresh;\n\n  // Mutated for each call to index\n  final List<IndexEntry<?>> entries = new ArrayList<>();\n\n  public BulkCallBuilder(ElasticsearchStorage es, BaseVersion version, String tag) {\n    this.tag = tag;\n    shouldAddType = version.supportsTypes();\n    http = Internal.instance.http(es);\n    pipeline = es.pipeline();\n    waitForRefresh = es.flushOnWrites();\n  }\n\n  static <T> IndexEntry<T> newIndexEntry(String index, String typeName, T input,\n    BulkIndexWriter<T> writer) {\n    return new AutoValue_BulkCallBuilder_IndexEntry<>(index, typeName, input, writer);\n  }\n\n  @AutoValue static abstract class IndexEntry<T> {\n    abstract String index();\n\n    abstract String typeName();\n\n    abstract T input();\n\n    abstract BulkIndexWriter<T> writer();\n  }\n\n  public <T> void index(String index, String typeName, T input, BulkIndexWriter<T> writer) {\n    entries.add(newIndexEntry(index, typeName, input, writer));\n  }\n\n  /**\n   * Creates a bulk request when there is more than one object to store\n   */\n  public HttpCall<Void> build() {\n    QueryStringEncoder urlBuilder = new QueryStringEncoder(\"/_bulk\");\n    if (pipeline != null) urlBuilder.addParam(\"pipeline\", pipeline);\n    if (waitForRefresh) urlBuilder.addParam(\"refresh\", \"wait_for\");\n\n    ByteBufAllocator alloc = RequestContext.mapCurrent(\n      RequestContext::alloc, () -> PooledByteBufAllocator.DEFAULT);\n\n    HttpCall.RequestSupplier request = new BulkRequestSupplier(\n      entries,\n      shouldAddType,\n      RequestHeaders.of(\n        HttpMethod.POST, urlBuilder.toString(),\n        HttpHeaderNames.CONTENT_TYPE, MediaType.JSON_UTF_8),\n      alloc);\n    return http.newCall(request, CHECK_FOR_ERRORS, tag);\n  }\n\n  static class BulkRequestSupplier implements HttpCall.RequestSupplier {\n    final List<IndexEntry<?>> entries;\n    final boolean shouldAddType;\n    final RequestHeaders headers;\n    final ByteBufAllocator alloc;\n\n    BulkRequestSupplier(List<IndexEntry<?>> entries, boolean shouldAddType,\n      RequestHeaders headers, ByteBufAllocator alloc) {\n      this.entries = List.copyOf(entries);\n      this.shouldAddType = shouldAddType;\n      this.headers = headers;\n      this.alloc = alloc;\n    }\n\n    @Override public RequestHeaders headers() {\n      return headers;\n    }\n\n    @Override public HttpRequest get() {\n      HttpRequestWriter writer = HttpRequest.streaming(headers);\n      writeEntry(writer, 0);\n      return writer;\n    }\n\n    // There's a high chance that the response is received before the request\n    // is complete. This can be a problem for BulkCallBuilder when it's sending\n    // streaming requests. Hence, we use backpressure, instead of buffering.\n    //\n    // Follow https://github.com/line/armeria/issues/3119 for doc updates.\n    private void writeEntry(HttpRequestWriter writer, int index) {\n      if (index == entries.size()) { // out of entries.\n        writer.close();\n        return;\n      }\n      // Write the current entry directly to the current request.\n      if (!writer.tryWrite(HttpData.wrap(serialize(alloc, entries.get(index), shouldAddType)))) {\n        // Stream aborted, no need to serialize anymore.\n        return;\n      }\n      // Recurse to proceed to the next entry, if any.\n      writer.whenConsumed().thenRun(() -> writeEntry(writer, index + 1));\n    }\n  }\n\n  static <T> ByteBuf serialize(ByteBufAllocator alloc, IndexEntry<T> entry,\n    boolean shouldAddType) {\n    // Fuzzily assume a general small span is 600 bytes to reduce resizing while building up the\n    // JSON. Any extra bytes will be released back after serializing the document.\n    ByteBuf document = alloc.heapBuffer(600);\n    ByteBuf metadata = alloc.heapBuffer(200);\n    try {\n      String id = entry.writer().writeDocument(entry.input(), new ByteBufOutputStream(document));\n      writeIndexMetadata(new ByteBufOutputStream(metadata), entry, id, shouldAddType);\n\n      ByteBuf payload = alloc.ioBuffer(document.readableBytes() + metadata.readableBytes() + 2);\n      try {\n        payload.writeBytes(metadata).writeByte('\\n').writeBytes(document).writeByte('\\n');\n      } catch (Throwable t) {\n        payload.release();\n        propagateIfFatal(t);\n        Exceptions.throwUnsafely(t);\n      }\n      return payload;\n    } finally {\n      document.release();\n      metadata.release();\n    }\n  }\n\n  static <T> void writeIndexMetadata(ByteBufOutputStream sink, IndexEntry<T> entry, String id,\n    boolean shouldAddType) {\n    try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {\n      writer.writeStartObject();\n      writer.writeObjectFieldStart(\"index\");\n      writer.writeStringField(\"_index\", entry.index());\n      // the _type parameter is needed for Elasticsearch < 6.x\n      if (shouldAddType) writer.writeStringField(\"_type\", entry.typeName());\n      writer.writeStringField(\"_id\", id);\n      writer.writeEndObject();\n      writer.writeEndObject();\n    } catch (IOException e) {\n      throw new AssertionError(e); // No I/O writing to a Buffer.\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkIndexWriter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufOutputStream;\nimport io.netty.buffer.ByteBufUtil;\nimport java.io.IOException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Iterator;\nimport java.util.Map;\nimport zipkin2.Annotation;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static zipkin2.internal.RecyclableBuffers.SHORT_STRING_LENGTH;\n\npublic abstract class BulkIndexWriter<T> {\n\n  /**\n   * Write a complete json document according to index strategy and returns the ID field.\n   */\n  public abstract String writeDocument(T input, ByteBufOutputStream sink);\n\n  public static final BulkIndexWriter<Span> SPAN = new BulkIndexWriter<Span>() {\n    @Override public String writeDocument(Span input, ByteBufOutputStream sink) {\n      return write(input, true, sink);\n    }\n  };\n  public static final BulkIndexWriter<Span>\n    SPAN_SEARCH_DISABLED = new BulkIndexWriter<Span>() {\n    @Override public String writeDocument(Span input, ByteBufOutputStream sink) {\n      return write(input, false, sink);\n    }\n  };\n\n  public static final BulkIndexWriter<Map.Entry<String, String>> AUTOCOMPLETE =\n    new BulkIndexWriter<Map.Entry<String, String>>() {\n      @Override public String writeDocument(Map.Entry<String, String> input,\n        ByteBufOutputStream sink) {\n        try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {\n          writeAutocompleteEntry(input.getKey(), input.getValue(), writer);\n        } catch (IOException e) {\n          throw new AssertionError(\"Couldn't close generator for a memory stream.\", e);\n        }\n        // Id is used to dedupe server side as necessary. Arbitrarily same format as _q value.\n        return input.getKey() + '=' + input.getValue();\n      }\n    };\n\n  static final Endpoint EMPTY_ENDPOINT = Endpoint.newBuilder().build();\n\n  /**\n   * In order to allow systems like Kibana to search by timestamp, we add a field \"timestamp_millis\"\n   * when storing. The cheapest way to do this without changing the codec is prefixing it to the\n   * json. For example. {\"traceId\":\"... becomes {\"timestamp_millis\":12345,\"traceId\":\"...\n   *\n   * <p>Tags are stored as a dictionary. Since some tag names will include inconsistent number of\n   * dots (ex \"error\" and perhaps \"error.message\"), we cannot index them naturally with\n   * elasticsearch. Instead, we add an index-only (non-source) field of {@code _q} which includes\n   * valid search queries. For example, the tag {@code error -> 500} results in {@code\n   * \"_q\":[\"error\", \"error=500\"]}. This matches the input query syntax, and can be checked manually\n   * with curl.\n   *\n   * <p>Ex {@code curl -s localhost:9200/zipkin:span-2017-08-11/_search?q=_q:error=500}\n   *\n   * @param searchEnabled encodes timestamp_millis and _q when non-empty\n   */\n  static String write(Span span, boolean searchEnabled, ByteBufOutputStream sink) {\n    int startIndex = sink.buffer().writerIndex();\n    try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {\n      writer.writeStartObject();\n      if (searchEnabled) addSearchFields(span, writer);\n      writer.writeStringField(\"traceId\", span.traceId());\n      if (span.parentId() != null) writer.writeStringField(\"parentId\", span.parentId());\n      writer.writeStringField(\"id\", span.id());\n      if (span.kind() != null) writer.writeStringField(\"kind\", span.kind().toString());\n      if (span.name() != null) writer.writeStringField(\"name\", span.name());\n      if (span.timestampAsLong() != 0L) {\n        writer.writeNumberField(\"timestamp\", span.timestampAsLong());\n      }\n      if (span.durationAsLong() != 0L) writer.writeNumberField(\"duration\", span.durationAsLong());\n      if (span.localEndpoint() != null && !EMPTY_ENDPOINT.equals(span.localEndpoint())) {\n        writer.writeFieldName(\"localEndpoint\");\n        write(span.localEndpoint(), writer);\n      }\n      if (span.remoteEndpoint() != null && !EMPTY_ENDPOINT.equals(span.remoteEndpoint())) {\n        writer.writeFieldName(\"remoteEndpoint\");\n        write(span.remoteEndpoint(), writer);\n      }\n      if (!span.annotations().isEmpty()) {\n        writer.writeArrayFieldStart(\"annotations\");\n        for (int i = 0, length = span.annotations().size(); i < length; ) {\n          write(span.annotations().get(i++), writer);\n        }\n        writer.writeEndArray();\n      }\n      if (!span.tags().isEmpty()) {\n        writer.writeObjectFieldStart(\"tags\");\n        Iterator<Map.Entry<String, String>> tags = span.tags().entrySet().iterator();\n        while (tags.hasNext()) write(tags.next(), writer);\n        writer.writeEndObject();\n      }\n      if (Boolean.TRUE.equals(span.debug())) writer.writeBooleanField(\"debug\", true);\n      if (Boolean.TRUE.equals(span.shared())) writer.writeBooleanField(\"shared\", true);\n      writer.writeEndObject();\n    } catch (IOException e) {\n      throw new AssertionError(e); // No I/O writing to a Buffer.\n    }\n\n    // get a slice representing the document we just wrote so that we can make a content hash\n    ByteBuf slice = sink.buffer().slice(startIndex, sink.buffer().writerIndex() - startIndex);\n\n    return span.traceId() + '-' + md5(slice);\n  }\n\n  static void writeAutocompleteEntry(String key, String value, JsonGenerator writer) {\n    try {\n      writer.writeStartObject();\n      writer.writeStringField(\"tagKey\", key);\n      writer.writeStringField(\"tagValue\", value);\n      writer.writeEndObject();\n    } catch (IOException e) {\n      throw new AssertionError(e); // No I/O writing to a Buffer.\n    }\n  }\n\n  static void write(Map.Entry<String, String> tag, JsonGenerator writer) throws IOException {\n    writer.writeStringField(tag.getKey(), tag.getValue());\n  }\n\n  static void write(Annotation annotation, JsonGenerator writer) throws IOException {\n    writer.writeStartObject();\n    writer.writeNumberField(\"timestamp\", annotation.timestamp());\n    writer.writeStringField(\"value\", annotation.value());\n    writer.writeEndObject();\n  }\n\n  static void write(Endpoint endpoint, JsonGenerator writer) throws IOException {\n    writer.writeStartObject();\n    if (endpoint.serviceName() != null) {\n      writer.writeStringField(\"serviceName\", endpoint.serviceName());\n    }\n    if (endpoint.ipv4() != null) writer.writeStringField(\"ipv4\", endpoint.ipv4());\n    if (endpoint.ipv6() != null) writer.writeStringField(\"ipv6\", endpoint.ipv6());\n    if (endpoint.portAsInt() != 0) writer.writeNumberField(\"port\", endpoint.portAsInt());\n    writer.writeEndObject();\n  }\n\n  static void addSearchFields(Span span, JsonGenerator writer) throws IOException {\n    long timestampMillis = span.timestampAsLong() / 1000L;\n    if (timestampMillis != 0L) writer.writeNumberField(\"timestamp_millis\", timestampMillis);\n    if (!span.tags().isEmpty() || !span.annotations().isEmpty()) {\n      writer.writeArrayFieldStart(\"_q\");\n      for (Annotation a : span.annotations()) {\n        if (a.value().length() > SHORT_STRING_LENGTH) continue;\n        writer.writeString(a.value());\n      }\n      for (Map.Entry<String, String> tag : span.tags().entrySet()) {\n        int length = tag.getKey().length() + tag.getValue().length() + 1;\n        if (length > SHORT_STRING_LENGTH) continue;\n        writer.writeString(tag.getKey()); // search is possible by key alone\n        writer.writeString(tag.getKey() + \"=\" + tag.getValue());\n      }\n      writer.writeEndArray();\n    }\n  }\n\n  static String md5(ByteBuf buf) {\n    final MessageDigest messageDigest;\n    try {\n      messageDigest = MessageDigest.getInstance(\"MD5\");\n    } catch (NoSuchAlgorithmException e) {\n      throw new AssertionError();\n    }\n    messageDigest.update(buf.nioBuffer());\n    return ByteBufUtil.hexDump(messageDigest.digest());\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/IndexNameFormatter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.google.auto.value.AutoValue;\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.GregorianCalendar;\nimport java.util.List;\nimport java.util.TimeZone;\nimport zipkin2.internal.DateUtil;\nimport zipkin2.internal.Nullable;\n\nimport static java.time.LocalDateTime.ofInstant;\nimport static java.util.Calendar.DAY_OF_MONTH;\n\n/**\n * <h3>Index-Prefix/type delimiter</h3>\n * When Elasticsearch dropped support for multiple type indexes, we introduced a delimited naming\n * convention to distinguish between span, dependency and autocomplete documents. Originally, this\n * was a colon prefix pattern. In version 7, Elasticsearch dropped support for colons in indexes. To\n * keep existing writes consistent, we still use colon in versions prior to ES 7, eventhough\n * starting at version 7, we change to hyphens. {@code zipkin2.elasticsearch.IndexTemplates} is\n * responsible for this decision.\n *\n * <p><h3>Creating indexes</h3>\n * Using the default index prefix of \"zipkin\", when indexes are created, they look like the\n * following, based on the version.\n *\n * <ul>\n *   <li>ES up to v6: zipkin:span-2019-05-03 zipkin:dependency-2019-05-03 zipkin:autocomplete-2019-05-03</li>\n *   <li>ES v7: zipkin-span-2019-05-03 zipkin-dependency-2019-05-03 zipkin-autocomplete-2019-05-03</li>\n * </ul>\n *\n * <p>We can allow an index prefix of up to 231 UTF-8 encoded bytes, subject to the index naming\n * constraints. This is the normal 255 limit minus the longest suffix (ex. -autocomplete-2019-05-03).\n *\n * <p><h3>Reading indexes</h3>\n * While ES 7 cannot write new indexes with a colons, it can read them. Upon upgrade, some sites\n * will have a mixed read state where some indexes delimit types with a colon and others a hyphen.\n * Accordingly, we use * in read patterns in place of a type delimiter. We use * because there is no\n * support for single character wildcards in ES.\n *\n * <p><h3>Elasticsearch 7 naming constraints</h3>\n * According to a <a href=\"https://github.com/elastic/elasticsearch/blob/83e9d0b9c63589f1dc5bda8abb6b10b27502ef71/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java#L162\">recent\n * reference</a>, the following index naming constraints apply to index names as of ES 7:\n *\n * <ul>\n *   <li>No more than 255 UTF-8 encoded bytes</li>\n *   <li>Cannot be . or ..</li>\n *   <li>Cannot contain : or #</li>\n *   <li>Cannot start with _ - or +</li>\n * </ul>\n */\n@AutoValue\npublic abstract class IndexNameFormatter {\n  public static Builder newBuilder() {\n    return new AutoValue_IndexNameFormatter.Builder();\n  }\n\n  public abstract Builder toBuilder();\n\n  private static final TimeZone UTC = TimeZone.getTimeZone(\"UTC\");\n\n  public abstract String index();\n\n  abstract char dateSeparator();\n\n  abstract DateTimeFormatter dateFormat();\n\n  @AutoValue.Builder\n  public abstract static class Builder {\n    public abstract Builder index(String index);\n\n    public abstract Builder dateSeparator(char dateSeparator);\n\n    abstract Builder dateFormat(DateTimeFormatter dateFormat);\n\n    abstract char dateSeparator();\n\n    public final IndexNameFormatter build() {\n      char separator = dateSeparator();\n      String format = separator == 0 ? \"yyyyMMdd\" : \"yyyy-MM-dd\".replace('-', separator);\n      return dateFormat(DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC)).autoBuild();\n    }\n\n    abstract IndexNameFormatter autoBuild();\n  }\n\n  /**\n   * Returns a set of index patterns that represent the range provided. Notably, this compresses\n   * months or years using wildcards (in order to send smaller API calls).\n   *\n   * <p>For example, if {@code beginMillis} is 2016-11-30 and {@code endMillis} is 2017-01-02, the\n   * result will be 2016-11-30, 2016-12-*, 2017-01-01 and 2017-01-02.\n   */\n  public List<String> formatTypeAndRange(@Nullable String type, long beginMillis, long endMillis) {\n    GregorianCalendar current = midnightUTC(beginMillis);\n    GregorianCalendar end = midnightUTC(endMillis);\n\n    String prefix = prefix(type);\n    List<String> indices = new ArrayList<>();\n    while (current.compareTo(end) <= 0) {\n      if (current.get(Calendar.MONTH) == Calendar.JANUARY && current.get(DAY_OF_MONTH) == 1) {\n        // attempt to compress a year\n        current.set(Calendar.DAY_OF_YEAR, current.getActualMaximum(Calendar.DAY_OF_YEAR));\n        if (current.compareTo(end) <= 0) {\n          indices.add(\n            \"%s-%s%c*\".formatted(prefix, current.get(Calendar.YEAR), dateSeparator()));\n          current.add(DAY_OF_MONTH, 1); // rollover to next year\n          continue;\n        } else {\n          current.set(Calendar.DAY_OF_YEAR, 1); // rollback to first of the year\n        }\n      } else if (current.get(DAY_OF_MONTH) == 1) {\n        // attempt to compress a month\n        current.set(DAY_OF_MONTH, current.getActualMaximum(DAY_OF_MONTH));\n        if (current.compareTo(end) <= 0) {\n          indices.add(formatIndexPattern(\"%s-%s%c%02d%c*\", current, prefix));\n          current.add(DAY_OF_MONTH, 1); // rollover to next month\n          continue;\n        }\n        current.set(DAY_OF_MONTH, 9); // try to compress days 0-9\n        if (current.compareTo(end) <= 0) {\n          indices.add(formatIndexPattern(\"%s-%s%c%02d%c0*\", current, prefix));\n          current.add(DAY_OF_MONTH, 1); // rollover to day 10\n          continue;\n        }\n        current.set(DAY_OF_MONTH, 1); // set back to day 1\n      } else if (current.get(DAY_OF_MONTH) == 10) {\n        current.set(DAY_OF_MONTH, 19); // try to compress days 10-19\n        if (current.compareTo(end) <= 0) {\n          indices.add(formatIndexPattern(\"%s-%s%c%02d%c1*\", current, prefix));\n          current.add(DAY_OF_MONTH, 1); // rollover to day 20\n          continue;\n        }\n        current.set(DAY_OF_MONTH, 10); // set back to day 10\n      } else if (current.get(DAY_OF_MONTH) == 20) {\n        current.set(DAY_OF_MONTH, 29); // try to compress days 20-29\n        if (current.compareTo(end) <= 0) {\n          indices.add(formatIndexPattern(\"%s-%s%c%02d%c2*\", current, prefix));\n          current.add(DAY_OF_MONTH, 1); // rollover to day 30\n          continue;\n        }\n        current.set(DAY_OF_MONTH, 20); // set back to day 20\n      }\n      indices.add(formatTypeAndTimestamp(type, current.getTimeInMillis()));\n      current.add(DAY_OF_MONTH, 1);\n    }\n    return indices;\n  }\n\n  String formatIndexPattern(String format, GregorianCalendar current, String prefix) {\n    return format.formatted(\n      prefix,\n      current.get(Calendar.YEAR),\n      dateSeparator(),\n      current.get(Calendar.MONTH) + 1,\n      dateSeparator());\n  }\n\n  static GregorianCalendar midnightUTC(long epochMillis) {\n    GregorianCalendar result = new GregorianCalendar(UTC);\n    result.setTimeInMillis(DateUtil.midnightUTC(epochMillis));\n    return result;\n  }\n\n  /** On insert, require a version-specific index-type delimiter as ES 7+ dropped colons */\n  public String formatTypeAndTimestampForInsert(String type, char indexTypeDelimiter,\n    long timestampMillis) {\n    return index() + indexTypeDelimiter + type + '-' + format(timestampMillis);\n  }\n\n  public String formatTypeAndTimestamp(@Nullable String type, long timestampMillis) {\n    return prefix(type) + \"-\" + format(timestampMillis);\n  }\n\n  private String prefix(@Nullable String type) {\n    // We use single-character wildcard here in order to read both : and - as starting in ES 7, :\n    // is no longer permitted.\n    return type != null ? index() + \"*\" + type : index();\n  }\n\n  public String formatType(@Nullable String type) {\n    return prefix(type) + \"-*\";\n  }\n\n  String format(long timestampMillis) {\n    return dateFormat().format(ofInstant(Instant.ofEpochMilli(timestampMillis), ZoneOffset.UTC));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/Internal.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.internal.client.HttpCall;\n\n/**\n * Escalate internal APIs so they can be used from outside packages. The only implementation is in\n * {@link ElasticsearchStorage}.\n *\n * <p>Inspired by {@code okhttp3.internal.Internal}.\n */\npublic abstract class Internal {\n  public static Internal instance;\n\n  public abstract HttpCall.Factory http(ElasticsearchStorage storage);\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/JsonReaders.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport zipkin2.internal.Nullable;\n\n/**\n * Utilities used here aim to reduce allocation overhead for common requests. It does so by skipping\n * unrelated fields. This is used for responses which could be large.\n */\npublic final class JsonReaders {\n  /**\n   * Navigates to a field of a JSON-serialized object. For example,\n   *\n   * <pre>{@code\n   * JsonParser status = enterPath(JsonAdapters.jsonParser(stream), \"message\", \"status\");\n   * if (status != null) throw new IllegalStateException(status.nextString());\n   * }</pre>\n   */\n  @Nullable public static JsonParser enterPath(JsonParser parser, String path1, String path2)\n    throws IOException {\n    return enterPath(parser, path1) != null ? enterPath(parser, path2) : null;\n  }\n\n  @Nullable public static JsonParser enterPath(JsonParser parser, String path) throws IOException {\n    if (!checkStartObject(parser, false)) return null;\n\n    JsonToken value;\n    while ((value = parser.nextValue()) != JsonToken.END_OBJECT) {\n      if (value == null) {\n        // End of input so ignore.\n        return null;\n      }\n      if (parser.getCurrentName().equalsIgnoreCase(path) && value != JsonToken.VALUE_NULL) {\n        return parser;\n      } else {\n        parser.skipChildren();\n      }\n    }\n    return null;\n  }\n\n  public static List<String> collectValuesNamed(JsonParser parser, String name) throws IOException {\n    checkStartObject(parser, true);\n    Set<String> result = new LinkedHashSet<>();\n    visitObject(parser, name, result);\n    return new ArrayList<>(result);\n  }\n\n  static void visitObject(JsonParser parser, String name, Set<String> result) throws IOException {\n    checkStartObject(parser, true);\n    JsonToken value;\n    while ((value = parser.nextValue()) != JsonToken.END_OBJECT) {\n      if (value == null) {\n        // End of input so ignore.\n        return;\n      }\n      if (parser.getCurrentName().equals(name)) {\n        result.add(parser.getText());\n      } else {\n        visitNextOrSkip(parser, name, result);\n      }\n    }\n  }\n\n  static void visitNextOrSkip(JsonParser parser, String name, Set<String> result)\n    throws IOException {\n    switch (parser.currentToken()) {\n      case START_ARRAY:\n        JsonToken token;\n        while ((token = parser.nextToken()) != JsonToken.END_ARRAY) {\n          if (token == null) {\n            // End of input so ignore.\n            return;\n          }\n          visitObject(parser, name, result);\n        }\n        break;\n      case START_OBJECT:\n        visitObject(parser, name, result);\n        break;\n      default:\n        // Skip current value.\n    }\n  }\n\n  static boolean checkStartObject(JsonParser parser, boolean shouldThrow) throws IOException {\n    try {\n      JsonToken currentToken = parser.currentToken();\n      // The parser may not be at a token, yet. If that's the case advance.\n      if (currentToken == null) currentToken = parser.nextToken();\n\n      // If we are still not at the expected token, we could be an another or an empty body.\n      if (currentToken == JsonToken.START_OBJECT) return true;\n      if (shouldThrow) {\n        throw new IllegalArgumentException(\"Expected start object, was \" + currentToken);\n      }\n      return false;\n    } catch (Throwable e) { // likely not json\n      if (shouldThrow) throw e;\n      return false;\n    }\n  }\n\n  JsonReaders() {\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/JsonSerializers.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport zipkin2.Annotation;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\n/**\n * JSON serialization utilities and parsing code.\n */\npublic final class JsonSerializers {\n  public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()\n    .setSerializationInclusion(JsonInclude.Include.NON_NULL);\n  public static final JsonFactory JSON_FACTORY = new JsonFactory();\n\n  public static JsonGenerator jsonGenerator(OutputStream stream) {\n    try {\n      return JSON_FACTORY.createGenerator(stream);\n    } catch (IOException e) {\n      throw new AssertionError(\"Could not create JSON generator for a memory stream.\", e);\n    }\n  }\n\n  public interface ObjectParser<T> {\n    T parse(JsonParser jsonParser) throws IOException;\n  }\n\n  public static final ObjectParser<Span> SPAN_PARSER = JsonSerializers::parseSpan;\n\n  static Span parseSpan(JsonParser parser) throws IOException {\n    if (!parser.isExpectedStartObjectToken()) {\n      throw new IllegalArgumentException(\"Not a valid JSON object, start token: \" +\n        parser.currentToken());\n    }\n\n    Span.Builder result = Span.newBuilder();\n\n    JsonToken value;\n    while ((value = parser.nextValue()) != JsonToken.END_OBJECT) {\n      if (value == null) {\n        throw new IOException(\"End of input while parsing object.\");\n      }\n      if (value == JsonToken.VALUE_NULL) {\n        continue;\n      }\n      switch (parser.currentName()) {\n        case \"traceId\":\n          result.traceId(parser.getText());\n          break;\n        case \"parentId\":\n          result.parentId(parser.getText());\n          break;\n        case \"id\":\n          result.id(parser.getText());\n          break;\n        case \"kind\":\n          result.kind(Span.Kind.valueOf(parser.getText()));\n          break;\n        case \"name\":\n          result.name(parser.getText());\n          break;\n        case \"timestamp\":\n          result.timestamp(parser.getLongValue());\n          break;\n        case \"duration\":\n          result.duration(parser.getLongValue());\n          break;\n        case \"localEndpoint\":\n          result.localEndpoint(parseEndpoint(parser));\n          break;\n        case \"remoteEndpoint\":\n          result.remoteEndpoint(parseEndpoint(parser));\n          break;\n        case \"annotations\":\n          if (value != JsonToken.START_ARRAY) {\n            throw new IOException(\"Invalid span, expecting annotations array start, got: \" +\n              value);\n          }\n          while (parser.nextToken() != JsonToken.END_ARRAY) {\n            Annotation a = parseAnnotation(parser);\n            result.addAnnotation(a.timestamp(), a.value());\n          }\n          break;\n        case \"tags\":\n          if (value != JsonToken.START_OBJECT) {\n            throw new IOException(\"Invalid span, expecting tags object, got: \" + value);\n          }\n          while (parser.nextValue() != JsonToken.END_OBJECT) {\n            result.putTag(parser.currentName(), parser.getValueAsString());\n          }\n          break;\n        case \"debug\":\n          result.debug(parser.getBooleanValue());\n          break;\n        case \"shared\":\n          result.shared(parser.getBooleanValue());\n          break;\n        default:\n          // Skip\n      }\n    }\n\n    return result.build();\n  }\n\n  static Endpoint parseEndpoint(JsonParser parser) throws IOException {\n    if (!parser.isExpectedStartObjectToken()) {\n      throw new IllegalArgumentException(\"Not a valid JSON object, start token: \" +\n        parser.currentToken());\n    }\n\n    String serviceName = null, ipv4 = null, ipv6 = null;\n    int port = 0;\n\n    while (parser.nextToken() != JsonToken.END_OBJECT) {\n      JsonToken value = parser.nextValue();\n      if (value == JsonToken.VALUE_NULL) {\n        continue;\n      }\n\n      switch (parser.currentName()) {\n        case \"serviceName\":\n          serviceName = parser.getText();\n          break;\n        case \"ipv4\":\n          ipv4 = parser.getText();\n          break;\n        case \"ipv6\":\n          ipv6 = parser.getText();\n          break;\n        case \"port\":\n          port = parser.getIntValue();\n          break;\n        default:\n          // Skip\n      }\n    }\n\n    if (serviceName == null && ipv4 == null && ipv6 == null && port == 0) return null;\n    return Endpoint.newBuilder()\n      .serviceName(serviceName)\n      .ip(ipv4)\n      .ip(ipv6)\n      .port(port)\n      .build();\n  }\n\n  static Annotation parseAnnotation(JsonParser parser) throws IOException {\n    if (!parser.isExpectedStartObjectToken()) {\n      throw new IllegalArgumentException(\"Not a valid JSON object, start token: \" +\n        parser.currentToken());\n    }\n\n    long timestamp = 0;\n    String value = null;\n\n    while (parser.nextValue() != JsonToken.END_OBJECT) {\n      switch (parser.currentName()) {\n        case \"timestamp\":\n          timestamp = parser.getLongValue();\n          break;\n        case \"value\":\n          value = parser.getValueAsString();\n          break;\n        default:\n          // Skip\n      }\n    }\n\n    if (timestamp == 0 || value == null) {\n      throw new IllegalArgumentException(\"Incomplete annotation at \" + parser.currentToken());\n    }\n    return Annotation.create(timestamp, value);\n  }\n\n  public static final ObjectParser<DependencyLink> DEPENDENCY_LINK_PARSER = parser -> {\n    if (!parser.isExpectedStartObjectToken()) {\n      throw new IllegalArgumentException(\"Expected start of dependency link object but was \"\n        + parser.currentToken());\n    }\n\n    DependencyLink.Builder result = DependencyLink.newBuilder();\n    JsonToken value;\n    while ((value = parser.nextValue()) != JsonToken.END_OBJECT) {\n      if (value == null) {\n        throw new IOException(\"End of input while parsing object.\");\n      }\n      switch (parser.currentName()) {\n        case \"parent\":\n          result.parent(parser.getText());\n          break;\n        case \"child\":\n          result.child(parser.getText());\n          break;\n        case \"callCount\":\n          result.callCount(parser.getLongValue());\n          break;\n        case \"errorCount\":\n          result.errorCount(parser.getLongValue());\n          break;\n        default:\n          // Skip\n      }\n    }\n    return result.build();\n  };\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/Aggregation.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class Aggregation {\n  transient final String field;\n\n  AggTerms terms;\n  Map<String, String> min;\n  Map<String, Aggregation> aggs;\n\n  Aggregation(String field) {\n    this.field = field;\n  }\n\n  public static Aggregation terms(String field, int size) {\n    Aggregation result = new Aggregation(field);\n    result.terms = new AggTerms(field, size);\n    return result;\n  }\n\n  public Aggregation orderBy(String subAgg, String direction) {\n    terms.order(subAgg, direction);\n    return this;\n  }\n\n  public static Aggregation min(String field) {\n    Aggregation result = new Aggregation(field);\n    result.min = Map.of(\"field\", field);\n    return result;\n  }\n\n  public AggTerms getTerms() {\n    return terms;\n  }\n\n  public Map<String, String> getMin() {\n    return min;\n  }\n\n  public Map<String, Aggregation> getAggs() {\n    return aggs;\n  }\n\n  static class AggTerms {\n    AggTerms(String field, int size) {\n      this.field = field;\n      this.size = size;\n    }\n\n    final String field;\n    final int size;\n    Map<String, String> order;\n\n    void order(String agg, String direction) {\n      order = Map.of(agg, direction);\n    }\n\n    public String getField() {\n      return field;\n    }\n\n    public int getSize() {\n      return size;\n    }\n\n    public Map<String, String> getOrder() {\n      return order;\n    }\n  }\n\n  public Aggregation addSubAggregation(Aggregation agg) {\n    if (aggs == null) aggs = new LinkedHashMap<>();\n    aggs.put(agg.field, agg);\n    return this;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.linecorp.armeria.client.Clients;\nimport com.linecorp.armeria.client.UnprocessedRequestException;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaders;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.HttpStatusClass;\nimport com.linecorp.armeria.common.RequestContext;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.util.Exceptions;\nimport com.linecorp.armeria.common.util.SafeCloseable;\nimport io.netty.util.concurrent.EventExecutor;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.function.Supplier;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\nimport static zipkin2.elasticsearch.internal.JsonSerializers.JSON_FACTORY;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.OBJECT_MAPPER;\n\npublic final class HttpCall<V> extends Call.Base<V> {\n\n  public interface BodyConverter<V> {\n    /**\n     * Prefer using the {@code parser} for request-scoped conversions. Typically, {@code\n     * contentString} is only for an unexpected failure.\n     */\n    V convert(JsonParser parser, Supplier<String> contentString) throws IOException;\n  }\n\n  /**\n   * A supplier of {@linkplain HttpHeaders headers} and {@linkplain HttpData body} of a request to\n   * Elasticsearch.\n   */\n  public interface RequestSupplier extends Supplier<HttpRequest> {\n    /**\n     * Returns the {@linkplain HttpHeaders headers} for this request.\n     */\n    RequestHeaders headers();\n  }\n\n  static class AggregatedRequestSupplier implements RequestSupplier {\n\n    final AggregatedHttpRequest request;\n\n    AggregatedRequestSupplier(AggregatedHttpRequest request) {\n      try (HttpData content = request.content()) {\n        if (!content.isPooled()) {\n          this.request = request;\n        } else {\n          // Unfortunately it's not possible to use pooled objects in requests and support clone()\n          // after sending the request.\n          this.request = AggregatedHttpRequest.of(\n            request.headers(), HttpData.wrap(content.array()), request.trailers());\n        }\n      }\n    }\n\n    @Override public RequestHeaders headers() {\n      return request.headers();\n    }\n\n    @Override public HttpRequest get() {\n      return request.toHttpRequest();\n    }\n  }\n\n  public static class Factory {\n    final WebClient httpClient;\n\n    public Factory(WebClient httpClient) {\n      this.httpClient = httpClient;\n    }\n\n    public <V> HttpCall<V> newCall(\n      AggregatedHttpRequest request, BodyConverter<V> bodyConverter, String name) {\n      return new HttpCall<>(\n        httpClient, new AggregatedRequestSupplier(request), bodyConverter, name);\n    }\n\n    public <V> HttpCall<V> newCall(\n      RequestSupplier request, BodyConverter<V> bodyConverter, String name) {\n      return new HttpCall<>(httpClient, request, bodyConverter, name);\n    }\n  }\n\n  // Visible for benchmarks\n  public final RequestSupplier request;\n  final BodyConverter<V> bodyConverter;\n  final String name;\n\n  final WebClient httpClient;\n\n  volatile CompletableFuture<AggregatedHttpResponse> responseFuture;\n\n  HttpCall(WebClient httpClient, RequestSupplier request, BodyConverter<V> bodyConverter,\n    String name) {\n    this.httpClient = httpClient;\n    this.name = name;\n    this.request = request;\n    this.bodyConverter = bodyConverter;\n  }\n\n  @Override protected V doExecute() throws IOException {\n    // TODO: testme\n    for (EventExecutor eventLoop : httpClient.options().factory().eventLoopGroup()) {\n      if (eventLoop.inEventLoop()) {\n        throw new RuntimeException(\"\"\"\n          Attempting to make a blocking request from an event loop. \\\n          Either use doEnqueue() or run this in a separate thread.\\\n          \"\"\");\n      }\n    }\n    final AggregatedHttpResponse response;\n    try {\n      response = sendRequest().join();\n    } catch (CompletionException e) {\n      propagateIfFatal(e);\n      Exceptions.throwUnsafely(e.getCause());\n      return null;  // Unreachable\n    }\n    return parseResponse(response, bodyConverter);\n  }\n\n  @SuppressWarnings(\"FutureReturnValueIgnored\")\n  // TODO: errorprone wants us to check this future before returning, but what would be a sensible\n  // check? Say it is somehow canceled, would we take action? Would callback.onError() be redundant?\n  @Override protected void doEnqueue(Callback<V> callback) {\n    sendRequest().handle((response, t) -> {\n      if (t != null) {\n        callback.onError(t);\n      } else {\n        try {\n          V value = parseResponse(response, bodyConverter);\n          callback.onSuccess(value);\n        } catch (Throwable t1) {\n          propagateIfFatal(t1);\n          callback.onError(t1);\n        }\n      }\n      return null;\n    });\n  }\n\n  @Override protected void doCancel() {\n    CompletableFuture<AggregatedHttpResponse> responseFuture = this.responseFuture;\n    if (responseFuture != null) {\n      responseFuture.cancel(false);\n    }\n  }\n\n  @Override public HttpCall<V> clone() {\n    return new HttpCall<>(httpClient, request, bodyConverter, name);\n  }\n\n  @Override public String toString() {\n    return \"HttpCall(\" + request + \")\";\n  }\n\n  CompletableFuture<AggregatedHttpResponse> sendRequest() {\n    final HttpResponse response;\n    try (SafeCloseable ignored =\n           Clients.withContextCustomizer(ctx -> ctx.logBuilder().name(name))) {\n      response = httpClient.execute(request.get());\n    }\n    CompletableFuture<AggregatedHttpResponse> responseFuture =\n      RequestContext.mapCurrent(\n        ctx -> response.aggregateWithPooledObjects(ctx.eventLoop(), ctx.alloc()),\n        // This should never be used in practice since the module runs in an Armeria server.\n        response::aggregate);\n    responseFuture = responseFuture.exceptionally(t -> {\n      if (t instanceof UnprocessedRequestException) {\n        Throwable cause = t.getCause();\n        // Go ahead and reduce the output in logs since this is usually a configuration or\n        // infrastructure issue and the Armeria stack trace won't help debugging that.\n        Exceptions.clearTrace(cause);\n\n        String message = cause.getMessage();\n        if (message == null) message = cause.getClass().getSimpleName();\n        throw new RejectedExecutionException(message, cause);\n      } else {\n        Exceptions.throwUnsafely(t);\n      }\n      return null;\n    });\n    this.responseFuture = responseFuture;\n    return responseFuture;\n  }\n\n  V parseResponse(AggregatedHttpResponse response, BodyConverter<V> bodyConverter)\n    throws IOException {\n    // Handle the case where there is no content, as that means we have no resources to release.\n    HttpStatus status = response.status();\n    if (response.content().isEmpty()) {\n      if (status.codeClass().equals(HttpStatusClass.SUCCESS)) {\n        return null;\n      } else if (status.code() == 404) {\n        throw new FileNotFoundException(request.headers().path());\n      } else {\n        throw new RuntimeException(\n          \"response for \" + request.headers().path() + \" failed: \" + response.status());\n      }\n    }\n\n    // If this is a client or server error, we look for a json message.\n    if ((status.codeClass().equals(HttpStatusClass.CLIENT_ERROR)\n      || status.codeClass().equals(HttpStatusClass.SERVER_ERROR))) {\n      bodyConverter = (parser, contentString) -> {\n        String message = null;\n        try {\n          JsonNode root = OBJECT_MAPPER.readTree(parser);\n          message = maybeRootCauseReason(root);\n          if (message == null) message = root.at(\"/Message\").textValue();\n        } catch (RuntimeException | IOException possiblyParseException) {\n          // EmptyCatch ignored\n        }\n        throw new RuntimeException(message != null ? message\n          : \"response for \" + request.headers().path() + \" failed: \" + contentString.get());\n      };\n    }\n\n    try (HttpData content = response.content();\n         InputStream stream = content.toInputStream();\n         JsonParser parser = JSON_FACTORY.createParser(stream)) {\n\n      if (status.code() == 404) throw new FileNotFoundException(request.headers().path());\n\n      return bodyConverter.convert(parser, content::toStringUtf8);\n    }\n  }\n\n  public static String maybeRootCauseReason(JsonNode root) {\n    // Prefer the root cause to an arbitrary reason.\n    String message;\n    if (!root.findPath(\"root_cause\").isMissingNode()) {\n      message = root.findPath(\"root_cause\").findPath(\"reason\").textValue();\n    } else {\n      message = root.findPath(\"reason\").textValue();\n    }\n    return message;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/SearchCallFactory.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport java.util.List;\nimport zipkin2.internal.Nullable;\n\nimport static zipkin2.elasticsearch.internal.JsonSerializers.OBJECT_MAPPER;\n\npublic class SearchCallFactory {\n  final HttpCall.Factory http;\n\n  public SearchCallFactory(HttpCall.Factory http) {\n    this.http = http;\n  }\n\n  public <V> HttpCall<V> newCall(SearchRequest request, HttpCall.BodyConverter<V> bodyConverter) {\n    final AggregatedHttpRequest httpRequest;\n    try {\n      httpRequest = AggregatedHttpRequest.of(\n        RequestHeaders.of(\n          HttpMethod.POST, lenientSearch(request.indices, request.type),\n          HttpHeaderNames.CONTENT_TYPE, MediaType.JSON_UTF_8),\n        HttpData.wrap(OBJECT_MAPPER.writeValueAsBytes(request)));\n    } catch (JsonProcessingException e) {\n      throw new AssertionError(\"Could not serialize SearchRequest to bytes.\", e);\n    }\n    return http.newCall(httpRequest, bodyConverter, request.tag());\n  }\n\n  /** Matches the behavior of {@code IndicesOptions#lenientExpandOpen()} */\n  String lenientSearch(List<String> indices, @Nullable String type) {\n    return '/' + String.join(\",\", indices) +\n      \"/_search?allow_no_indices=true&expand_wildcards=open&ignore_unavailable=true\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/SearchRequest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport zipkin2.internal.Nullable;\n\npublic final class SearchRequest {\n\n  public static SearchRequest create(List<String> indices) {\n    return new SearchRequest(indices, null);\n  }\n\n  public static SearchRequest create(List<String> indices, String type) {\n    return new SearchRequest(indices, type);\n  }\n\n  /**\n   * The maximum results returned in a query. This only affects non-aggregation requests.\n   *\n   * <p>Not configurable as it implies adjustments to the index template (index.max_result_window)\n   *\n   * <p> See https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-from-size.html\n   */\n  static final int MAX_RESULT_WINDOW = 10000; // the default elasticsearch allowed limit\n\n  transient final List<String> indices;\n  @Nullable transient final String type;\n\n  Integer size = MAX_RESULT_WINDOW;\n  Boolean _source;\n  Object query;\n  Map<String, Aggregation> aggs;\n\n  SearchRequest(List<String> indices, @Nullable String type) {\n    this.indices = indices;\n    this.type = type;\n  }\n\n  public static class Filters extends ArrayList<Object> {\n    public Filters addRange(String field, long from, Long to) {\n      add(new Range(field, from, to));\n      return this;\n    }\n\n    public Filters addTerm(String field, String value) {\n      add(new Term(field, value));\n      return this;\n    }\n  }\n\n  public SearchRequest filters(Filters filters) {\n    return query(new BoolQuery(\"must\", filters));\n  }\n\n  public SearchRequest term(String field, String value) {\n    return query(new Term(field, value));\n  }\n\n  public SearchRequest terms(String field, Collection<String> values) {\n    return query(new Terms(field, values));\n  }\n\n  public SearchRequest addAggregation(Aggregation agg) {\n    size = null; // we return aggs, not source data\n    _source = false;\n    if (aggs == null) aggs = new LinkedHashMap<>();\n    aggs.put(agg.field, agg);\n    return this;\n  }\n\n  public Integer getSize() {\n    return size;\n  }\n\n  public Boolean get_source() {\n    return _source;\n  }\n\n  public Object getQuery() {\n    return query;\n  }\n\n  public Map<String, Aggregation> getAggs() {\n    return aggs;\n  }\n\n  String tag() {\n    return aggs != null ? \"aggregation\" : \"search\";\n  }\n\n  SearchRequest query(Object filter) {\n    query = Map.of(\"bool\", Map.of(\"filter\", filter));\n    return this;\n  }\n\n  static class Term {\n\n    final Map<String, String> term;\n\n    Term(String field, String value) {\n      term = Map.of(field, value);\n    }\n    public Map<String, String> getTerm() {\n      return term;\n    }\n  }\n\n  static class Terms {\n    final Map<String, Collection<String>> terms;\n\n    Terms(String field, Collection<String> values) {\n      this.terms = Map.of(field, values);\n    }\n\n    public Map<String, Collection<String>> getTerms() {\n      return terms;\n    }\n  }\n\n  static class Range {\n    final Map<String, Bounds> range;\n\n    Range(String field, long from, Long to) {\n      range = Map.of(field, new Bounds(from, to));\n    }\n\n    public Map<String, Bounds> getRange() {\n      return range;\n    }\n\n    static class Bounds {\n      final long from;\n      final Long to;\n      final boolean include_lower = true;\n      final boolean include_upper = true;\n\n      Bounds(long from, Long to) {\n        this.from = from;\n        this.to = to;\n      }\n\n      public long getFrom() {\n        return from;\n      }\n\n      public Long getTo() {\n        return to;\n      }\n\n      @JsonProperty(\"include_lower\")\n      public boolean isIncludeLower() {\n        return include_lower;\n      }\n\n      @JsonProperty(\"include_upper\")\n      public boolean isIncludeUpper() {\n        return include_upper;\n      }\n    }\n  }\n\n  static class BoolQuery {\n    final Map<String, Object> bool;\n\n    BoolQuery(String op, Object clause) {\n      bool = Map.of(op, clause);\n    }\n\n    public Map<String, Object> getBool() {\n      return bool;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/SearchResultConverter.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport zipkin2.elasticsearch.internal.JsonSerializers.ObjectParser;\n\nimport static zipkin2.elasticsearch.internal.JsonReaders.enterPath;\n\npublic class SearchResultConverter<T> implements HttpCall.BodyConverter<List<T>> {\n  final ObjectParser<T> adapter;\n\n  public static <T> SearchResultConverter<T> create(ObjectParser<T> adapter) {\n    return new SearchResultConverter<>(adapter);\n  }\n\n  protected SearchResultConverter(ObjectParser<T> adapter) {\n    this.adapter = adapter;\n  }\n\n  @Override\n  public List<T> convert(JsonParser parser, Supplier<String> contentString) throws IOException {\n    JsonParser hits = enterPath(parser, \"hits\", \"hits\");\n    if (hits == null || !hits.isExpectedStartArrayToken()) return List.of();\n\n    List<T> result = new ArrayList<>();\n    while (hits.nextToken() != JsonToken.END_ARRAY) {\n      JsonParser source = enterPath(hits, \"_source\");\n      if (source != null) result.add(adapter.parse(source));\n    }\n    return result.isEmpty() ? List.of() : result;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/BaseVersionTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass BaseVersionTest {\n  static final AggregatedHttpResponse OPENSEARCH_RESPONSE = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"PV-NhJd\",\n        \"cluster_name\" : \"CollectorDBCluster\",\n        \"cluster_uuid\" : \"UjZaM0fQRC6tkHINCg9y8w\",\n         \"version\" : {\n          \"distribution\" : \"opensearch\",\n          \"number\" : \"2.11.1\",\n          \"build_type\" : \"tar\",\n          \"build_hash\" : \"6b1986e964d440be9137eba1413015c31c5a7752\",\n          \"build_date\" : \"2023-11-29T21:43:10.135035992Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"9.7.0\",\n          \"minimum_wire_compatibility_version\" : \"7.10.0\",\n          \"minimum_index_compatibility_version\" : \"7.0.0\"\n        },\n        \"tagline\" : \"The OpenSearch Project: https://opensearch.org/\"\n      }\n      \"\"\");\n\n    static final AggregatedHttpResponse ELASTICSEARCH_RESPONSE = AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n        {\n          \"name\" : \"zipkin-elasticsearch\",\n          \"cluster_name\" : \"docker-cluster\",\n          \"cluster_uuid\" : \"wByRPgSgTryYl0TZXW4MsA\",\n          \"version\" : {\n            \"number\" : \"7.0.1\",\n            \"build_flavor\" : \"default\",\n            \"build_type\" : \"tar\",\n            \"build_hash\" : \"e4efcb5\",\n            \"build_date\" : \"2019-04-29T12:56:03.145736Z\",\n            \"build_snapshot\" : false,\n            \"lucene_version\" : \"8.0.0\",\n            \"minimum_wire_compatibility_version\" : \"6.7.0\",\n            \"minimum_index_compatibility_version\" : \"6.0.0-beta1\"\n          },\n          \"tagline\" : \"You Know, for Search\"\n        }\n        \"\"\");\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  @BeforeEach void setUp() {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri())).build();\n  }\n\n  @AfterEach void tearDown() {\n    storage.close();\n  }\n\n  ElasticsearchStorage storage;\n\n\n  @Test void opensearch() throws Exception {\n    server.enqueue(OPENSEARCH_RESPONSE);\n\n    assertThat(storage.version())\n      .isInstanceOf(OpensearchVersion.class)\n      .isEqualTo(new OpensearchVersion(2, 11));\n  }\n  \n  @Test void elasticsearch() throws Exception {\n    server.enqueue(ELASTICSEARCH_RESPONSE);\n\n    assertThat(storage.version())\n      .isInstanceOf(ElasticsearchVersion.class)\n      .isEqualTo(new ElasticsearchVersion(7, 0));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchAutocompleteTagsTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass ElasticsearchAutocompleteTagsTest {\n\n  static final AggregatedHttpResponse SUCCESS_RESPONSE =\n    AggregatedHttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,\n      HttpData.ofUtf8(TestResponses.AUTOCOMPLETE_VALUES));\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  ElasticsearchStorage storage;\n  ElasticsearchAutocompleteTags tagStore;\n\n  @BeforeEach void setUp() {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .autocompleteKeys(List.of(\"http#host\", \"http-url\", \"http.method\")).build();\n    tagStore = new ElasticsearchAutocompleteTags(storage);\n  }\n\n  @AfterEach void tearDown() {\n    storage.close();\n  }\n\n  @Test void get_list_of_autocomplete_keys() throws Exception {\n    // note: we don't enqueue a request!\n    assertThat(tagStore.getKeys().execute())\n      .contains(\"http#host\", \"http-url\", \"http.method\");\n  }\n\n  @Test void getValues_requestIncludesKeyName() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n    tagStore.getValues(\"http.method\").execute();\n    assertThat(server.takeRequest().request().contentUtf8()).contains(\"\\\"tagKey\\\":\\\"http.method\\\"\");\n  }\n\n  @Test void getValues() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    assertThat(tagStore.getValues(\"http.method\").execute()).containsOnly(\"get\", \"post\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchSpanConsumerTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.storage.SpanConsumer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.UTF_8;\n\nclass ElasticsearchSpanConsumerTest {\n  static final Endpoint WEB_ENDPOINT = Endpoint.newBuilder().serviceName(\"web\").build();\n  static final Endpoint APP_ENDPOINT = Endpoint.newBuilder().serviceName(\"app\").build();\n\n  final AggregatedHttpResponse SUCCESS_RESPONSE =\n    AggregatedHttpResponse.of(ResponseHeaders.of(HttpStatus.OK), HttpData.empty());\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  ElasticsearchStorage storage;\n  SpanConsumer spanConsumer;\n\n  @BeforeEach void setUp() throws Exception {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .autocompleteKeys(List.of(\"environment\"))\n      .build();\n\n    ensureIndexTemplate();\n  }\n\n  @AfterEach void tearDown() throws IOException {\n    storage.close();\n  }\n\n  void ensureIndexTemplate() throws Exception {\n    // gets the index template so that each test doesn't have to\n    ensureIndexTemplates(storage);\n    spanConsumer = storage.spanConsumer();\n  }\n\n  private void ensureIndexTemplates(ElasticsearchStorage storage) throws InterruptedException {\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,\n      \"{\\\"version\\\":{\\\"number\\\":\\\"6.0.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get tags template\n    storage.ensureIndexTemplates();\n    server.takeRequest(); // get version\n    server.takeRequest(); // get span template\n    server.takeRequest(); // get dependency template\n    server.takeRequest(); // get tags template\n  }\n\n  @Test void addsTimestamp_millisIntoJson() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span span =\n      Span.newBuilder().traceId(\"20\").id(\"20\").name(\"get\").timestamp(TODAY * 1000).build();\n\n    accept(span);\n\n    assertThat(server.takeRequest().request().contentUtf8())\n      .contains(\"\\n{\\\"timestamp_millis\\\":\" + TODAY + \",\\\"traceId\\\":\");\n  }\n\n  @Test void writesSpanNaturallyWhenNoTimestamp() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span span = Span.newBuilder().traceId(\"1\").id(\"1\").name(\"foo\").build();\n    accept(Span.newBuilder().traceId(\"1\").id(\"1\").name(\"foo\").build());\n\n    assertThat(server.takeRequest().request().contentUtf8())\n      .contains(\"\\n\" + new String(SpanBytesEncoder.JSON_V2.encode(span), UTF_8) + \"\\n\");\n  }\n\n  @Test void traceIsSearchableByServerServiceName() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span clientSpan =\n      Span.newBuilder()\n        .traceId(\"20\")\n        .id(\"22\")\n        .name(\"\")\n        .parentId(\"21\")\n        .timestamp(1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(WEB_ENDPOINT)\n        .build();\n\n    Span serverSpan =\n      Span.newBuilder()\n        .traceId(\"20\")\n        .id(\"22\")\n        .name(\"get\")\n        .parentId(\"21\")\n        .timestamp(2000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(APP_ENDPOINT)\n        .build();\n\n    accept(serverSpan, clientSpan);\n\n    // make sure that both timestamps are in the index\n    assertThat(server.takeRequest().request().contentUtf8())\n      .contains(\"{\\\"timestamp_millis\\\":2\")\n      .contains(\"{\\\"timestamp_millis\\\":1\");\n  }\n\n  @Test void addsPipelineId() throws Exception {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .pipeline(\"zipkin\")\n      .build();\n    ensureIndexTemplate();\n\n    server.enqueue(SUCCESS_RESPONSE);\n\n    accept(Span.newBuilder().traceId(\"1\").id(\"1\").name(\"foo\").build());\n\n    AggregatedHttpRequest request = server.takeRequest().request();\n    assertThat(request.path()).isEqualTo(\"/_bulk?pipeline=zipkin\");\n  }\n\n  @Test void choosesTypeSpecificIndex() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span span =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"2\")\n        .parentId(\"1\")\n        .name(\"s\")\n        .localEndpoint(APP_ENDPOINT)\n        .addAnnotation(TimeUnit.DAYS.toMicros(365) /* 1971-01-01 */, \"foo\")\n        .build();\n\n    // sanity check data\n    assertThat(span.timestamp()).isNull();\n\n    accept(span);\n\n    // index timestamp is the server timestamp, not current time!\n    assertThat(server.takeRequest().request().contentUtf8())\n      .startsWith(\"{\\\"index\\\":{\\\"_index\\\":\\\"zipkin:span-1971-01-01\\\",\\\"_type\\\":\\\"span\\\"\");\n  }\n\n  /** Much simpler template which doesn't write the timestamp_millis field */\n  @Test void searchDisabled_simplerIndexTemplate() throws Exception {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .searchEnabled(false)\n      .build();\n\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,\n      \"{\\\"version\\\":{\\\"number\\\":\\\"6.0.0\\\"}}\"));\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.NOT_FOUND)); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // put span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get tags template\n    storage.ensureIndexTemplates();\n    server.takeRequest(); // get version\n    server.takeRequest(); // get span template\n\n    assertThat(server.takeRequest().request().contentUtf8()) // put span template\n      .contains(\n        \"\"\"\n          \"mappings\": {\n            \"span\": {\n              \"properties\": {\n                \"traceId\": { \"type\": \"keyword\", \"norms\": false },\n                \"annotations\": { \"enabled\": false },\n                \"tags\": { \"enabled\": false }\n              }\n            }\n          }\n        \"\"\");\n  }\n\n  /** Less overhead as a span json isn't rewritten to include a millis timestamp */\n  @Test void searchDisabled_doesntAddTimestampMillis() throws Exception {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .searchEnabled(false)\n      .build();\n    ensureIndexTemplates(storage);\n    server.enqueue(SUCCESS_RESPONSE); // for the bulk request\n\n    Span span =\n      Span.newBuilder().traceId(\"20\").id(\"20\").name(\"get\").timestamp(TODAY * 1000).build();\n\n    storage.spanConsumer().accept(List.of(span)).execute();\n\n    assertThat(server.takeRequest().request().contentUtf8()).doesNotContain(\"timestamp_millis\");\n  }\n\n  @Test void addsAutocompleteValue() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    accept(Span.newBuilder().traceId(\"1\").id(\"1\").timestamp(1).putTag(\"environment\", \"A\").build());\n\n    assertThat(server.takeRequest().request().contentUtf8())\n      .endsWith(\"\"\"\n        {\"index\":{\"_index\":\"zipkin:autocomplete-1970-01-01\",\"_type\":\"autocomplete\",\"_id\":\"environment=A\"}}\n        {\"tagKey\":\"environment\",\"tagValue\":\"A\"}\n        \"\"\");\n  }\n\n  @Test void addsAutocompleteValue_suppressesWhenSameDay() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span s = Span.newBuilder().traceId(\"1\").id(\"1\").timestamp(1).putTag(\"environment\", \"A\").build();\n    accept(s);\n    accept(s.toBuilder().id(2).build());\n\n    server.takeRequest(); // skip first\n    // the tag is in the same date range as the other, so it should not write the tag again\n    assertThat(server.takeRequest().request().contentUtf8())\n      .doesNotContain(\"autocomplete\");\n  }\n\n  @Test void addsAutocompleteValue_differentDays() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span s = Span.newBuilder().traceId(\"1\").id(\"1\").timestamp(1).putTag(\"environment\", \"A\").build();\n    accept(s);\n    accept(s.toBuilder().id(2).timestamp(1 + TimeUnit.DAYS.toMicros(1)).build());\n\n    server.takeRequest(); // skip first\n    // different day == different context\n    assertThat(server.takeRequest().request().contentUtf8())\n      .endsWith(\"\"\"\n        {\"index\":{\"_index\":\"zipkin:autocomplete-1970-01-02\",\"_type\":\"autocomplete\",\"_id\":\"environment=A\"}}\n        {\"tagKey\":\"environment\",\"tagValue\":\"A\"}\n        \"\"\");\n  }\n\n  @Test void addsAutocompleteValue_revertsSuppressionOnFailure() throws Exception {\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR));\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Span s = Span.newBuilder().traceId(\"1\").id(\"1\").timestamp(1).putTag(\"environment\", \"A\").build();\n    try {\n      accept(s);\n      failBecauseExceptionWasNotThrown(RuntimeException.class);\n    } catch (RuntimeException expected) {\n    }\n    accept(s);\n\n    // We only cache when there was no error.. the second request should be same as the first\n    assertThat(server.takeRequest().request().contentUtf8())\n      .isEqualTo(server.takeRequest().request().contentUtf8());\n  }\n\n  void accept(Span... spans) throws Exception {\n    spanConsumer.accept(List.of(spans)).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchSpanStoreTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport java.io.IOException;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.TestObjects;\nimport zipkin2.storage.QueryRequest;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;\n\nclass ElasticsearchSpanStoreTest {\n  static final AggregatedHttpResponse EMPTY_RESPONSE =\n    AggregatedHttpResponse.of(ResponseHeaders.of(HttpStatus.OK), HttpData.empty());\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  @BeforeEach void setUp() {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri())).build();\n    spanStore = new ElasticsearchSpanStore(storage);\n  }\n\n  @AfterEach void tearDown() throws IOException {\n    storage.close();\n  }\n\n  ElasticsearchStorage storage;\n  ElasticsearchSpanStore spanStore;\n\n  @Test void doesntTruncateTraceIdByDefault() throws Exception {\n    server.enqueue(EMPTY_RESPONSE);\n    spanStore.getTrace(\"48fec942f3e78b893041d36dc43227fd\").execute();\n\n    assertThat(server.takeRequest().request().contentUtf8())\n      .contains(\"\\\"traceId\\\":\\\"48fec942f3e78b893041d36dc43227fd\\\"\");\n  }\n\n  @Test void truncatesTraceIdTo16CharsWhenNotStrict() throws Exception {\n    storage.close();\n    storage = storage.toBuilder().strictTraceId(false).build();\n    spanStore = new ElasticsearchSpanStore(storage);\n\n    server.enqueue(EMPTY_RESPONSE);\n    spanStore.getTrace(\"48fec942f3e78b893041d36dc43227fd\").execute();\n\n    assertThat(server.takeRequest().request().contentUtf8())\n      .contains(\"\\\"traceId\\\":\\\"3041d36dc43227fd\\\"\");\n  }\n\n  @Test void serviceNames_defaultsTo24HrsAgo_6x() throws Exception {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, TestResponses.SERVICE_NAMES));\n    spanStore.getServiceNames().execute();\n\n    requestLimitedTo2DaysOfIndices_singleTypeIndex();\n  }\n\n  @Test void spanNames_defaultsTo24HrsAgo_6x() throws Exception {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, TestResponses.SPAN_NAMES));\n    spanStore.getSpanNames(\"foo\").execute();\n\n    requestLimitedTo2DaysOfIndices_singleTypeIndex();\n  }\n\n  @Test void searchDisabled_doesntMakeRemoteQueryRequests() throws Exception {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri()))\n      .searchEnabled(false)\n      .build();\n\n    // skip template check\n    ElasticsearchSpanStore spanStore = new ElasticsearchSpanStore(storage);\n\n    QueryRequest request = QueryRequest.newBuilder().endTs(TODAY).lookback(DAY).limit(10).build();\n    assertThat(spanStore.getTraces(request).execute()).isEmpty();\n    assertThat(spanStore.getServiceNames().execute()).isEmpty();\n    assertThat(spanStore.getSpanNames(\"icecream\").execute()).isEmpty();\n\n    assertThat(server.takeRequest(100, TimeUnit.MILLISECONDS)).isNull();\n  }\n\n  void requestLimitedTo2DaysOfIndices_singleTypeIndex() {\n    long today = TestObjects.midnightUTC(System.currentTimeMillis());\n    long yesterday = today - TimeUnit.DAYS.toMillis(1);\n\n    // 24 hrs ago always will fall into 2 days (ex. if it is 4:00pm, 24hrs ago is a different day)\n    String indexesToSearch = storage.indexNameFormatter().formatTypeAndTimestamp(TYPE_SPAN, yesterday)\n      + \",\"\n      + storage.indexNameFormatter().formatTypeAndTimestamp(TYPE_SPAN, today);\n\n    AggregatedHttpRequest request = server.takeRequest().request();\n    assertThat(request.path()).startsWith(\"/\" + indexesToSearch + \"/_search\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchSpecificTemplatesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.mock;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V5_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V7_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V7_8;\n\nclass ElasticsearchSpecificTemplatesTest {\n  static final ElasticsearchVersion V2_4 = new ElasticsearchVersion(2, 4);\n  static final ElasticsearchVersion V6_7 = new ElasticsearchVersion(6, 7);\n  static final ElasticsearchVersion V7_9 = new ElasticsearchVersion(7, 9);\n\n  ElasticsearchStorage storage =\n    ElasticsearchStorage.newBuilder(() -> mock(WebClient.class)).build();\n\n  /** Unsupported, but we should test that parsing works */\n  @Test void version2_unsupported() {\n    assertThatThrownBy(() -> storage.versionSpecificTemplates(V2_4))\n      .hasMessage(\"Elasticsearch versions 5-8.x are supported, was: 2.4\");\n  }\n\n  @Test void version5() {\n    IndexTemplates template = storage.versionSpecificTemplates(V5_0);\n\n    assertThat(template.version()).isEqualTo(V5_0);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"In v5.x, the index_patterns field was named template\")\n      .contains(\"\\\"template\\\":\");\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Until v7.x, we delimited index and type with a colon\")\n      .contains(\"\\\"template\\\": \\\"zipkin:autocomplete-*\\\"\");\n  }\n\n  @Test void version6() {\n    IndexTemplates template = storage.versionSpecificTemplates(V6_7);\n\n    assertThat(template.version()).isEqualTo(V6_7);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Until v7.x, we delimited index and type with a colon\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin:autocomplete-*\\\"\");\n  }\n\n  @Test void version6_wrapsPropertiesWithType() {\n    IndexTemplates template = storage.versionSpecificTemplates(V6_7);\n\n    assertThat(template.dependency()).contains(\"\"\"\n        \"mappings\": {\n          \"dependency\": {\n            \"enabled\": false\n          }\n        }\\\n      \"\"\");\n\n    assertThat(template.autocomplete()).contains(\"\"\"\n        \"mappings\": {\n          \"autocomplete\": {\n            \"enabled\": true,\n            \"properties\": {\n              \"tagKey\": { \"type\": \"keyword\", \"norms\": false },\n              \"tagValue\": { \"type\": \"keyword\", \"norms\": false }\n            }\n          }\n        }\\\n      \"\"\");\n  }\n\n  @Test void version7() {\n    IndexTemplates template = storage.versionSpecificTemplates(V7_0);\n\n    assertThat(template.version()).isEqualTo(V7_0);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.autocomplete())\n      .withFailMessage(\"7.x does not support the key index.mapper.dynamic\")\n      .doesNotContain(\"\\\"index.mapper.dynamic\\\": false\");\n  }\n\n  @Test void version7_doesntWrapPropertiesWithType() {\n    IndexTemplates template = storage.versionSpecificTemplates(V7_0);\n\n    assertThat(template.dependency()).contains(\"\"\"\n        \"mappings\": {\n          \"enabled\": false\n        }\\\n      \"\"\");\n\n    assertThat(template.autocomplete()).contains(\"\"\"\n        \"mappings\": {\n          \"enabled\": true,\n          \"properties\": {\n            \"tagKey\": { \"type\": \"keyword\", \"norms\": false },\n            \"tagValue\": { \"type\": \"keyword\", \"norms\": false }\n          }\n        }\\\n      \"\"\");\n  }\n\n  @Test void version78_legacy() {\n    IndexTemplates template = storage.versionSpecificTemplates(V7_8);\n\n    assertThat(template.version()).isEqualTo(V7_8);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.span())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.autocomplete())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.dependency())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n  }\n\n  @Test void version78_composable() {\n    // Set up a new storage with priority\n    storage.close();\n    storage =\n      ElasticsearchStorage.newBuilder(() -> mock(WebClient.class)).templatePriority(0).build();\n    IndexTemplates template = storage.versionSpecificTemplates(V7_8);\n\n    assertThat(template.version()).isEqualTo(V7_8);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.span())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.autocomplete())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.dependency())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n  }\n\n  @Test void version79_legacy() {\n    IndexTemplates template = storage.versionSpecificTemplates(V7_9);\n\n    assertThat(template.version()).isEqualTo(V7_9);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.span())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.autocomplete())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.dependency())\n      .doesNotContain(\"\\\"template\\\": {\\n\")\n      .doesNotContain(\"\\\"priority\\\": 0\\n\");\n  }\n\n  @Test void version79_composable() {\n    // Set up a new storage with priority\n    storage.close();\n    storage =\n      ElasticsearchStorage.newBuilder(() -> mock(WebClient.class)).templatePriority(0).build();\n    IndexTemplates template = storage.versionSpecificTemplates(V7_9);\n\n    assertThat(template.version()).isEqualTo(V7_9);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.span())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.autocomplete())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n    assertThat(template.dependency())\n      .contains(\"\\\"template\\\": {\\n\")\n      .contains(\"\\\"priority\\\": 0\\n\");\n  }\n\n  @Test void searchEnabled_minimalSpanIndexing_6x() {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> mock(WebClient.class))\n      .searchEnabled(false)\n      .build();\n\n    IndexTemplates template = storage.versionSpecificTemplates(V6_7);\n\n    assertThat(template.span())\n      .contains(\"\"\"\n          \"mappings\": {\n            \"span\": {\n              \"properties\": {\n                \"traceId\": { \"type\": \"keyword\", \"norms\": false },\n                \"annotations\": { \"enabled\": false },\n                \"tags\": { \"enabled\": false }\n              }\n            }\n          }\\\n        \"\"\");\n  }\n\n  @Test void searchEnabled_minimalSpanIndexing_7x() {\n    storage = ElasticsearchStorage.newBuilder(() -> mock(WebClient.class))\n      .searchEnabled(false)\n      .build();\n\n    IndexTemplates template = storage.versionSpecificTemplates(V7_0);\n\n    // doesn't wrap in a type name\n    assertThat(template.span())\n      .contains(\"\"\"\n          \"mappings\": {\n            \"properties\": {\n              \"traceId\": { \"type\": \"keyword\", \"norms\": false },\n              \"annotations\": { \"enabled\": false },\n              \"tags\": { \"enabled\": false }\n            }\n          }\\\n        \"\"\");\n  }\n\n  @Test void strictTraceId_doesNotIncludeAnalysisSection() {\n    IndexTemplates template = storage.versionSpecificTemplates(V6_7);\n\n    assertThat(template.span()).doesNotContain(\"analysis\");\n  }\n\n  @Test void strictTraceId_false_includesAnalysisForMixedLengthTraceId() {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> mock(WebClient.class))\n      .strictTraceId(false)\n      .build();\n\n    IndexTemplates template = storage.versionSpecificTemplates(V6_7);\n\n    assertThat(template.span()).contains(\"analysis\");\n  }\n  \n  @Test void indexTemplatesUrl_6x() {\n    assertThat(VersionSpecificTemplates.forVersion(V6_7).indexTemplatesUrl(\"idx\", \"_doc\", null))\n      .isEqualTo(\"/_template/idx_doc_template?include_type_name=true\");\n  }\n\n  @Test void indexTemplatesUrl_6x_withPriority() {\n    assertThat(VersionSpecificTemplates.forVersion(V6_7).indexTemplatesUrl(\"idx\", \"_doc\", 1))\n      .isEqualTo(\"/_template/idx_doc_template?include_type_name=true\");\n  }\n\n  @Test void indexTemplatesUrl_78() {\n    assertThat(VersionSpecificTemplates.forVersion(V7_8).indexTemplatesUrl(\"idx\", \"_doc\", null))\n      .isEqualTo(\"/_template/idx_doc_template\");\n  }\n\n  @Test void indexTemplatesUrl_78_withPriority() {\n    assertThat(VersionSpecificTemplates.forVersion(V7_8).indexTemplatesUrl(\"idx\", \"_doc\", 1))\n      .isEqualTo(\"/_index_template/idx_doc_template\");\n  }\n  \n  @Test void indexTemplatesUrl_79() {\n    assertThat(VersionSpecificTemplates.forVersion(V7_9).indexTemplatesUrl(\"idx\", \"_doc\", null))\n      .isEqualTo(\"/_template/idx_doc_template\");\n  }\n\n  @Test void indexTemplatesUrl_79_withPriority() {\n    assertThat(VersionSpecificTemplates.forVersion(V7_9).indexTemplatesUrl(\"idx\", \"_doc\", 1))\n      .isEqualTo(\"/_index_template/idx_doc_template\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchStorageTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.ResponseTimeoutException;\nimport com.linecorp.armeria.client.UnprocessedRequestException;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroupException;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport java.time.Instant;\nimport java.util.concurrent.RejectedExecutionException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\nimport zipkin2.elasticsearch.ElasticsearchStorage.LazyHttpClient;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.DAY;\n\nclass ElasticsearchStorageTest {\n  static final AggregatedHttpResponse SUCCESS_RESPONSE =\n    AggregatedHttpResponse.of(ResponseHeaders.of(HttpStatus.OK), HttpData.empty());\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  ElasticsearchStorage storage;\n\n  @BeforeEach void setUp() {\n    storage = newBuilder().build();\n  }\n\n  @AfterEach void tearDown() {\n    storage.close();\n  }\n\n  @Test void ensureIndexTemplates_false() throws Exception {\n    storage.close();\n    storage = newBuilder().ensureTemplates(false).build();\n\n    server.enqueue(SUCCESS_RESPONSE); // dependencies request\n\n    long endTs = Instant.parse(\"2016-10-02T00:00:00Z\").toEpochMilli();\n    storage.spanStore().getDependencies(endTs, DAY).execute();\n\n    assertThat(server.takeRequest().request().path())\n      .startsWith(\"/zipkin*dependency-2016-10-01,zipkin*dependency-2016-10-02/_search\");\n  }\n\n  @Test void memoizesIndexTemplate() throws Exception {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"6.7.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get tags template\n    server.enqueue(SUCCESS_RESPONSE); // dependencies request\n    server.enqueue(SUCCESS_RESPONSE); // dependencies request\n\n    long endTs = Instant.parse(\"2016-10-02T00:00:00Z\").toEpochMilli();\n    storage.spanStore().getDependencies(endTs, DAY).execute();\n    storage.spanStore().getDependencies(endTs, DAY).execute();\n\n    server.takeRequest(); // get version\n    server.takeRequest(); // get span template\n    server.takeRequest(); // get dependency template\n    server.takeRequest(); // get tags template\n\n    assertThat(server.takeRequest().request().path())\n      .startsWith(\"/zipkin*dependency-2016-10-01,zipkin*dependency-2016-10-02/_search\");\n    assertThat(server.takeRequest().request().path())\n      .startsWith(\"/zipkin*dependency-2016-10-01,zipkin*dependency-2016-10-02/_search\");\n  }\n\n  static final AggregatedHttpResponse HEALTH_RESPONSE = AggregatedHttpResponse.of(\n    HttpStatus.OK,\n    MediaType.JSON_UTF_8,\n    \"\"\"\n    {\n      \"cluster_name\": \"elasticsearch_zipkin\",\n      \"status\": \"yellow\",\n      \"timed_out\": false,\n      \"number_of_nodes\": 1,\n      \"number_of_data_nodes\": 1,\n      \"active_primary_shards\": 5,\n      \"active_shards\": 5,\n      \"relocating_shards\": 0,\n      \"initializing_shards\": 0,\n      \"unassigned_shards\": 5,\n      \"delayed_unassigned_shards\": 0,\n      \"number_of_pending_tasks\": 0,\n      \"number_of_in_flight_fetch\": 0,\n      \"task_max_waiting_in_queue_millis\": 0,\n      \"active_shards_percent_as_number\": 50\n    }\n    \"\"\");\n\n  static final AggregatedHttpResponse RESPONSE_UNAUTHORIZED = AggregatedHttpResponse.of(\n    HttpStatus.UNAUTHORIZED,\n    MediaType.JSON_UTF_8, // below is actual message from Amazon\n    \"{\\\"Message\\\":\\\"User: anonymous is not authorized to perform: es:ESHttpGet\\\"}}\");\n\n  static final AggregatedHttpResponse RESPONSE_VERSION_6 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"6.7.0\\\"}}\");\n\n  @Test void check_ensuresIndexTemplates_memozied() {\n    server.enqueue(RESPONSE_VERSION_6);\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get tags template\n\n    server.enqueue(HEALTH_RESPONSE);\n\n    assertThat(storage.check()).isEqualTo(CheckResult.OK);\n\n    // Later checks do not redo index template requests\n    server.enqueue(HEALTH_RESPONSE);\n\n    assertThat(storage.check()).isEqualTo(CheckResult.OK);\n  }\n\n  // makes sure we don't NPE\n  @Test void check_ensuresIndexTemplates_fail_onNoContent() {\n    server.enqueue(SUCCESS_RESPONSE); // empty instead of version json\n\n    CheckResult result = storage.check();\n    assertThat(result.ok()).isFalse();\n    assertThat(result.error().getMessage())\n      .isEqualTo(\"No content reading Elasticsearch/OpenSearch version\");\n  }\n\n  // makes sure we don't NPE\n  @Test void check_fail_onNoContent() {\n    storage.ensuredTemplates = true; // assume index templates called before\n\n    server.enqueue(SUCCESS_RESPONSE); // empty instead of success response\n\n    CheckResult result = storage.check();\n    assertThat(result.ok()).isFalse();\n    assertThat(result.error().getMessage())\n      .isEqualTo(\"No content reading Elasticsearch/OpenSearch version\");\n  }\n\n  // TODO: when Armeria's mock server supports it, add a test for IOException\n\n  @Test void check_unauthorized() {\n    server.enqueue(RESPONSE_UNAUTHORIZED);\n\n    CheckResult result = storage.check();\n    assertThat(result.ok()).isFalse();\n    assertThat(result.error().getMessage())\n      .isEqualTo(\"User: anonymous is not authorized to perform: es:ESHttpGet\");\n  }\n\n  /**\n   * See {@link HttpCallTest#unprocessedRequest()} which shows {@link UnprocessedRequestException}\n   * are re-wrapped as {@link RejectedExecutionException}.\n   */\n  @Test void isOverCapacity() {\n    // timeout\n    assertThat(storage.isOverCapacity(ResponseTimeoutException.get())).isTrue();\n\n    // top-level\n    assertThat(storage.isOverCapacity(new RejectedExecutionException(\n      \"{\\\"status\\\":429,\\\"error\\\":{\\\"type\\\":\\\"es_rejected_execution_exception\\\"}}\"))).isTrue();\n\n    // re-wrapped\n    assertThat(storage.isOverCapacity(\n      new RejectedExecutionException(\"Rejected execution: No endpoints.\",\n        new EndpointGroupException(\"No endpoints\")))).isTrue();\n\n    // not applicable\n    assertThat(storage.isOverCapacity(new IllegalStateException(\"Rejected execution\"))).isFalse();\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    assertThat(storage).hasToString(\n      \"ElasticsearchStorage{initialEndpoints=%s, index=zipkin}\".formatted(server.httpUri()));\n  }\n\n  /** Ensure that Zipkin doesn't include \"include_type_name\" parameter with unsupported versions */\n  @Test void check_create_indexTemplate_resourcePath__version66() {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"6.6.6\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_template/zipkin:span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_template/zipkin:dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_template/zipkin:autocomplete_template\");\n  }\n\n  /** Ensure that Zipkin includes \"include_type_name\" parameter with 6.7 */\n  @Test void check_create_indexTemplate_resourcePath_version67() {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"6.7.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_template/zipkin:span_template?include_type_name=true\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_template/zipkin:dependency_template?include_type_name=true\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_template/zipkin:autocomplete_template?include_type_name=true\");\n  }\n\n  /** Ensure that Zipkin doesn't include \"include_type_name\" parameter with version >7.0 */\n  @Test void check_create_indexTemplate_resourcePath_version71() {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"7.0.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_template/zipkin-span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_template/zipkin-dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_template/zipkin-autocomplete_template\");\n  }\n\n  /** Ensure that Zipkin uses the legacy resource path when priority is not set. */\n  @Test void check_create_legacy_indexTemplate_resourcePath_version78() {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"7.8.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_template/zipkin-span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_template/zipkin-dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_template/zipkin-autocomplete_template\");\n  }\n\n  /**\n   * Ensure that Zipkin uses the correct resource path of /_index_template when creating index\n   * template for ES 7.8 when priority is set, as opposed to ES < 7.8 that uses /_template/\n   */\n  @Test void check_create_composable_indexTemplate_resourcePath_version78() {\n    // Set up a new storage with priority\n    storage.close();\n    storage = newBuilder().templatePriority(0).build();\n\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"7.8.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_index_template/zipkin-span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_index_template/zipkin-dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_index_template/zipkin-autocomplete_template\");\n  }\n\n  /** Ensure that Zipkin uses the legacy resource path when priority is not set. */\n  @Test void check_create_legacy_indexTemplate_resourcePath_version79() {\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"7.9.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_template/zipkin-span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_template/zipkin-dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_template/zipkin-autocomplete_template\");\n  }\n\n  /**\n   * Ensure that Zipkin uses the correct resource path of /_index_template when creating index\n   * template for ES 7.9 when priority is set, as opposed to ES < 7.8 that uses /_template/\n   */\n  @Test void check_create_composable_indexTemplate_resourcePath_version79() throws Exception {\n    // Set up a new storage with priority\n    storage.close();\n    storage = newBuilder().templatePriority(0).build();\n\n    server.enqueue(AggregatedHttpResponse.of(\n      HttpStatus.OK, MediaType.JSON_UTF_8, \"{\\\"version\\\":{\\\"number\\\":\\\"7.9.0\\\"}}\"));\n    server.enqueue(SUCCESS_RESPONSE); // get span template\n    server.enqueue(SUCCESS_RESPONSE); // get dependency template\n    server.enqueue(SUCCESS_RESPONSE); // get autocomplete template\n    server.enqueue(SUCCESS_RESPONSE); // cluster health\n\n    storage.check();\n\n    server.takeRequest(); // get version\n\n    assertThat(server.takeRequest().request().path()) // get span template\n      .startsWith(\"/_index_template/zipkin-span_template\");\n    assertThat(server.takeRequest().request().path()) // // get dependency template\n      .startsWith(\"/_index_template/zipkin-dependency_template\");\n    assertThat(server.takeRequest().request().path()) // get autocomplete template\n      .startsWith(\"/_index_template/zipkin-autocomplete_template\");\n  }\n\n  ElasticsearchStorage.Builder newBuilder() {\n    return ElasticsearchStorage.newBuilder(new LazyHttpClient() {\n      @Override public WebClient get() {\n        return WebClient.of(server.httpUri());\n      }\n\n      @Override public String toString() {\n        return server.httpUri().toString();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/ElasticsearchVersionTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.elasticsearch.ElasticsearchStorageTest.RESPONSE_UNAUTHORIZED;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V5_0;\nimport static zipkin2.elasticsearch.ElasticsearchVersion.V7_0;\n\nclass ElasticsearchVersionTest {\n  static final ElasticsearchVersion V2_4 = new ElasticsearchVersion(2, 4);\n  static final ElasticsearchVersion V6_7 = new ElasticsearchVersion(6, 7);\n\n  static final AggregatedHttpResponse VERSION_RESPONSE_7 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"zipkin-elasticsearch\",\n        \"cluster_name\" : \"docker-cluster\",\n        \"cluster_uuid\" : \"wByRPgSgTryYl0TZXW4MsA\",\n        \"version\" : {\n          \"number\" : \"7.0.1\",\n          \"build_flavor\" : \"default\",\n          \"build_type\" : \"tar\",\n          \"build_hash\" : \"e4efcb5\",\n          \"build_date\" : \"2019-04-29T12:56:03.145736Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"8.0.0\",\n          \"minimum_wire_compatibility_version\" : \"6.7.0\",\n          \"minimum_index_compatibility_version\" : \"6.0.0-beta1\"\n        },\n        \"tagline\" : \"You Know, for Search\"\n      }\n      \"\"\");\n  static final AggregatedHttpResponse VERSION_RESPONSE_6 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"PV-NhJd\",\n        \"cluster_name\" : \"CollectorDBCluster\",\n        \"cluster_uuid\" : \"UjZaM0fQRC6tkHINCg9y8w\",\n        \"version\" : {\n          \"number\" : \"6.7.0\",\n          \"build_flavor\" : \"oss\",\n          \"build_type\" : \"tar\",\n          \"build_hash\" : \"8453f77\",\n          \"build_date\" : \"2019-03-21T15:32:29.844721Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"7.7.0\",\n          \"minimum_wire_compatibility_version\" : \"5.6.0\",\n          \"minimum_index_compatibility_version\" : \"5.0.0\"\n        },\n        \"tagline\" : \"You Know, for Search\"\n      }\n      \"\"\");\n  static final AggregatedHttpResponse VERSION_RESPONSE_5 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"vU0g1--\",\n        \"cluster_name\" : \"elasticsearch\",\n        \"cluster_uuid\" : \"Fnm277ITSNyzsy0UCVFN7g\",\n        \"version\" : {\n          \"number\" : \"5.0.0\",\n          \"build_hash\" : \"253032b\",\n          \"build_date\" : \"2016-10-26T04:37:51.531Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"6.2.0\"\n        },\n        \"tagline\" : \"You Know, for Search\"\n      }\n      \"\"\");\n  static final AggregatedHttpResponse VERSION_RESPONSE_2 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"Kamal\",\n        \"cluster_name\" : \"elasticsearch\",\n        \"version\" : {\n          \"number\" : \"2.4.0\",\n          \"build_hash\" : \"ce9f0c7394dee074091dd1bc4e9469251181fc55\",\n          \"build_timestamp\" : \"2016-08-29T09:14:17Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"5.5.2\"\n        },\n        \"tagline\" : \"You Know, for Search\"\n      }\n      \"\"\");\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  @BeforeEach void setUp() {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri())).build();\n  }\n\n  @AfterEach void tearDown() {\n    storage.close();\n  }\n\n  ElasticsearchStorage storage;\n\n  @Test void wrongContent() {\n    server.enqueue(AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.OK),\n      HttpData.ofUtf8(\"you got mail\")));\n\n    assertThatThrownBy(() -> ElasticsearchVersion.get(storage.http()))\n      .hasMessage(\".version.number not found in response: you got mail\");\n  }\n\n  @Test void unauthorized() {\n    server.enqueue(RESPONSE_UNAUTHORIZED);\n\n    assertThatThrownBy(() -> ElasticsearchVersion.get(storage.http()))\n      .hasMessage(\"User: anonymous is not authorized to perform: es:ESHttpGet\");\n  }\n\n  /** Unsupported, but we should test that parsing works */\n  @Test void version2() throws Exception {\n    server.enqueue(VERSION_RESPONSE_2);\n\n    assertThat(ElasticsearchVersion.get(storage.http()))\n      .isEqualTo(V2_4);\n  }\n\n  @Test void version5() throws Exception {\n    server.enqueue(VERSION_RESPONSE_5);\n\n    assertThat(ElasticsearchVersion.get(storage.http()))\n      .isEqualTo(V5_0);\n  }\n\n  @Test void version6() throws Exception {\n    server.enqueue(VERSION_RESPONSE_6);\n\n    assertThat(ElasticsearchVersion.get(storage.http()))\n      .isEqualTo(V6_7);\n  }\n\n  @Test void version7() throws Exception {\n    server.enqueue(VERSION_RESPONSE_7);\n\n    assertThat(ElasticsearchVersion.get(storage.http()))\n      .isEqualTo(V7_0);\n  }\n\n  /** Prove we compare better than a float. A float of 7.10 is the same as 7.1! */\n  @Test void version7_10IsGreaterThan_V7_2() {\n    assertThat(new ElasticsearchVersion(7, 10))\n      .hasToString(\"7.10\")\n      .isGreaterThan(new ElasticsearchVersion(7, 2));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/InternalForTests.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport io.netty.buffer.ByteBufOutputStream;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport zipkin2.DependencyLink;\nimport zipkin2.elasticsearch.internal.BulkCallBuilder;\nimport zipkin2.elasticsearch.internal.BulkIndexWriter;\nimport zipkin2.elasticsearch.internal.JsonSerializers;\n\n/** Package accessor for integration tests */\npublic class InternalForTests {\n  public static void writeDependencyLinks(ElasticsearchStorage es, List<DependencyLink> links,\n    long midnightUTC) {\n    String index = ((ElasticsearchSpanConsumer) es.spanConsumer())\n      .formatTypeAndTimestampForInsert(\"dependency\", midnightUTC);\n    BulkCallBuilder indexer = new BulkCallBuilder(es, es.version(), \"indexlinks\");\n    for (DependencyLink link : links)\n      indexer.index(index, \"dependency\", link, DEPENDENCY_LINK_WRITER);\n    try {\n      indexer.build().execute();\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  static final BulkIndexWriter<DependencyLink> DEPENDENCY_LINK_WRITER =\n    new BulkIndexWriter<DependencyLink>() {\n      @Override public String writeDocument(DependencyLink link, ByteBufOutputStream sink) {\n        try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {\n          writer.writeStartObject();\n          writer.writeStringField(\"parent\", link.parent());\n          writer.writeStringField(\"child\", link.child());\n          writer.writeNumberField(\"callCount\", link.callCount());\n          if (link.errorCount() > 0) writer.writeNumberField(\"errorCount\", link.errorCount());\n          writer.writeEndObject();\n        } catch (IOException e) {\n          throw new AssertionError(e); // No I/O writing to a Buffer.\n        }\n        return link.parent() + \"|\" + link.child();\n      }\n    };\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/JsonReadersTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport java.io.IOException;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.elasticsearch.internal.JsonReaders;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;\nimport static zipkin2.elasticsearch.internal.JsonReaders.collectValuesNamed;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.JSON_FACTORY;\n\nclass JsonReadersTest {\n  @Test void enterPath_nested() throws IOException {\n    String content = \"\"\"\n      {\n        \"name\" : \"Kamal\",\n        \"cluster_name\" : \"elasticsearch\",\n        \"version\" : {\n          \"number\" : \"2.4.0\",\n          \"build_hash\" : \"ce9f0c7394dee074091dd1bc4e9469251181fc55\",\n          \"build_timestamp\" : \"2016-08-29T09:14:17Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"5.5.2\"\n        },\n        \"tagline\" : \"You Know, for Search\"\n      }\n      \"\"\";\n\n    assertThat(\n      JsonReaders.enterPath(JSON_FACTORY.createParser(content), \"version\", \"number\").getText())\n      .isEqualTo(\"2.4.0\");\n  }\n\n  @Test void enterPath_nullOnNoInput() throws IOException {\n    assertThat(JsonReaders.enterPath(JSON_FACTORY.createParser(\"\"), \"message\"))\n      .isNull();\n  }\n\n  @Test void enterPath_nullOnWrongInput() throws IOException {\n    assertThat(JsonReaders.enterPath(JSON_FACTORY.createParser(\"[]\"), \"message\"))\n      .isNull();\n  }\n\n  @Test void collectValuesNamed_emptyWhenNotFound() throws IOException {\n    String content = \"\"\"\n      {\n        \"took\": 1,\n        \"timed_out\": false,\n        \"_shards\": {\n          \"total\": 0,\n          \"successful\": 0,\n          \"failed\": 0\n        },\n        \"hits\": {\n          \"total\": 0,\n          \"max_score\": 0,\n          \"hits\": []\n        }\n      }\n      \"\"\";\n\n    assertThat(collectValuesNamed(JSON_FACTORY.createParser(content), \"key\")).isEmpty();\n  }\n\n  // All elasticsearch results start with an object, not an array.\n  @Test void collectValuesNamed_exceptionOnWrongData() throws IOException {\n    assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {\n      assertThat(collectValuesNamed(JSON_FACTORY.createParser(\"[]\"), \"key\")).isEmpty();\n    });\n  }\n\n  @Test void collectValuesNamed_mergesArrays() throws IOException {\n    List<String> result =\n      collectValuesNamed(JSON_FACTORY.createParser(TestResponses.SPAN_NAMES), \"key\");\n\n    assertThat(result).containsExactly(\"methodcall\", \"yak\");\n  }\n\n  @Test void collectValuesNamed_mergesChildren() throws IOException {\n    List<String> result =\n      collectValuesNamed(JSON_FACTORY.createParser(TestResponses.SERVICE_NAMES), \"key\");\n\n    assertThat(result).containsExactly(\"yak\", \"service\");\n  }\n\n  @Test void collectValuesNamed_nested() throws IOException {\n    String content = \"\"\"\n      {\n        \"took\": 49,\n        \"timed_out\": false,\n        \"_shards\": {\n          \"total\": 5,\n          \"successful\": 5,\n          \"failed\": 0\n        },\n        \"hits\": {\n          \"total\": 1,\n          \"max_score\": 0,\n          \"hits\": []\n        },\n        \"aggregations\": {\n          \"traceId_agg\": {\n            \"doc_count_error_upper_bound\": 0,\n            \"sum_other_doc_count\": 0,\n            \"buckets\": [\n              {\n                \"key\": \"000000000000007b\",\n                \"doc_count\": 1,\n                \"timestamps_agg\": {\n                  \"value\": 1474761600001,\n                  \"value_as_string\": \"1474761600001\"\n                }\n              }\n            ]\n          }\n        }\n      }\n      \"\"\";\n\n    assertThat(collectValuesNamed(JSON_FACTORY.createParser(content), \"key\"))\n      .containsExactly(\"000000000000007b\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/JsonSerializersTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.codec.DependencyLinkBytesEncoder;\nimport zipkin2.codec.SpanBytesEncoder;\nimport zipkin2.elasticsearch.internal.JsonSerializers;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.entry;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.UTF_8;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.SPAN_PARSER;\n\nclass JsonSerializersTest {\n  @Test void span_ignoreNull_parentId() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"parentId\": null\n      }\n      \"\"\";\n\n    parse(SPAN_PARSER, json);\n  }\n\n  @Test void span_ignoreNull_timestamp() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"timestamp\": null\n      }\n      \"\"\";\n\n    parse(SPAN_PARSER, json);\n  }\n\n  @Test void span_ignoreNull_duration() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"duration\": null\n      }\n      \"\"\";\n\n    parse(SPAN_PARSER, json);\n  }\n\n  @Test void span_ignoreNull_debug() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"debug\": null\n      }\n      \"\"\";\n\n    parse(SPAN_PARSER, json);\n  }\n\n  @Test void span_ignoreNull_annotation_endpoint() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"annotations\": [\n          {\n            \"timestamp\": 1461750491274000,\n            \"value\": \"cs\",\n            \"endpoint\": null\n          }\n        ]\n      }\n      \"\"\";\n\n    parse(SPAN_PARSER, json);\n  }\n\n  @Test void span_tag_long_read() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"tags\": {\n            \"num\": 9223372036854775807\n        }\n      }\n      \"\"\";\n\n    Span span = parse(SPAN_PARSER, json);\n    assertThat(span.tags()).containsExactly(entry(\"num\", \"9223372036854775807\"));\n  }\n\n  @Test void span_tag_double_read() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"tags\": {\n            \"num\": 1.23456789\n        }\n      }\n      \"\"\";\n\n    Span span = parse(SPAN_PARSER, json);\n    assertThat(span.tags()).containsExactly(entry(\"num\", \"1.23456789\"));\n  }\n\n  @Test void span_roundTrip() {\n    assertThat(parse(SPAN_PARSER, new String(SpanBytesEncoder.JSON_V2.encode(CLIENT_SPAN), UTF_8)))\n      .isEqualTo(CLIENT_SPAN);\n  }\n\n  /**\n   * This isn't a test of what we \"should\" accept as a span, rather that characters that trip-up\n   * json don't fail in SPAN_PARSER.\n   */\n  @Test void span_specialCharsInJson() {\n    // service name is surrounded by control characters\n    Endpoint e = Endpoint.newBuilder().serviceName(new String(new char[] {0, 'a', 1})).build();\n    Span worstSpanInTheWorld =\n      Span.newBuilder()\n        .traceId(\"1\")\n        .id(\"1\")\n        // name is terrible\n        .name(new String(new char[] {'\"', '\\\\', '\\t', '\\b', '\\n', '\\r', '\\f'}))\n        .localEndpoint(e)\n        // annotation value includes some json newline characters\n        .addAnnotation(1L, \"\\u2028 and \\u2029\")\n        // binary annotation key includes a quote and value newlines\n        .putTag(\n          \"\\\"foo\",\n          \"Database error: ORA-00942:\\u2028 and \\u2029 table or view does not exist\\n\")\n        .build();\n\n    assertThat(\n      parse(SPAN_PARSER, new String(SpanBytesEncoder.JSON_V2.encode(worstSpanInTheWorld), UTF_8)))\n      .isEqualTo(worstSpanInTheWorld);\n  }\n\n  @Test void span_endpointHighPort() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"serviceName\": \"service\",\n          \"port\": 65535\n        }\n      }\n      \"\"\";\n\n    assertThat(parse(SPAN_PARSER, json).localEndpoint())\n      .isEqualTo(Endpoint.newBuilder().serviceName(\"service\").port(65535).build());\n  }\n\n  @Test void span_noServiceName() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"port\": 65535\n        }\n      }\n      \"\"\";\n\n    assertThat(parse(SPAN_PARSER, json).localEndpoint())\n      .isEqualTo(Endpoint.newBuilder().serviceName(\"\").port(65535).build());\n  }\n\n  @Test void span_nullServiceName() {\n    String json =\n      \"\"\"\n      {\n        \"traceId\": \"6b221d5bc9e6496c\",\n        \"name\": \"get-traces\",\n        \"id\": \"6b221d5bc9e6496c\",\n        \"localEndpoint\": {\n          \"serviceName\": null,\n          \"port\": 65535\n        }\n      }\n      \"\"\";\n\n    assertThat(parse(SPAN_PARSER, json).localEndpoint())\n      .isEqualTo(Endpoint.newBuilder().serviceName(\"\").port(65535).build());\n  }\n\n  @Test void span_readsTraceIdHighFromTraceIdField() throws IOException {\n    String with128BitTraceId =\n      (\"\"\"\n        {\n          \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\");\n    String withLower64bitsTraceId =\n      (\"\"\"\n        {\n          \"traceId\": \"6b221d5bc9e6496c\",\n          \"name\": \"get-traces\",\n          \"id\": \"6b221d5bc9e6496c\"\n        }\n        \"\"\");\n\n    assertThat(parse(SPAN_PARSER, with128BitTraceId))\n      .isEqualTo(\n        parse(JsonSerializers.SPAN_PARSER, withLower64bitsTraceId)\n          .toBuilder()\n          .traceId(\"48485a3953bb61246b221d5bc9e6496c\")\n          .build());\n  }\n\n  @Test void dependencyLinkRoundTrip() {\n    DependencyLink link =\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(2).build();\n\n    assertThat(parse(JsonSerializers.DEPENDENCY_LINK_PARSER,\n      new String(DependencyLinkBytesEncoder.JSON_V1.encode(link), UTF_8))).isEqualTo(link);\n  }\n\n  @Test void dependencyLinkRoundTrip_withError() {\n    DependencyLink link =\n      DependencyLink.newBuilder().parent(\"foo\").child(\"bar\").callCount(2).errorCount(1).build();\n\n    assertThat(parse(JsonSerializers.DEPENDENCY_LINK_PARSER,\n      new String(DependencyLinkBytesEncoder.JSON_V1.encode(link), UTF_8))).isEqualTo(link);\n  }\n\n  static <T> T parse(JsonSerializers.ObjectParser<T> parser, String json) {\n    try {\n      JsonParser jsonParser = JsonSerializers.JSON_FACTORY.createParser(json);\n      jsonParser.nextToken();\n      return parser.parse(jsonParser);\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/OpensearchSpecificTemplatesTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.mock;\n\nclass OpensearchSpecificTemplatesTest {\n  static final OpensearchVersion V0_3 = new OpensearchVersion(0, 3);\n  static final OpensearchVersion V1_3 = new OpensearchVersion(1, 3);\n  static final OpensearchVersion V2_0 = new OpensearchVersion(2, 0);\n\n  ElasticsearchStorage storage =\n    ElasticsearchStorage.newBuilder(() -> mock(WebClient.class)).build();\n\n  /** Unsupported, but we should test that parsing works */\n  @Test void version_unsupported() {\n    assertThatThrownBy(() -> storage.versionSpecificTemplates(V0_3))\n      .hasMessage(\"OpenSearch versions 1-3.x are supported, was: 0.3\");\n  }\n\n  @Test void version2() {\n    IndexTemplates template = storage.versionSpecificTemplates(V2_0);\n\n    assertThat(template.version()).isEqualTo(V2_0);\n    assertThat(template.autocomplete())\n      .withFailMessage(\"Starting at v7.x, we delimit index and type with hyphen\")\n      .contains(\"\\\"index_patterns\\\": \\\"zipkin-autocomplete-*\\\"\");\n    assertThat(template.autocomplete())\n      .withFailMessage(\"7.x does not support the key index.mapper.dynamic\")\n      .doesNotContain(\"\\\"index.mapper.dynamic\\\": false\");\n  }\n\n  @Test void version2_doesntWrapPropertiesWithType() {\n    IndexTemplates template = storage.versionSpecificTemplates(V2_0);\n\n    assertThat(template.dependency()).contains(\"\"\"\n        \"mappings\": {\n          \"enabled\": false\n        }\\\n      \"\"\");\n\n    assertThat(template.autocomplete()).contains(\"\"\"\n        \"mappings\": {\n          \"enabled\": true,\n          \"properties\": {\n            \"tagKey\": { \"type\": \"keyword\", \"norms\": false },\n            \"tagValue\": { \"type\": \"keyword\", \"norms\": false }\n          }\n        }\\\n      \"\"\");\n  }\n\n  @Test void searchEnabled_minimalSpanIndexing_1x() {\n    storage = ElasticsearchStorage.newBuilder(() -> mock(WebClient.class))\n      .searchEnabled(false)\n      .build();\n\n    IndexTemplates template = storage.versionSpecificTemplates(V1_3);\n\n    // doesn't wrap in a type name\n    assertThat(template.span())\n      .contains(\"\"\"\n          \"mappings\": {\n            \"properties\": {\n              \"traceId\": { \"type\": \"keyword\", \"norms\": false },\n              \"annotations\": { \"enabled\": false },\n              \"tags\": { \"enabled\": false }\n            }\n          }\\\n        \"\"\");\n  }\n\n  @Test void strictTraceId_doesNotIncludeAnalysisSection() {\n    IndexTemplates template = storage.versionSpecificTemplates(V1_3);\n\n    assertThat(template.span()).doesNotContain(\"analysis\");\n  }\n\n  @Test void strictTraceId_false_includesAnalysisForMixedLengthTraceId() {\n    storage.close();\n    storage = ElasticsearchStorage.newBuilder(() -> mock(WebClient.class))\n      .strictTraceId(false)\n      .build();\n\n    IndexTemplates template = storage.versionSpecificTemplates(V1_3);\n\n    assertThat(template.span()).contains(\"analysis\");\n  }\n\n  @Test void indexTemplatesUrl_1x() {\n    assertThat(VersionSpecificTemplates.forVersion(V1_3).indexTemplatesUrl(\"idx\", \"_doc\", null))\n      .isEqualTo(\"/_template/idx_doc_template\");\n  }\n\n  @Test void indexTemplatesUrl_1x_withPriority() {\n    assertThat(VersionSpecificTemplates.forVersion(V1_3).indexTemplatesUrl(\"idx\", \"_doc\", 1))\n      .isEqualTo(\"/_index_template/idx_doc_template\");\n  }\n\n  @Test void indexTemplatesUrl_2x() {\n    assertThat(VersionSpecificTemplates.forVersion(V2_0).indexTemplatesUrl(\"idx\", \"_doc\", null))\n      .isEqualTo(\"/_template/idx_doc_template\");\n  }\n\n  @Test void indexTemplatesUrl_2x_withPriority() {\n    assertThat(VersionSpecificTemplates.forVersion(V2_0).indexTemplatesUrl(\"idx\", \"_doc\", 1))\n      .isEqualTo(\"/_index_template/idx_doc_template\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/OpensearchVersionTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.MediaType;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.elasticsearch.ElasticsearchStorageTest.RESPONSE_UNAUTHORIZED;\n\nclass OpensearchVersionTest {\n  static final OpensearchVersion V1_3 = new OpensearchVersion(1, 3);\n  static final OpensearchVersion V2_11 = new OpensearchVersion(2, 11);\n\n  static final AggregatedHttpResponse VERSION_RESPONSE_1 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"zipkin-elasticsearch\",\n        \"cluster_name\" : \"docker-cluster\",\n        \"cluster_uuid\" : \"wByRPgSgTryYl0TZXW4MsA\",\n        \"version\" : {\n          \"distribution\" : \"opensearch\",\n          \"number\" : \"1.3.14\",\n          \"build_type\" : \"tar\",\n          \"build_hash\" : \"21940d8239b50285ef7f98a1762ef281a5b1c7ee\",\n          \"build_date\" : \"2023-12-08T22:13:08.793451Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"8.10.1\",\n          \"minimum_wire_compatibility_version\" : \"6.8.0\",\n          \"minimum_index_compatibility_version\" : \"6.0.0-beta1\"\n        },\n        \"tagline\" : \"The OpenSearch Project: https://opensearch.org/\"\n      }\n      \"\"\");\n  static final AggregatedHttpResponse VERSION_RESPONSE_2 = AggregatedHttpResponse.of(\n    HttpStatus.OK, MediaType.JSON_UTF_8, \"\"\"\n      {\n        \"name\" : \"PV-NhJd\",\n        \"cluster_name\" : \"CollectorDBCluster\",\n        \"cluster_uuid\" : \"UjZaM0fQRC6tkHINCg9y8w\",\n         \"version\" : {\n          \"distribution\" : \"opensearch\",\n          \"number\" : \"2.11.1\",\n          \"build_type\" : \"tar\",\n          \"build_hash\" : \"6b1986e964d440be9137eba1413015c31c5a7752\",\n          \"build_date\" : \"2023-11-29T21:43:10.135035992Z\",\n          \"build_snapshot\" : false,\n          \"lucene_version\" : \"9.7.0\",\n          \"minimum_wire_compatibility_version\" : \"7.10.0\",\n          \"minimum_index_compatibility_version\" : \"7.0.0\"\n        },\n        \"tagline\" : \"The OpenSearch Project: https://opensearch.org/\"\n      }\n      \"\"\");\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  @BeforeEach void setUp() {\n    storage = ElasticsearchStorage.newBuilder(() -> WebClient.of(server.httpUri())).build();\n  }\n\n  @AfterEach void tearDown() {\n    storage.close();\n  }\n\n  ElasticsearchStorage storage;\n\n  @Test void wrongContent() {\n    server.enqueue(AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.OK),\n      HttpData.ofUtf8(\"you got mail\")));\n\n    assertThatThrownBy(() -> OpensearchVersion.get(storage.http()))\n      .hasMessage(\".version.number not found in response: you got mail\");\n  }\n\n  @Test void unauthorized() {\n    server.enqueue(RESPONSE_UNAUTHORIZED);\n\n    assertThatThrownBy(() -> ElasticsearchVersion.get(storage.http()))\n      .hasMessage(\"User: anonymous is not authorized to perform: es:ESHttpGet\");\n  }\n\n  @Test void version1() throws Exception {\n    server.enqueue(VERSION_RESPONSE_1);\n\n    assertThat(OpensearchVersion.get(storage.http()))\n      .isEqualTo(V1_3);\n  }\n\n  /** Prove we compare better than a float. A float of 2.10 is the same as 2.1! */\n  @Test void version2_10IsGreaterThan_V2_2() {\n    assertThat(new OpensearchVersion(2, 10))\n      .hasToString(\"2.10\")\n      .isGreaterThan(new OpensearchVersion(2, 2));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/SearchResultConverterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch; // to access package private stuff\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Annotation;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\nimport zipkin2.elasticsearch.internal.JsonSerializers;\nimport zipkin2.elasticsearch.internal.client.SearchResultConverter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.elasticsearch.TestResponses.SPANS;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.JSON_FACTORY;\n\nclass SearchResultConverterTest {\n  SearchResultConverter<Span> converter = SearchResultConverter.create(JsonSerializers.SPAN_PARSER);\n\n  @Test void convert() throws IOException {\n    // Our normal test data has recent timestamps to make testing the server and dependency linker\n    // work as there are values related to recency used in search defaults.\n    // This test needs stable timestamps because items like MD5 need to match.\n    long stableMicros = (TODAY - 1) * 1000L; // can't result in a zero value, so minimum ts of 1.\n    List<Span> stableTrace = TestObjects.TRACE.stream()\n      .map(s -> {\n        Span.Builder builder = s.toBuilder().timestamp(s.timestampAsLong() - stableMicros)\n          .clearAnnotations();\n        for (Annotation a : s.annotations()) {\n          builder.addAnnotation(a.timestamp() - stableMicros, a.value());\n        }\n        return builder.build();\n      }).collect(Collectors.toList());\n    assertThat(converter.convert(JSON_FACTORY.createParser(SPANS), Assertions::fail))\n      .containsExactlyElementsOf(stableTrace);\n  }\n\n  @Test void convert_noHits() throws IOException {\n    assertThat(converter.convert(JSON_FACTORY.createParser(\"{}\"), Assertions::fail))\n      .isEmpty();\n  }\n\n  @Test void convert_onlyOneLevelHits() throws IOException {\n    assertThat(converter.convert(JSON_FACTORY.createParser(\"{\\\"hits\\\":{}}\"), Assertions::fail))\n      .isEmpty();\n  }\n\n  @Test void convert_hitsHitsButEmpty() throws IOException {\n    assertThat(\n      converter.convert(JSON_FACTORY.createParser(\"{\\\"hits\\\":{\\\"hits\\\":[]}}\"), Assertions::fail))\n      .isEmpty();\n  }\n\n  @Test void convert_hitsHitsButNoSource() throws IOException {\n    assertThat(\n      converter.convert(JSON_FACTORY.createParser(\"{\\\"hits\\\":{\\\"hits\\\":[{}]}}\"), Assertions::fail))\n      .isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/TestResponses.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch;\n\nfinal class TestResponses {\n  static final String SPANS = \"\"\"\n    {\n      \"took\": 4,\n      \"timed_out\": false,\n      \"_shards\": {\n        \"total\": 5,\n        \"successful\": 5,\n        \"skipped\": 0,\n        \"failed\": 0\n      },\n      \"hits\": {\n        \"total\": 4,\n        \"max_score\": 0,\n        \"hits\": [\n          {\n            \"_index\": \"zipkin:span-2019-07-20\",\n            \"_type\": \"span\",\n            \"_id\": \"7180c278b62e8f6a216a2aea45d08fc9-2a40476ca7a22f2c85ac18b9c1f3a99c\",\n            \"_score\": 0,\n            \"_source\": {\n              \"traceId\": \"7180c278b62e8f6a216a2aea45d08fc9\",\n              \"duration\": 350000,\n              \"localEndpoint\": {\n                \"serviceName\": \"frontend\",\n                \"ipv4\": \"127.0.0.1\"\n              },\n              \"timestamp_millis\": 1,\n              \"kind\": \"SERVER\",\n              \"name\": \"get\",\n              \"id\": \"0000000000000001\",\n              \"timestamp\": 1000\n            }\n          },\n          {\n            \"_index\": \"zipkin:span-2019-07-20\",\n            \"_type\": \"span\",\n            \"_id\": \"7180c278b62e8f6a216a2aea45d08fc9-466fed1eb1d5cef4a76a227e83a7a7a8\",\n            \"_score\": 0,\n            \"_source\": {\n              \"traceId\": \"7180c278b62e8f6a216a2aea45d08fc9\",\n              \"duration\": 200000,\n              \"remoteEndpoint\": {\n                \"serviceName\": \"backend\",\n                \"ipv4\": \"192.168.99.101\",\n                \"port\": 9000\n              },\n              \"localEndpoint\": {\n                \"serviceName\": \"frontend\",\n                \"ipv4\": \"127.0.0.1\"\n              },\n              \"timestamp_millis\": 51,\n              \"kind\": \"CLIENT\",\n              \"name\": \"get\",\n              \"annotations\": [\n                {\n                  \"timestamp\": 101000,\n                  \"value\": \"foo\"\n                }\n              ],\n              \"id\": \"0000000000000002\",\n              \"parentId\": \"0000000000000001\",\n              \"timestamp\": 51000,\n              \"tags\": {\n                \"clnt/finagle.version\": \"6.45.0\",\n                \"http.path\": \"/api\"\n              }\n            }\n          },\n          {\n            \"_index\": \"zipkin:span-2019-07-20\",\n            \"_type\": \"span\",\n            \"_id\": \"7180c278b62e8f6a216a2aea45d08fc9-74d915e86c8f53d59ef5850b4e966199\",\n            \"_score\": 0,\n            \"_source\": {\n              \"traceId\": \"7180c278b62e8f6a216a2aea45d08fc9\",\n              \"duration\": 150000,\n              \"shared\": true,\n              \"localEndpoint\": {\n                \"serviceName\": \"backend\",\n                \"ipv4\": \"192.168.99.101\",\n                \"port\": 9000\n              },\n              \"timestamp_millis\": 101,\n              \"kind\": \"SERVER\",\n              \"name\": \"get\",\n              \"id\": \"0000000000000002\",\n              \"parentId\": \"0000000000000001\",\n              \"timestamp\": 101000\n            }\n          },\n          {\n            \"_index\": \"zipkin:span-2019-07-20\",\n            \"_type\": \"span\",\n            \"_id\": \"7180c278b62e8f6a216a2aea45d08fc9-989c12147ff4ca03ce10d8488d93b89d\",\n            \"_score\": 0,\n            \"_source\": {\n              \"traceId\": \"7180c278b62e8f6a216a2aea45d08fc9\",\n              \"duration\": 50000,\n              \"remoteEndpoint\": {\n                \"serviceName\": \"db\",\n                \"ipv6\": \"2001:db8::c001\",\n                \"port\": 3036\n              },\n              \"localEndpoint\": {\n                \"serviceName\": \"backend\",\n                \"ipv4\": \"192.168.99.101\",\n                \"port\": 9000\n              },\n              \"timestamp_millis\": 151,\n              \"kind\": \"CLIENT\",\n              \"name\": \"query\",\n              \"annotations\": [\n                {\n                  \"timestamp\": 191000,\n                  \"value\": \"⻩\"\n                }\n              ],\n              \"id\": \"0000000000000003\",\n              \"parentId\": \"0000000000000002\",\n              \"timestamp\": 151000,\n              \"tags\": {\n                  \"error\": \"💩\"\n              }\n            }\n          }\n        ]\n      }\n    }\n    \"\"\";\n  static final String SERVICE_NAMES =\n    \"\"\"\n    {\n      \"took\": 4,\n      \"timed_out\": false,\n      \"_shards\": {\n        \"total\": 5,\n        \"successful\": 5,\n        \"failed\": 0\n      },\n      \"hits\": {\n        \"total\": 1,\n        \"max_score\": 0,\n        \"hits\": []\n      },\n      \"aggregations\": {\n        \"binaryAnnotations_agg\": {\n          \"doc_count\": 1,\n          \"binaryAnnotationsServiceName_agg\": {\n            \"doc_count_error_upper_bound\": 0,\n            \"sum_other_doc_count\": 0,\n            \"buckets\": [\n              {\n                \"key\": \"yak\",\n                \"doc_count\": 1\n              }\n            ]\n          }\n        },\n        \"annotations_agg\": {\n          \"doc_count\": 2,\n          \"annotationsServiceName_agg\": {\n            \"doc_count_error_upper_bound\": 0,\n            \"sum_other_doc_count\": 0,\n            \"buckets\": [\n              {\n                \"key\": \"service\",\n                \"doc_count\": 2\n              }\n            ]\n          }\n        }\n      }\n    }\n    \"\"\";\n\n  static final String SPAN_NAMES =\n    \"\"\"\n    {\n      \"took\": 1,\n      \"timed_out\": false,\n      \"_shards\": {\n        \"total\": 5,\n        \"successful\": 5,\n        \"failed\": 0\n      },\n      \"hits\": {\n        \"total\": 2,\n        \"max_score\": 0,\n        \"hits\": []\n      },\n      \"aggregations\": {\n        \"name_agg\": {\n          \"doc_count_error_upper_bound\": 0,\n          \"sum_other_doc_count\": 0,\n          \"buckets\": [\n            {\n              \"key\": \"methodcall\",\n              \"doc_count\": 1\n            },\n            {\n              \"key\": \"yak\",\n              \"doc_count\": 1\n            }\n          ]\n        }\n      }\n    }\n    \"\"\";\n\n  static final String AUTOCOMPLETE_VALUES = \"\"\"\n    {\n      \"took\": 12,\n      \"timed_out\": false,\n      \"_shards\": {\n        \"total\": 5,\n        \"successful\": 5,\n        \"skipped\": 0,\n        \"failed\": 0\n      },\n      \"hits\": {\n        \"total\": 2,\n        \"max_score\": 0,\n        \"hits\": [\n          {\n            \"_index\": \"zipkin:autocomplete-2018-12-08\",\n            \"_type\": \"autocomplete\",\n            \"_id\": \"http.method|POST\",\n            \"_score\": 0\n          },\n          {\n            \"_index\": \"zipkin:autocomplete-2018-12-08\",\n            \"_type\": \"autocomplete\",\n            \"_id\": \"http.method|GET\",\n            \"_score\": 0\n          }\n        ]\n      },\n      \"aggregations\": {\n        \"tagValue\": {\n          \"doc_count_error_upper_bound\": 0,\n          \"sum_other_doc_count\": 0,\n          \"buckets\": [\n            {\n              \"key\": \"get\",\n              \"doc_count\": 1\n            },\n            {\n              \"key\": \"post\",\n              \"doc_count\": 1\n            }\n          ]\n        }\n      }\n    }\n    \"\"\";\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ElasticsearchBaseExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport com.linecorp.armeria.client.ClientFactory;\nimport com.linecorp.armeria.client.ClientOptions;\nimport com.linecorp.armeria.client.ClientOptionsBuilder;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.WebClientBuilder;\nimport com.linecorp.armeria.client.logging.ContentPreviewingClient;\nimport com.linecorp.armeria.client.logging.LoggingClient;\nimport com.linecorp.armeria.client.logging.LoggingClientBuilder;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.logging.LogLevel;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.ElasticsearchStorage.Builder;\n\nimport static zipkin2.elasticsearch.integration.IgnoredDeprecationWarnings.IGNORE_THESE_WARNINGS;\n\nclass ElasticsearchBaseExtension implements BeforeAllCallback, AfterAllCallback {\n  static final Logger LOGGER = LoggerFactory.getLogger(ElasticsearchBaseExtension.class);\n\n  final GenericContainer<?> container;\n\n  ElasticsearchBaseExtension(GenericContainer container) {\n    this.container = container;\n  }\n\n  @Override public void beforeAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using baseUrl {}\", baseUrl());\n  }\n\n  @Override public void afterAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.stop();\n  }\n\n  Builder computeStorageBuilder() {\n    WebClientBuilder builder = WebClient.builder(baseUrl())\n      // Elasticsearch 7 never returns a response when receiving an HTTP/2 preface instead of the\n      // more valid behavior of returning a bad request response, so we can't use the preface.\n      //\n      // TODO: find or raise a bug with Elastic\n      .factory(ClientFactory.builder().useHttp2Preface(false).build());\n    builder.decorator((delegate, ctx, req) -> {\n      final HttpResponse response = delegate.execute(ctx, req);\n      return HttpResponse.from(response.aggregate().thenApply(r -> {\n        // ES will return a 'warning' response header when using deprecated api, detect this and\n        // fail early so we can do something about it.\n        // Example usage: https://github.com/elastic/elasticsearch/blob/3049e55f093487bb582a7e49ad624961415ba31c/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/IndexPrivilegeIntegTests.java#L559\n        final String warningHeader = r.headers().get(\"warning\");\n        if (warningHeader != null) {\n          if (IGNORE_THESE_WARNINGS.stream().noneMatch(p -> p.matcher(warningHeader).find())) {\n            throw new IllegalArgumentException(\"Detected usage of deprecated API for request \"\n              + req + \":\\n\" + warningHeader);\n          }\n        }\n        // Convert AggregatedHttpResponse back to HttpResponse.\n        return r.toHttpResponse();\n      }));\n    });\n\n    // When ES_DEBUG=true log full headers, request and response body to the category\n    // com.linecorp.armeria.client.logging\n    if (Boolean.parseBoolean(System.getenv(\"ES_DEBUG\"))) {\n      ClientOptionsBuilder options = ClientOptions.builder();\n      LoggingClientBuilder loggingBuilder = LoggingClient.builder()\n        .requestLogLevel(LogLevel.INFO)\n        .successfulResponseLogLevel(LogLevel.INFO);\n      options.decorator(loggingBuilder.newDecorator());\n      options.decorator(ContentPreviewingClient.newDecorator(Integer.MAX_VALUE));\n      builder.options(options.build());\n    }\n\n    WebClient client = builder.build();\n    return ElasticsearchStorage.newBuilder(new ElasticsearchStorage.LazyHttpClient() {\n      @Override public WebClient get() {\n        return client;\n      }\n\n      @Override public void close() {\n        client.endpointGroup().close();\n      }\n\n      @Override public String toString() {\n        return client.uri().toString();\n      }\n    }).index(\"zipkin-test\").flushOnWrites(true);\n  }\n\n  String baseUrl() {\n    return \"http://\" + container.getHost() + \":\" + container.getMappedPort(9200);\n  }\n\n  static String index(TestInfo testInfo) {\n    String result;\n    if (testInfo.getTestMethod().isPresent()) {\n      result = testInfo.getTestMethod().get().getName();\n    } else {\n      assert testInfo.getTestClass().isPresent();\n      result = testInfo.getTestClass().get().getSimpleName();\n    }\n    result = result.toLowerCase();\n    return result.length() <= 48 ? result : result.substring(result.length() - 48);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ElasticsearchExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\nclass ElasticsearchExtension extends ElasticsearchBaseExtension {\n  static final Logger LOGGER = LoggerFactory.getLogger(ElasticsearchExtension.class);\n\n  ElasticsearchExtension(int majorVersion) {\n    super(new ElasticsearchContainer(majorVersion));\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class ElasticsearchContainer extends GenericContainer<ElasticsearchContainer> {\n    ElasticsearchContainer(int majorVersion) {\n      super(parse(\"ghcr.io/openzipkin/zipkin-elasticsearch\" + majorVersion + \":3.4.3\"));\n      addExposedPort(9200);\n      waitStrategy = Wait.forHealthcheck();\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ITElasticsearchStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport com.linecorp.armeria.client.ClientFactory;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport java.io.IOException;\nimport java.util.List;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport zipkin2.Span;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.InternalForTests;\nimport zipkin2.storage.StorageComponent;\n\nimport static zipkin2.elasticsearch.integration.ElasticsearchExtension.index;\nimport static zipkin2.storage.ITDependencies.aggregateLinks;\n\nabstract class ITElasticsearchStorage {\n\n  static final Logger LOGGER = LoggerFactory.getLogger(ITElasticsearchStorage.class);\n\n  abstract ElasticsearchBaseExtension elasticsearch();\n\n  @Nested\n  class ITTraces extends zipkin2.storage.ITTraces<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStore extends zipkin2.storage.ITSpanStore<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStoreHeavy extends zipkin2.storage.ITSpanStoreHeavy<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSearchEnabledFalse extends zipkin2.storage.ITSearchEnabledFalse<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITServiceAndSpanNames extends zipkin2.storage.ITServiceAndSpanNames<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITAutocompleteTags extends zipkin2.storage.ITAutocompleteTags<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITStrictTraceIdFalse extends zipkin2.storage.ITStrictTraceIdFalse<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependencies extends zipkin2.storage.ITDependencies<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override protected void processDependencies(List<Span> spans) {\n      aggregateDependencies(storage, spans);\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependenciesHeavy extends zipkin2.storage.ITDependenciesHeavy<ElasticsearchStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n\n    @Override protected void processDependencies(List<Span> spans) {\n      aggregateDependencies(storage, spans);\n    }\n\n    @Override public void clear() throws IOException {\n      storage.clear();\n    }\n  }\n\n  /**\n   * The current implementation does not include dependency aggregation. It includes retrieval of\n   * pre-aggregated links, usually made via zipkin-dependencies\n   */\n  static void aggregateDependencies(ElasticsearchStorage storage, List<Span> spans) {\n    aggregateLinks(spans).forEach(\n      (midnight, links) -> InternalForTests.writeDependencyLinks(\n        storage, links, midnight));\n  }\n\n  @Test void usageOfDeprecatedFeatures() {\n    WebClient webClient = WebClient.builder(elasticsearch().baseUrl()).factory(ClientFactory.builder()\n      .useHttp2Preface(false).build()).build();\n    final AggregatedHttpResponse response =\n      webClient.execute(HttpRequest.of(RequestHeaders.of(HttpMethod.GET,\n        \"/_migration/deprecations\"))).aggregate().join();\n    String responseBody = response.contentAscii();\n    if (!responseBody.equals(\"\"\"\n      {\"cluster_settings\":[],\"node_settings\":[],\"index_settings\":{},\"ml_settings\":[],\"ccr_auto_followed_system_indices\":[]}\"\"\")) {\n      LOGGER.warn(\"The ElasticSearch instance used during IT's is using deprecated features or \"\n        + \"configuration. This is likely nothing to be really worried about (for example 'xpack.monitoring.enabled' \"\n        + \"setting), but nevertheless it should be looked at to see if our docker image used during \"\n        + \"integration tests needs updating for the next version of ElasticSearch. \"\n        + \"See https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-deprecation.html \"\n        + \"for more information. This is the deprecation warning we received:\\n\\n\"\n        + response.contentAscii());\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ITElasticsearchStorageV7.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static zipkin2.elasticsearch.integration.ElasticsearchExtension.index;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Tag(\"docker\")\nclass ITElasticsearchStorageV7 extends ITElasticsearchStorage {\n\n  @RegisterExtension static ElasticsearchExtension elasticsearch = new ElasticsearchExtension(7);\n\n  @Override ElasticsearchExtension elasticsearch() {\n    return elasticsearch;\n  }\n\n  @Nested\n  class ITEnsureIndexTemplate extends zipkin2.elasticsearch.integration.ITEnsureIndexTemplate {\n    @Override protected ElasticsearchStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ITElasticsearchStorageV8.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static zipkin2.elasticsearch.integration.ElasticsearchExtension.index;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Tag(\"docker\")\nclass ITElasticsearchStorageV8 extends ITElasticsearchStorage {\n\n  @RegisterExtension static ElasticsearchExtension elasticsearch = new ElasticsearchExtension(8);\n\n  @Override ElasticsearchExtension elasticsearch() {\n    return elasticsearch;\n  }\n\n  @Nested\n  class ITEnsureIndexTemplate extends zipkin2.elasticsearch.integration.ITEnsureIndexTemplate {\n    @Override protected ElasticsearchStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ITEnsureIndexTemplate.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport java.io.IOException;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport zipkin2.Span;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\nimport zipkin2.elasticsearch.internal.Internal;\nimport zipkin2.storage.ITStorage;\nimport zipkin2.storage.StorageComponent;\n\nimport static com.linecorp.armeria.common.HttpHeaderNames.CONTENT_TYPE;\nimport static com.linecorp.armeria.common.HttpMethod.DELETE;\nimport static com.linecorp.armeria.common.HttpMethod.GET;\nimport static com.linecorp.armeria.common.HttpMethod.PUT;\nimport static com.linecorp.armeria.common.MediaType.JSON_UTF_8;\nimport static zipkin2.TestObjects.spanBuilder;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\nabstract class ITEnsureIndexTemplate extends ITStorage<ElasticsearchStorage> {\n  @Override protected abstract ElasticsearchStorage.Builder newStorageBuilder(TestInfo testInfo);\n\n  @Override protected void configureStorageForTest(StorageComponent.Builder storage) {\n  }\n\n  @Override protected boolean initializeStoragePerTest() {\n    return true; // We need a different index pattern per test\n  }\n\n  @Override protected void clear() throws Exception {\n    storage.clear();\n  }\n\n  @Test void createZipkinIndexTemplate_getTraces_returnsSuccess(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    storage = newStorageBuilder(testInfo).templatePriority(10).build();\n    try {\n      // Delete all templates in order to create the \"catch-all\" index template, because\n      // ES does not allow multiple index templates of the same index_patterns and priority\n      http(DELETE, \"/_template/*\");\n      setUpCatchAllTemplate();\n\n      // Implicitly creates an index template\n      checkStorage();\n\n      // Get all templates. We don't assert on this at the moment. This is for logging on ES_DEBUG.\n      http(GET, \"/_template\");\n\n      // Now, add a span, which should be indexed differently than default.\n      Span span = spanBuilder(testSuffix).putTag(\"queryTest\", \"ok\").build();\n      accept(List.of(span));\n\n      // Assert that Zipkin's templates work and source is returned\n      assertGetTracesReturns(\n        requestBuilder()\n          .parseAnnotationQuery(\"queryTest=\" + span.tags().get(\"queryTest\"))\n          .build(),\n        List.of(span));\n    } finally {\n      // Delete \"catch-all\" index template, so it does not interfere with any other test\n      http(DELETE, catchAllIndexPath());\n    }\n  }\n\n  /**\n   * Create a \"catch-all\" index template with the lowest priority prior to running tests to ensure\n   * that the index templates created during tests with higher priority function as designed. Only\n   * applicable for ES >= 7.8\n   */\n  void setUpCatchAllTemplate() throws IOException {\n    AggregatedHttpRequest updateTemplate = AggregatedHttpRequest.of(\n      RequestHeaders.of(PUT, catchAllIndexPath(), CONTENT_TYPE, JSON_UTF_8),\n      HttpData.ofUtf8(catchAllTemplate()));\n    Internal.instance.http(storage).newCall(updateTemplate, (parser, contentString) -> null,\n      \"update-template\").execute();\n  }\n\n  String catchAllIndexPath() {\n    return \"/_index_template/catch-all\";\n  }\n\n  /** Catch-all template doesn't store source */\n  String catchAllTemplate() {\n    return \"\"\"\n      {\n        \"index_patterns\" : [\"*\"],\n        \"priority\" : 5,\n        \"template\": {\n          \"settings\" : {\n            \"number_of_shards\" : 1\n          },\n          \"mappings\" : {\n            \"_source\": {\"enabled\": false }\n          }\n        }\n      }\\\n      \"\"\";\n  }\n\n  void http(HttpMethod method, String path) throws IOException {\n    AggregatedHttpRequest delete = AggregatedHttpRequest.of(method, path);\n    Internal.instance.http(storage)\n      .newCall(delete, (parser, contentString) -> null, method + \"-\" + path).execute();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/ITOpenSearchStorageV2.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.elasticsearch.ElasticsearchStorage;\n\nimport static zipkin2.elasticsearch.integration.ElasticsearchExtension.index;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Tag(\"docker\")\nclass ITOpenSearchStorageV2 extends ITElasticsearchStorage {\n\n  @RegisterExtension static OpenSearchExtension opensearch = new OpenSearchExtension(2);\n\n  @Override OpenSearchExtension elasticsearch() {\n    return opensearch;\n  }\n\n  @Nested\n  class ITEnsureIndexTemplate extends zipkin2.elasticsearch.integration.ITEnsureIndexTemplate {\n    @Override protected ElasticsearchStorage.Builder newStorageBuilder(TestInfo testInfo) {\n      return elasticsearch().computeStorageBuilder().index(index(testInfo));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/IgnoredDeprecationWarnings.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport java.util.List;\nimport java.util.regex.Pattern;\n\nimport static java.util.Arrays.asList;\nimport static java.util.regex.Pattern.compile;\n\n/**\n * When ES emits a deprecation warning header in response to a method being called, the integration\n * test will fail. We cannot always fix our code however to take into account all deprecation\n * warnings, as we have to support multiple versions of ES. For these cases, add the warning message\n * to {@link #IGNORE_THESE_WARNINGS} array so it will not raise an exception anymore.\n */\nabstract class IgnoredDeprecationWarnings {\n\n  // These will be matched using header.contains(ignored[i]), so find a unique substring of the\n  // warning header for it to be ignored\n  static List<Pattern> IGNORE_THESE_WARNINGS = asList(\n    // Basic license doesn't include x-pack.\n    // https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html#_enable_elasticsearch_security_features\n    compile(\"Elasticsearch built-in security features are not enabled.\"),\n    compile(\"Elasticsearch 7\\\\.x will read, but not allow creation of new indices containing ':'\"),\n    compile(\"has index patterns \\\\[.*] matching patterns from existing older templates\"),\n    compile(\"has index patterns \\\\[.*] matching patterns from existing composable templates\")\n  );\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/integration/OpenSearchExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.integration;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\nimport static org.testcontainers.utility.DockerImageName.parse;\n\nclass OpenSearchExtension extends ElasticsearchBaseExtension {\n  static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchExtension.class);\n\n  OpenSearchExtension(int majorVersion) {\n    super(new OpenSearchContainer(majorVersion));\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class OpenSearchContainer extends GenericContainer<OpenSearchContainer> {\n      OpenSearchContainer(int majorVersion) {\n      super(parse(\"ghcr.io/openzipkin/zipkin-opensearch\" + majorVersion + \":3.4.3\"));\n      addExposedPort(9200);\n      waitStrategy = Wait.forHealthcheck();\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkCallBuilderTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport java.util.concurrent.RejectedExecutionException;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.elasticsearch.internal.BulkCallBuilder.CHECK_FOR_ERRORS;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.JSON_FACTORY;\n\nclass BulkCallBuilderTest {\n  @Test void throwsRejectedExecutionExceptionWhenOverCapacity() {\n    String response =\n      \"{\\\"took\\\":0,\\\"errors\\\":true,\\\"items\\\":[{\\\"index\\\":{\\\"_index\\\":\\\"dev-zipkin:span-2019.04.18\\\",\\\"_type\\\":\\\"span\\\",\\\"_id\\\":\\\"2511\\\",\\\"status\\\":429,\\\"error\\\":{\\\"type\\\":\\\"es_rejected_execution_exception\\\",\\\"reason\\\":\\\"rejected execution of org.elasticsearch.transport.TransportService$7@7ec1ea93 on EsThreadPoolExecutor[bulk, queue capacity = 200, org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor@621571ba[Running, pool size = 4, active threads = 4, queued tasks = 200, completed tasks = 3838534]]\\\"}}}]}\";\n\n    assertThatThrownBy(\n      () -> CHECK_FOR_ERRORS.convert(JSON_FACTORY.createParser(response), () -> response))\n      .isInstanceOf(RejectedExecutionException.class)\n      .hasMessage(\n        \"rejected execution of org.elasticsearch.transport.TransportService$7@7ec1ea93 on EsThreadPoolExecutor[bulk, queue capacity = 200, org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor@621571ba[Running, pool size = 4, active threads = 4, queued tasks = 200, completed tasks = 3838534]]\");\n  }\n\n  @Test void throwsRuntimeExceptionAsRootCauseReasonWhenPresent() {\n    String response = \"\"\"\n      {\n        \"error\": {\n          \"root_cause\": [\n            {\n              \"type\": \"illegal_argument_exception\",\n              \"reason\": \"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\"\n            }\n          ],\n          \"type\": \"search_phase_execution_exception\",\n          \"reason\": \"all shards failed\",\n          \"phase\": \"query\",\n          \"grouped\": true,\n          \"failed_shards\": [\n            {\n              \"shard\": 0,\n              \"index\": \"zipkin-2017-05-14\",\n              \"node\": \"IqceAwZnSvyv0V0xALkEnQ\",\n              \"reason\": {\n                \"type\": \"illegal_argument_exception\",\n                \"reason\": \"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\"\n              }\n            }\n          ]\n        },\n        \"status\": 400\n      }\n      \"\"\";\n\n    assertThatThrownBy(\n      () -> CHECK_FOR_ERRORS.convert(JSON_FACTORY.createParser(response), () -> response))\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\");\n  }\n\n  /** Tests lack of a root cause won't crash */\n  @Test void throwsRuntimeExceptionAsReasonWhenPresent() {\n    String response = \"\"\"\n      {\n        \"error\": {\n          \"type\": \"search_phase_execution_exception\",\n          \"reason\": \"all shards failed\",\n          \"phase\": \"query\"\n        },\n        \"status\": 400\n      }\n      \"\"\";\n\n    assertThatThrownBy(\n      () -> CHECK_FOR_ERRORS.convert(JSON_FACTORY.createParser(response), () -> response))\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\"all shards failed\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkIndexWriterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport io.netty.buffer.ByteBufOutputStream;\nimport io.netty.buffer.ByteBufUtil;\nimport io.netty.buffer.Unpooled;\nimport java.nio.charset.StandardCharsets;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\nimport zipkin2.codec.SpanBytesDecoder;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.CLIENT_SPAN;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\n\nclass BulkIndexWriterTest {\n\n  // Our usual test span depends on currentTime for testing span stores with TTL, but we'd prefer\n  // to have a fixed span here to avoid depending on business logic in test assertions.\n  static final Span STABLE_SPAN = CLIENT_SPAN.toBuilder()\n    .timestamp(100)\n    .clearAnnotations()\n    .build();\n\n  ByteBufOutputStream buffer;\n\n  @BeforeEach void setUp() {\n    buffer = new ByteBufOutputStream(Unpooled.buffer());\n  }\n\n  @Test void span_addsDocumentId() throws Exception {\n    String id = BulkIndexWriter.SPAN.writeDocument(STABLE_SPAN, buffer);\n\n    assertThat(id)\n      .isEqualTo(\"7180c278b62e8f6a216a2aea45d08fc9-198140c2a26bfa58fed4a572dfe3d63b\");\n  }\n\n  @Test void spanSearchDisabled_addsDocumentId() throws Exception {\n    String id = BulkIndexWriter.SPAN_SEARCH_DISABLED.writeDocument(STABLE_SPAN, buffer);\n\n    assertThat(id)\n      .isEqualTo(\"7180c278b62e8f6a216a2aea45d08fc9-bfe7a3c0d9ee83b1d218bd0f383f006a\");\n  }\n\n  @Test void spanSearchFields_skipsWhenNoData() {\n    Span span = Span.newBuilder()\n      .traceId(\"20\")\n      .id(\"22\")\n      .parentId(\"21\")\n      .timestamp(0L)\n      .localEndpoint(FRONTEND)\n      .kind(Kind.CLIENT)\n      .build();\n\n    BulkIndexWriter.SPAN.writeDocument(span, buffer);\n\n    assertThat(buffer.buffer().toString(StandardCharsets.UTF_8)).startsWith(\"{\\\"traceId\\\":\\\"\");\n  }\n\n  @Test void spanSearchFields_addsTimestampFieldWhenNoTags() {\n    Span span =\n      Span.newBuilder()\n        .traceId(\"20\")\n        .id(\"22\")\n        .name(\"\")\n        .parentId(\"21\")\n        .timestamp(1000L)\n        .localEndpoint(FRONTEND)\n        .kind(Kind.CLIENT)\n        .build();\n\n    BulkIndexWriter.SPAN.writeDocument(span, buffer);\n\n    assertThat(buffer.buffer().toString(StandardCharsets.UTF_8))\n      .startsWith(\"{\\\"timestamp_millis\\\":1,\\\"traceId\\\":\");\n  }\n\n  @Test void spanSearchFields_addsQueryFieldForAnnotations() {\n    Span span = Span.newBuilder()\n      .traceId(\"20\")\n      .id(\"22\")\n      .name(\"\")\n      .parentId(\"21\")\n      .localEndpoint(FRONTEND)\n      .addAnnotation(1L, \"\\\"foo\")\n      .build();\n\n    BulkIndexWriter.SPAN.writeDocument(span, buffer);\n\n    assertThat(buffer.buffer().toString(StandardCharsets.UTF_8))\n      .startsWith(\"{\\\"_q\\\":[\\\"\\\\\\\"foo\\\"],\\\"traceId\");\n  }\n\n  @Test void spanSearchFields_addsQueryFieldForTags() {\n    Span span = Span.newBuilder()\n      .traceId(\"20\")\n      .id(\"22\")\n      .parentId(\"21\")\n      .localEndpoint(FRONTEND)\n      .putTag(\"\\\"foo\", \"\\\"bar\")\n      .build();\n\n    BulkIndexWriter.SPAN.writeDocument(span, buffer);\n\n    assertThat(buffer.buffer().toString(StandardCharsets.UTF_8))\n      .startsWith(\"{\\\"_q\\\":[\\\"\\\\\\\"foo\\\",\\\"\\\\\\\"foo=\\\\\\\"bar\\\"],\\\"traceId\");\n  }\n\n  @Test void spanSearchFields_readableByNormalJsonCodec() {\n    Span span =\n      Span.newBuilder().traceId(\"20\").id(\"20\").name(\"get\").timestamp(TODAY * 1000).build();\n\n    BulkIndexWriter.SPAN.writeDocument(span, buffer);\n\n    assertThat(SpanBytesDecoder.JSON_V2.decodeOne(ByteBufUtil.getBytes(buffer.buffer())))\n      .isEqualTo(span); // ignores timestamp_millis field\n  }\n\n  @Test void spanSearchDisabled_doesntAddQueryFields() {\n    BulkIndexWriter.SPAN_SEARCH_DISABLED.writeDocument(CLIENT_SPAN, buffer);\n\n    assertThat(buffer.buffer().toString(StandardCharsets.UTF_8))\n      .startsWith(\"{\\\"traceId\\\":\\\"\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/IndexNameFormatterTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal;\n\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.TimeZone;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\npublic class IndexNameFormatterTest {\n  IndexNameFormatter formatter =\n      IndexNameFormatter.newBuilder().index(\"zipkin\").dateSeparator('-').build();\n  DateFormat iso8601 = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n\n  public IndexNameFormatterTest() {\n    iso8601.setTimeZone(TimeZone.getTimeZone(\"UTC\"));\n  }\n\n  @Test void indexNameForTimestampRange_sameTime() throws ParseException {\n    long start = iso8601.parse(\"2016-11-01T01:01:01Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, start))\n      .containsExactly(\"zipkin*span-2016-11-01\");\n  }\n\n  @Test void indexNameForTimestampRange_sameDay() throws ParseException {\n    long start = iso8601.parse(\"2016-11-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016-11-01\");\n  }\n\n  @Test void indexNameForTimestampRange_sameMonth() throws ParseException {\n    long start = iso8601.parse(\"2016-11-15T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-16T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016-11-15\", \"zipkin*span-2016-11-16\");\n  }\n\n  @Test void indexNameForTimestampRange_sameMonth_startingAtOne() throws ParseException {\n    long start = iso8601.parse(\"2016-11-1T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-3T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016-11-01\", \"zipkin*span-2016-11-02\", \"zipkin*span-2016-11-03\");\n  }\n\n  @Test void indexNameForTimestampRange_nextMonth() throws ParseException {\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016-10-31\", \"zipkin*span-2016-11-01\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesMonth() throws ParseException {\n    long start = iso8601.parse(\"2016-10-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-10-31T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016-10-*\");\n  }\n\n  @Test void indexNameForTimestampRange_skipMonths() throws ParseException {\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-12-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016-10-31\", \"zipkin*span-2016-11-*\", \"zipkin*span-2016-12-01\");\n  }\n\n  @Test void indexNameForTimestampRange_skipMonths_leapYear() throws ParseException {\n    long start = iso8601.parse(\"2016-02-28T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-04-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016-02-28\",\n            \"zipkin*span-2016-02-29\",\n            \"zipkin*span-2016-03-*\",\n            \"zipkin*span-2016-04-01\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesYear() throws ParseException {\n    long start = iso8601.parse(\"2016-01-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-12-31T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016-*\");\n  }\n\n  @Test void indexNameForTimestampRange_skipYears() throws ParseException {\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2018-01-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016-10-31\",\n            \"zipkin*span-2016-11-*\",\n            \"zipkin*span-2016-12-*\",\n            \"zipkin*span-2017-*\",\n            \"zipkin*span-2018-01-01\");\n  }\n\n  @Test void indexNameForTimestampRange_other_sameDay() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-11-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016.11.01\");\n  }\n\n  @Test void indexNameForTimestampRange_other_sameMonth() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-11-15T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-16T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016.11.15\", \"zipkin*span-2016.11.16\");\n  }\n\n  @Test void indexNameForTimestampRange_sameMonth_other_startingAtOne() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-11-1T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-3T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016.11.01\", \"zipkin*span-2016.11.02\", \"zipkin*span-2016.11.03\");\n  }\n\n  @Test void indexNameForTimestampRange_other_nextMonth() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-11-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016.10.31\", \"zipkin*span-2016.11.01\");\n  }\n\n  @Test void indexNameForTimestampRange_other_compressesMonth() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-10-31T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016.10.*\");\n  }\n\n  @Test void indexNameForTimestampRange_other_skipMonths() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-12-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016.10.31\", \"zipkin*span-2016.11.*\", \"zipkin*span-2016.12.01\");\n  }\n\n  @Test void indexNameForTimestampRange_skipMonths_other_leapYear() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-02-28T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-04-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016.02.28\",\n            \"zipkin*span-2016.02.29\",\n            \"zipkin*span-2016.03.*\",\n            \"zipkin*span-2016.04.01\");\n  }\n\n  @Test void indexNameForTimestampRange_other_compressesYear() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-01-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-12-31T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\"zipkin*span-2016.*\");\n  }\n\n  @Test void indexNameForTimestampRange_other_skipYears() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-31T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2018-01-01T23:59:59Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n        .containsExactly(\n            \"zipkin*span-2016.10.31\",\n            \"zipkin*span-2016.11.*\",\n            \"zipkin*span-2016.12.*\",\n            \"zipkin*span-2017.*\",\n            \"zipkin*span-2018.01.01\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesTens() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-10-30T01:01:01Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n      .containsExactly(\n        \"zipkin*span-2016.10.0*\",\n        \"zipkin*span-2016.10.1*\",\n        \"zipkin*span-2016.10.2*\",\n        \"zipkin*span-2016.10.30\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesTens_startingAtNine() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-09T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-10-30T01:01:01Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n      .containsExactly(\n        \"zipkin*span-2016.10.09\",\n        \"zipkin*span-2016.10.1*\",\n        \"zipkin*span-2016.10.2*\",\n        \"zipkin*span-2016.10.30\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesTens_startingAtNineteen() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-10-19T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-10-30T01:01:01Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n      .containsExactly(\n        \"zipkin*span-2016.10.19\",\n        \"zipkin*span-2016.10.2*\",\n        \"zipkin*span-2016.10.30\");\n  }\n\n  @Test void indexNameForTimestampRange_compressesTens_not30DayMonth() throws ParseException {\n    formatter = formatter.toBuilder().dateSeparator('.').build();\n    long start = iso8601.parse(\"2016-06-01T01:01:01Z\").getTime();\n    long end = iso8601.parse(\"2016-06-30T01:01:01Z\").getTime();\n\n    assertThat(formatter.formatTypeAndRange(\"span\", start, end))\n      .containsExactly(\"zipkin*span-2016.06.*\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/client/HttpCallTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client; // to access package-private stuff\n\nimport com.linecorp.armeria.client.UnprocessedRequestException;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.client.endpoint.EndpointGroupException;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.common.logging.RequestLog;\nimport com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.internal.Nullable;\n\nimport static com.linecorp.armeria.common.MediaType.PLAIN_TEXT_UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.awaitility.Awaitility.await;\nimport static org.junit.jupiter.api.Assertions.fail;\nimport static zipkin2.TestObjects.UTF_8;\n\nclass HttpCallTest {\n  static final HttpCall.BodyConverter<Object> NULL = (parser, contentString) -> null;\n\n  private static final AggregatedHttpResponse SUCCESS_RESPONSE =\n    AggregatedHttpResponse.of(HttpStatus.OK);\n\n  @RegisterExtension static MockWebServerExtension server = new MockWebServerExtension();\n\n  static final AggregatedHttpRequest REQUEST = AggregatedHttpRequest.of(HttpMethod.GET, \"/\");\n\n  HttpCall.Factory http;\n\n  @BeforeEach void setUp() {\n    http = new HttpCall.Factory(WebClient.of(server.httpUri()));\n  }\n\n  @Test void emptyContent() throws IOException {\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.OK, PLAIN_TEXT_UTF_8, \"\"));\n\n    HttpCall<String> call = http.newCall(REQUEST, (parser, contentString) -> fail(), \"test\");\n    assertThat(call.execute()).isNull();\n\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.OK, PLAIN_TEXT_UTF_8, \"\"));\n    CompletableCallback<String> future = new CompletableCallback<>();\n    http.newCall(REQUEST, (parser, contentString) -> \"hello\", \"test\").enqueue(future);\n    assertThat(future.join()).isNull();\n  }\n\n  @Test void propagatesOnDispatcherThreadWhenFatal() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    final LinkedBlockingQueue<Object> q = new LinkedBlockingQueue<>();\n    CountDownLatch latch = new CountDownLatch(1);\n    http.newCall(REQUEST, (parser, contentString) -> {\n      latch.countDown();\n      throw new LinkageError();\n    }, \"test\").enqueue(new Callback<Object>() {\n      @Override public void onSuccess(@Nullable Object value) {\n        q.add(value);\n      }\n\n      @Override public void onError(Throwable t) {\n        q.add(t);\n      }\n    });\n\n    // It can take some time for the HTTP response to process. Wait until we reach the parser\n    latch.await();\n\n    // Wait a little longer for a callback to fire (it should never do this)\n    assertThat(q.poll(100, TimeUnit.MILLISECONDS))\n      .as(\"expected callbacks to never signal\")\n      .isNull();\n  }\n\n  @Test void executionException_conversionException() {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Call<?> call = http.newCall(REQUEST, (parser, contentString) -> {\n      throw new IllegalArgumentException(\"eeek\");\n    }, \"test\");\n\n    assertThatThrownBy(call::execute).isInstanceOf(IllegalArgumentException.class);\n  }\n\n  @Test void cloned() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    Call<?> call = http.newCall(REQUEST, (parser, contentString) -> null, \"test\");\n    call.execute();\n\n    assertThatThrownBy(call::execute).isInstanceOf(IllegalStateException.class);\n\n    server.enqueue(SUCCESS_RESPONSE);\n\n    call.clone().execute();\n  }\n\n  @Test void executionException_5xx() {\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR));\n\n    Call<?> call = http.newCall(REQUEST, NULL, \"test\");\n\n    assertThatThrownBy(call::execute)\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\"response for / failed: 500 Internal Server Error\");\n  }\n\n  @Test void executionException_404() {\n    server.enqueue(AggregatedHttpResponse.of(HttpStatus.NOT_FOUND));\n\n    Call<?> call = http.newCall(REQUEST, NULL, \"test\");\n\n    assertThatThrownBy(call::execute)\n      .isInstanceOf(FileNotFoundException.class)\n      .hasMessage(\"/\");\n  }\n\n  @Test void releasesAllReferencesToByteBuf() {\n    // Force this to be a ref-counted response\n    byte[] message = \"{\\\"Message\\\":\\\"error\\\"}\".getBytes(UTF_8);\n    ByteBuf encodedBuf = PooledByteBufAllocator.DEFAULT.buffer(message.length);\n    encodedBuf.writeBytes(message);\n    AggregatedHttpResponse response = AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.FORBIDDEN),\n      HttpData.wrap(encodedBuf)\n    );\n\n    HttpCall<Object> call = http.newCall(REQUEST, NULL, \"test\");\n\n    // Invoke the parser directly because using the fake server will not result in ref-counted\n    assertThatThrownBy(() -> call.parseResponse(response, NULL)).hasMessage(\"error\");\n    assertThat(encodedBuf.refCnt()).isEqualTo(0);\n  }\n\n  // For simplicity, we also parse messages from AWS Elasticsearch, as it prevents copy/paste.\n  @Test void executionException_message() {\n    Map<AggregatedHttpResponse, String> responseToMessage = new LinkedHashMap<>();\n    responseToMessage.put(AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.FORBIDDEN),\n      HttpData.ofUtf8(\n        \"{\\\"Message\\\":\\\"User: anonymous is not authorized to perform: es:ESHttpGet\\\"}\")\n    ), \"User: anonymous is not authorized to perform: es:ESHttpGet\");\n    responseToMessage.put(AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.FORBIDDEN)\n    ), \"response for / failed: 403 Forbidden\");\n    responseToMessage.put(AggregatedHttpResponse.of(\n      ResponseHeaders.of(HttpStatus.BAD_GATEWAY),\n      HttpData.ofUtf8(\"Message: sleet\") // note: not json\n    ), \"response for / failed: Message: sleet\"); // In this case, we give request context\n\n    Call<?> call = http.newCall(REQUEST, NULL, \"test\");\n\n    for (Map.Entry<AggregatedHttpResponse, String> entry : responseToMessage.entrySet()) {\n      server.enqueue(entry.getKey());\n\n      call = call.clone();\n      assertThatThrownBy(call::execute)\n        .isInstanceOf(RuntimeException.class)\n        .hasMessage(entry.getValue());\n    }\n  }\n\n  @Test void setsCustomName() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    AtomicReference<RequestLog> log = new AtomicReference<>();\n    http = new HttpCall.Factory(WebClient.builder(server.httpUri())\n      .decorator((client, ctx, req) -> {\n        ctx.log().whenComplete().thenAccept(log::set);\n        return client.execute(ctx, req);\n      })\n      .build());\n\n    http.newCall(REQUEST, NULL, \"custom-name\").execute();\n\n    await().untilAsserted(() -> assertThat(log).doesNotHaveValue(null));\n    assertThat(log.get().name()).isEqualTo(\"custom-name\");\n  }\n\n  @Test void wrongScheme() {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    http = new HttpCall.Factory(WebClient.builder(\"https://localhost:\" + server.httpPort()).build());\n\n    assertThatThrownBy(() -> http.newCall(REQUEST, NULL, \"test\").execute())\n      .isInstanceOf(RejectedExecutionException.class)\n      // depending on JDK this is \"OPENSSL_internal\" or \"not an SSL/TLS record\"\n      .hasMessageContaining(\"SSL\");\n  }\n\n  @Test void unprocessedRequest() {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    http = new HttpCall.Factory(WebClient.builder(server.httpUri())\n      .decorator((client, ctx, req) -> {\n        throw UnprocessedRequestException.of(new EndpointGroupException(\"No endpoints\"));\n      })\n      .build());\n\n    assertThatThrownBy(() -> http.newCall(REQUEST, NULL, \"test\").execute())\n      .isInstanceOf(RejectedExecutionException.class)\n      .hasMessage(\"No endpoints\");\n  }\n\n  @Test void throwsRuntimeExceptionAsReasonWhenPresent() {\n    String body =\n      \"{\\\"error\\\":{\\\"root_cause\\\":[{\\\"type\\\":\\\"illegal_argument_exception\\\",\\\"reason\\\":\\\"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\\\"}],\\\"type\\\":\\\"search_phase_execution_exception\\\",\\\"reason\\\":\\\"all shards failed\\\",\\\"phase\\\":\\\"query\\\",\\\"grouped\\\":true,\\\"failed_shards\\\":[{\\\"shard\\\":0,\\\"index\\\":\\\"zipkin-2017-05-14\\\",\\\"node\\\":\\\"IqceAwZnSvyv0V0xALkEnQ\\\",\\\"reason\\\":{\\\"type\\\":\\\"illegal_argument_exception\\\",\\\"reason\\\":\\\"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\\\"}}]},\\\"status\\\":400}\";\n    server.enqueue(\n      AggregatedHttpResponse.of(ResponseHeaders.of(HttpStatus.BAD_REQUEST), HttpData.ofUtf8(body))\n    );\n\n    assertThatThrownBy(() -> http.newCall(REQUEST, NULL, \"test\").execute())\n      .isInstanceOf(RuntimeException.class)\n      .hasMessage(\n        \"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\");\n  }\n\n  @Test void streamingContent() throws Exception {\n    server.enqueue(SUCCESS_RESPONSE);\n\n    HttpCall.RequestSupplier supplier = new HttpCall.RequestSupplier() {\n\n      final RequestHeaders headers = RequestHeaders.of(HttpMethod.POST, \"/\");\n\n      @Override public RequestHeaders headers() {\n        return headers;\n      }\n\n      @Override public HttpRequest get() {\n        return HttpRequest.of(headers, HttpData.ofUtf8(\"hello\"), HttpData.ofUtf8(\" world\"));\n      }\n    };\n\n    http.newCall(supplier, NULL, \"test\").execute();\n\n    AggregatedHttpRequest request = server.takeRequest().request();\n    assertThat(request.method()).isEqualTo(HttpMethod.POST);\n    assertThat(request.path()).isEqualTo(\"/\");\n    assertThat(request.contentUtf8()).isEqualTo(\"hello world\");\n  }\n\n  // TODO(adriancole): Find a home for this generic conversion between Call and Java 8.\n  static final class CompletableCallback<T> extends CompletableFuture<T> implements Callback<T> {\n\n    @Override public void onSuccess(T value) {\n      complete(value);\n    }\n\n    @Override public void onError(Throwable t) {\n      completeExceptionally(t);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/client/SearchCallFactoryTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport com.linecorp.armeria.client.WebClient;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\n\nclass SearchCallFactoryTest {\n  WebClient httpClient = mock(WebClient.class);\n\n  SearchCallFactory client = new SearchCallFactory(new HttpCall.Factory(httpClient));\n\n  /** Declaring queries alphabetically helps simplify amazon signature logic */\n  @Test void lenientSearchOrdersQueryAlphabetically() {\n    assertThat(client.lenientSearch(List.of(\"zipkin:span-2016-10-01\"), null))\n        .endsWith(\"/_search?allow_no_indices=true&expand_wildcards=open&ignore_unavailable=true\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/client/SearchRequestTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.elasticsearch.internal.client;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.elasticsearch.internal.JsonSerializers.OBJECT_MAPPER;\n\nclass SearchRequestTest {\n\n  SearchRequest request = SearchRequest.create(List.of(\"zipkin-2016.11.31\"));\n\n  @Test void defaultSizeIsMaxResultWindow() {\n    assertThat(request.size)\n      .isEqualTo(10000);\n  }\n\n  /** Indices and type affect the request URI, not the json body */\n  @Test void doesntSerializeIndicesOrType() throws Exception {\n    assertThat(OBJECT_MAPPER.writeValueAsString(request))\n      .isEqualTo(\"{\\\"size\\\":10000}\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/elasticsearch/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# stop huge spam\norg.slf4j.simpleLogger.log.org.testcontainers.dockerclient=off\n\n# Ensure when ES_DEBUG=true tests dump trace output\norg.slf4j.simpleLogger.log.com.linecorp.armeria.client.logging=info\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/README.md",
    "content": "# storage-mysql-v1\nThis MySQL (Legacy) storage component includes a blocking `SpanStore` and span consumer function.\n`SpanStore.getDependencies()` aggregates dependency links on-demand.\n\nThe implementation uses JOOQ to generate MySQL SQL commands. MySQL 5.6+\nfeatures are used, but tests run against MariaDB 10.3.\n\nSee the [schema DDL](src/main/resources/mysql.sql).\n\n`zipkin2.storage.mysql.v1.MySQLStorage.Builder` includes defaults that will\noperate against a given Datasource.\n\n## Testing this component\nThis module conditionally runs integration tests against a Docker managed MySQL container.\n\nEx.\n```\n$ ./mvnw clean verify -pl :zipkin-storage-mysql-v1\n```\n\nIf you run tests via Maven or otherwise without Docker, you'll notice tests are silently skipped.\n```\nResults :\n\nTests run: 49, Failures: 0, Errors: 0, Skipped: 48\n```\n\nThis behaviour is intentional: We don't want to burden developers with\ninstalling and running all storage options to test unrelated change.\nThat said, all integration tests run on pull request.\n\n## Exploring Zipkin Data\n\nWhen troubleshooting, it is important to note that zipkin ids are encoded as hex.\nIf you want to view data in mysql, you'll need to use the hex function accordingly.\n\nFor example, all the below query the same trace using different tools:\n* zipkin-ui: `http://1.2.3.4:9411/traces/27960dafb1ea7454`\n* zipkin-api: `http://1.2.3.4:9411/api/v1/trace/27960dafb1ea7454?raw`\n* mysql: `select * from zipkin_spans where trace_id = x'27960dafb1ea7454';`\n\nIf you are trying to debug from data in the database, it is helpful to\nformat IDs as hex, and timestamps as dates. The following is an example\nquery which will return one line for each update to a span in the last\n5 minutes.\n\n```sql\nSELECT lower(concat(CASE trace_id_high\n                        WHEN '0' THEN ''\n                        ELSE hex(trace_id_high)\n                    END,hex(trace_id))) AS trace_id,\n       lower(hex(parent_id)) as parent_id,\n       lower(hex(id)) as span_id,\n       name,\n       from_unixtime(start_ts/1000000) as timestamp\nFROM zipkin_spans\nwhere (start_ts/1000000) > UNIX_TIMESTAMP(now()) - 5 * 60;\n```\n\nFor example, the output below shows two traces recently reported. One of\nwhich is using 128-bit trace IDs. You could copy and paste the `trace_id`\ninto zipkin's UI to troubleshoot further.\n```\n+----------------------------------+------------------+------------------+------+--------------------------+\n| trace_id                         | parent_id        | span_id          | name | timestamp                |\n+----------------------------------+------------------+------------------+------+--------------------------+\n| abbd9f5da49e5848aa4b729ff2bc90a3 | NULL             | aa4b729ff2bc90a3 | get  | 2017-04-19 12:43:00.9830 |\n| abbd9f5da49e5848aa4b729ff2bc90a3 | aa4b729ff2bc90a3 | 7888a4aef81f074d | get  | 2017-04-19 12:43:01.1960 |\n| 11b98d7107dac980                 | 11b98d7107dac980 | bc33c2d5ad25bf89 | get  | 2017-04-19 12:42:45.4240 |\n| 11b98d7107dac980                 | NULL             | 11b98d7107dac980 | get  | 2017-04-19 12:42:45.0680 |\n+----------------------------------+------------------+------------------+------+--------------------------+\n```\n\n## Applying the schema\n\n```bash\n# Barracuda supports compression (In AWS RDS, this must be assigned in a parameter group)\n$ mysql -uroot -e \"SET GLOBAL innodb_file_format=Barracuda\"\n# This command should work even in RDS, and return \"Barracuda\"\n$ mysql -uroot -e \"show global variables like 'innodb_file_format'\"\n\n# install the schema and indexes\n$ mysql -uroot -e \"create database if not exists zipkin\"\n$ mysql -uroot -Dzipkin < zipkin-storage/mysql-v1/src/main/resources/mysql.sql\n```\n\n## Generating the schema types\n\n```bash\n$ rm -rf rm -rf zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/\n$ ./mvnw -pl :zipkin-storage-mysql-v1 clean org.jooq:jooq-codegen-maven:generate com.mycila:license-maven-plugin:format\n```\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin.zipkin2</groupId>\n    <artifactId>zipkin-storage-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>zipkin-storage-mysql-v1</artifactId>\n  <name>Storage: MySQL (v1)</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/../..</main.basedir>\n\n    <jooq.version>3.19.18</jooq.version>\n\n    <!-- jOOQ doesn't add the Generated annotation, so we have to explicitly disable rules -->\n    <errorprone.args>-Xep:InconsistentCapitalization:OFF</errorprone.args>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.jooq</groupId>\n      <artifactId>jooq</artifactId>\n      <version>${jooq.version}</version>\n    </dependency>\n\n    <!-- for Generated annotation -->\n    <dependency>\n      <groupId>javax.annotation</groupId>\n      <artifactId>javax.annotation-api</artifactId>\n      <version>${javax-annotation-api.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.mariadb.jdbc</groupId>\n      <artifactId>mariadb-java-client</artifactId>\n      <version>${mariadb-java-client.version}</version>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mysql</artifactId>\n      <version>${testcontainers.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <pluginManagement>\n      <plugins>\n        <plugin>\n          <groupId>org.jooq</groupId>\n          <artifactId>jooq-codegen-maven</artifactId>\n          <version>${jooq.version}</version>\n          <executions>\n            <execution>\n              <goals>\n                <goal>generate</goal>\n              </goals>\n            </execution>\n          </executions>\n          <dependencies>\n            <dependency>\n              <groupId>org.jooq</groupId>\n              <artifactId>jooq</artifactId>\n              <version>${jooq.version}</version>\n            </dependency>\n            <dependency>\n              <groupId>org.mariadb.jdbc</groupId>\n              <artifactId>mariadb-java-client</artifactId>\n              <version>${mariadb-java-client.version}</version>\n            </dependency>\n          </dependencies>\n          <configuration>\n            <jdbc>\n              <driver>org.mariadb.jdbc.Driver</driver>\n              <url>jdbc:mariadb://localhost:3306/zipkin</url>\n              <user>root</user>\n              <password />\n            </jdbc>\n            <generator>\n              <generate>\n                <relations>false</relations>\n                <deprecated>false</deprecated>\n                <records>false</records>\n                <pojos>false</pojos>\n                <!-- Prevents Intellij from conflating source level with api level -->\n                <generatedAnnotationType>JAVAX_ANNOTATION_GENERATED</generatedAnnotationType>\n              </generate>\n              <database>\n                <name>org.jooq.meta.mysql.MySQLDatabase</name>\n                <includes>.*</includes>\n                <excludes />\n                <inputSchema>zipkin</inputSchema>\n              </database>\n              <target>\n                <packageName>zipkin2.storage.mysql.v1.internal.generated</packageName>\n                <directory>src/main/java</directory>\n              </target>\n            </generator>\n          </configuration>\n        </plugin>\n      </plugins>\n    </pluginManagement>\n  </build>\n</project>\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/AggregateDependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.Cursor;\nimport org.jooq.DSLContext;\nimport org.jooq.Record;\nimport org.jooq.Record1;\nimport org.jooq.SelectConditionStep;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.internal.DependencyLinker;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class AggregateDependencies implements Function<DSLContext, List<DependencyLink>> {\n  final Schema schema;\n  final long startTsBegin, startTsEnd;\n\n  AggregateDependencies(Schema schema, long startTsBegin, long startTsEnd) {\n    this.schema = schema;\n    this.startTsBegin = startTsBegin;\n    this.startTsEnd = startTsEnd;\n  }\n\n  @Override\n  public List<DependencyLink> apply(DSLContext context) {\n    // Subquery on trace IDs to prevent only matching the part of the trace that exists within\n    // the interval: we want all of the trace.\n    SelectConditionStep<Record1<Long>> traceIDs = context.selectDistinct(ZIPKIN_SPANS.TRACE_ID)\n      .from(ZIPKIN_SPANS)\n      .where(startTsBegin == startTsEnd\n        ? ZIPKIN_SPANS.START_TS.lessOrEqual(startTsEnd)\n        : ZIPKIN_SPANS.START_TS.between(startTsBegin, startTsEnd));\n    // Lazy fetching the cursor prevents us from buffering the whole dataset in memory.\n    Cursor<Record> cursor = context.selectDistinct(schema.dependencyLinkerFields)\n      // left joining allows us to keep a mapping of all span ids, not just ones that have\n      // special annotations. We need all span ids to reconstruct the trace tree. We need\n      // the whole trace tree so that we can accurately skip local spans.\n      .from(ZIPKIN_SPANS.leftJoin(ZIPKIN_ANNOTATIONS)\n        // NOTE: we are intentionally grouping only on the low-bits of trace id. This\n        // buys time for applications to upgrade to 128-bit instrumentation.\n        .on(ZIPKIN_SPANS.TRACE_ID.eq(ZIPKIN_ANNOTATIONS.TRACE_ID)\n          .and(ZIPKIN_SPANS.ID.eq(ZIPKIN_ANNOTATIONS.SPAN_ID)))\n        .and(ZIPKIN_ANNOTATIONS.A_KEY.in(\"lc\", \"cs\", \"ca\", \"sr\", \"sa\", \"ma\", \"mr\", \"ms\", \"error\")))\n      .where(ZIPKIN_SPANS.TRACE_ID.in(traceIDs))\n      // Grouping so that later code knows when a span or trace is finished.\n      .groupBy(schema.dependencyLinkerGroupByFields)\n      .fetchLazy();\n\n    Iterator<Iterator<Span>> traces =\n      new DependencyLinkV2SpanIterator.ByTraceId(cursor.iterator(), schema.hasTraceIdHigh);\n\n    if (!traces.hasNext()) return List.of();\n\n    DependencyLinker linker = new DependencyLinker();\n\n    List<Span> nextTrace = new ArrayList<>();\n    while (traces.hasNext()) {\n      Iterator<Span> i = traces.next();\n      while (i.hasNext()) nextTrace.add(i.next());\n      linker.putTrace(nextTrace);\n      nextTrace.clear();\n    }\n\n    return linker.link();\n  }\n\n  @Override\n  public String toString() {\n    return \"AggregateDependencies{\"\n      + \"startTsBegin=\"\n      + startTsBegin\n      + \", startTsEnd=\"\n      + startTsEnd\n      + '}';\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/DSLContexts.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport org.jooq.DSLContext;\nimport org.jooq.ExecuteListenerProvider;\nimport org.jooq.SQLDialect;\nimport org.jooq.conf.Settings;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.DefaultConfiguration;\nimport zipkin2.internal.Nullable;\n\nfinal class DSLContexts {\n  private final Settings settings;\n  private final ExecuteListenerProvider listenerProvider;\n\n  DSLContexts(Settings settings, @Nullable ExecuteListenerProvider listenerProvider) {\n    this.settings = settings;\n    this.listenerProvider = listenerProvider;\n  }\n\n  DSLContext get(Connection conn) {\n    return DSL.using(\n        new DefaultConfiguration()\n            .set(conn)\n            .set(SQLDialect.MYSQL)\n            .set(settings)\n            .set(listenerProvider));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/DataSourceCall.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.io.IOException;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.concurrent.Executor;\nimport java.util.function.Function;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport zipkin2.Call;\nimport zipkin2.Callback;\n\n/** Uncancelable call built with an executor */\nfinal class DataSourceCall<V> extends Call.Base<V> {\n\n  static final class Factory {\n    final DataSource datasource;\n    final DSLContexts context;\n    final Executor executor;\n\n    Factory(DataSource datasource, DSLContexts context, Executor executor) {\n      this.datasource = datasource;\n      this.context = context;\n      this.executor = executor;\n    }\n\n    <V> DataSourceCall<V> create(Function<DSLContext, V> queryFunction) {\n      return new DataSourceCall<>(this, queryFunction);\n    }\n  }\n\n  final Factory factory;\n  final Function<DSLContext, V> queryFunction;\n\n  DataSourceCall(Factory factory, Function<DSLContext, V> queryFunction) {\n    this.factory = factory;\n    this.queryFunction = queryFunction;\n  }\n\n  @Override\n  protected final V doExecute() throws IOException {\n    try (Connection conn = factory.datasource.getConnection()) {\n      DSLContext context = factory.context.get(conn);\n      return queryFunction.apply(context);\n    } catch (SQLException e) {\n      throw new IOException(e);\n    }\n  }\n\n  @Override\n  protected void doEnqueue(Callback<V> callback) {\n    class CallbackRunnable implements Runnable {\n      @Override\n      public void run() {\n        try {\n          callback.onSuccess(doExecute());\n        } catch (IOException e) {\n          // unwrap the exception\n          if (e.getCause() instanceof SQLException) {\n            callback.onError(e.getCause());\n          } else {\n            callback.onError(e);\n          }\n        } catch (Throwable t) {\n          propagateIfFatal(t);\n          callback.onError(t);\n        }\n      }\n    }\n    factory.executor.execute(new CallbackRunnable());\n  }\n\n  @Override\n  public String toString() {\n    return queryFunction.toString();\n  }\n\n  @Override\n  public Call<V> clone() {\n    return new DataSourceCall<>(factory, queryFunction);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/DependencyLinkV2SpanIterator.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\nimport org.jooq.Record;\nimport org.jooq.TableField;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\nimport zipkin2.v1.V1BinaryAnnotation;\n\nimport static zipkin2.storage.mysql.v1.Schema.maybeGet;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\n/**\n * Lazy converts rows into {@linkplain Span} objects suitable for dependency links. This takes\n * short-cuts to require less data. For example, it folds shared RPC spans into one, and doesn't\n * include tags, non-core annotations or time units.\n *\n * <p>Out-of-date schemas may be missing the trace_id_high field. When present, the {@link\n * Span#traceId()} could be 32 characters in logging statements.\n */\nfinal class DependencyLinkV2SpanIterator implements Iterator<Span> {\n\n  /** Assumes the input records are sorted by trace id, span id */\n  static final class ByTraceId implements Iterator<Iterator<Span>> {\n    final PeekingIterator<Record> delegate;\n    final boolean hasTraceIdHigh;\n\n    long currentTraceIdHi, currentTraceIdLo;\n\n    ByTraceId(Iterator<Record> delegate, boolean hasTraceIdHigh) {\n      this.delegate = new PeekingIterator<>(delegate);\n      this.hasTraceIdHigh = hasTraceIdHigh;\n    }\n\n    @Override\n    public boolean hasNext() {\n      return delegate.hasNext();\n    }\n\n    @Override\n    public Iterator<Span> next() {\n      if (!hasNext()) throw new NoSuchElementException();\n      currentTraceIdHi = hasTraceIdHigh ? traceIdHigh(delegate) : 0L;\n      currentTraceIdLo = delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID);\n      return new DependencyLinkV2SpanIterator(delegate, currentTraceIdHi, currentTraceIdLo);\n    }\n\n    @Override\n    public void remove() {\n      throw new UnsupportedOperationException();\n    }\n  }\n\n  final PeekingIterator<Record> delegate;\n  final long traceIdHi, traceIdLo;\n\n  DependencyLinkV2SpanIterator(PeekingIterator<Record> delegate, long traceIdHi, long traceIdLo) {\n    this.delegate = delegate;\n    this.traceIdHi = traceIdHi;\n    this.traceIdLo = traceIdLo;\n  }\n\n  @Override\n  public boolean hasNext() {\n    return delegate.hasNext()\n        // We don't have a query parameter for strictTraceId when fetching dependency links, so we\n        // ignore traceIdHigh. Otherwise, a single trace can appear as two, doubling callCount.\n        && delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID) == traceIdLo;\n  }\n\n  @Override\n  public Span next() {\n    if (!hasNext()) throw new NoSuchElementException();\n    Record row = delegate.peek();\n\n    long spanId = row.getValue(ZipkinSpans.ZIPKIN_SPANS.ID);\n    boolean error = false;\n    String lcService = null, srService = null, csService = null, caService = null, saService = null,\n      maService = null, mrService = null, msService = null;\n    while (hasNext()) { // there are more values for this trace\n      if (spanId != delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.ID)) {\n        break; // if we are in a new span\n      }\n      Record next = delegate.next(); // row for the same span\n\n      String key = emptyToNull(next, ZIPKIN_ANNOTATIONS.A_KEY);\n      String value = emptyToNull(next, ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME);\n      if (key == null || value == null) continue; // neither client nor server\n      switch (key) {\n        case \"lc\":\n          lcService = value;\n          break;\n        case \"ca\":\n          caService = value;\n          break;\n        case \"cs\":\n          csService = value;\n          break;\n        case \"ma\":\n          maService = value;\n          break;\n        case \"mr\":\n          mrService = value;\n          break;\n        case \"ms\":\n          msService = value;\n          break;\n        case \"sa\":\n          saService = value;\n          break;\n        case \"sr\":\n          srService = value;\n          break;\n        case \"error\":\n          // a span is in error if it has a tag, not an annotation, of name \"error\"\n          error = V1BinaryAnnotation.TYPE_STRING == next.get(ZIPKIN_ANNOTATIONS.A_TYPE);\n      }\n    }\n\n    // The client address is more authoritative than the client send owner.\n    if (caService == null) caService = csService;\n\n    // Finagle labels two sides of the same socket (\"ca\", \"sa\") with the same name.\n    // Skip the client side, so it isn't mistaken for a loopback request\n    if (saService != null && saService.equals(caService)) caService = null;\n\n    long parentId = maybeGet(row, ZipkinSpans.ZIPKIN_SPANS.PARENT_ID, 0L);\n    Span.Builder result =\n        Span.newBuilder().traceId(traceIdHi, traceIdLo).parentId(parentId).id(spanId);\n\n    if (error) {\n      result.putTag(\"error\", \"\" /* actual value doesn't matter */);\n    }\n\n    if (srService != null) {\n      return result\n          .kind(Span.Kind.SERVER)\n          .localEndpoint(ep(srService))\n          .remoteEndpoint(ep(caService))\n          .build();\n    } else if (saService != null) {\n      Endpoint localEndpoint = ep(caService);\n      // When span.kind is missing, the local endpoint is \"lc\" and the remote endpoint is \"sa\"\n      if (localEndpoint == null) localEndpoint = ep(lcService);\n      return result\n          .kind(csService != null ? Span.Kind.CLIENT : null)\n          .localEndpoint(localEndpoint)\n          .remoteEndpoint(ep(saService))\n          .build();\n    } else if (csService != null) {\n      return result.kind(Span.Kind.SERVER).localEndpoint(ep(caService)).build();\n    } else if (mrService != null) {\n      return result\n        .kind(Span.Kind.CONSUMER)\n        .localEndpoint(ep(mrService))\n        .remoteEndpoint(ep(maService))\n        .build();\n    } else if (msService != null) {\n      return result\n        .kind(Span.Kind.PRODUCER)\n        .localEndpoint(ep(msService))\n        .remoteEndpoint(ep(maService))\n        .build();\n    }\n    return result.build();\n  }\n\n  @Override\n  public void remove() {\n    throw new UnsupportedOperationException();\n  }\n\n  static long traceIdHigh(PeekingIterator<Record> delegate) {\n    return delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH);\n  }\n\n  static @Nullable String emptyToNull(Record next, TableField<Record, String> field) {\n    String result = next.getValue(field);\n    return result != null && !result.isEmpty() ? result : null;\n  }\n\n  static Endpoint ep(@Nullable String serviceName) {\n    return serviceName != null ? Endpoint.newBuilder().serviceName(serviceName).build() : null;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/HasErrorCount.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\nfinal class HasErrorCount {\n  private static final Logger LOG = Logger.getLogger(HasErrorCount.class.getName());\n\n  static boolean test(DataSource datasource, DSLContexts context) {\n    try (Connection conn = datasource.getConnection()) {\n      DSLContext dsl = context.get(conn);\n      dsl.select(ZIPKIN_DEPENDENCIES.ERROR_COUNT).from(ZIPKIN_DEPENDENCIES).limit(1).fetchAny();\n      return true;\n    } catch (DataAccessException e) {\n      if (e.sqlState().equals(\"42S22\")) {\n        LOG.warning(\n            \"\"\"\n            zipkin_dependencies.error_count doesn't exist, so DependencyLink.errorCount is not supported. \\\n            Execute: alter table zipkin_dependencies add `error_count` BIGINT\\\n            \"\"\");\n        return false;\n      }\n      problemReading(e);\n    } catch (SQLException | RuntimeException e) {\n      problemReading(e);\n    }\n    return false;\n  }\n\n  static void problemReading(Exception e) {\n    LOG.log(Level.WARNING, \"problem reading zipkin_dependencies.error_count\", e);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/HasIpv6.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\nfinal class HasIpv6 {\n  private static final Logger LOG = Logger.getLogger(HasIpv6.class.getName());\n\n  static boolean test(DataSource datasource, DSLContexts context) {\n    try (Connection conn = datasource.getConnection()) {\n      DSLContext dsl = context.get(conn);\n      dsl.select(ZIPKIN_ANNOTATIONS.ENDPOINT_IPV6).from(ZIPKIN_ANNOTATIONS).limit(1).fetchAny();\n      return true;\n    } catch (DataAccessException e) {\n      if (e.sqlState().equals(\"42S22\")) {\n        LOG.warning(\n            \"\"\"\n            zipkin_annotations.ipv6 doesn't exist, so Endpoint.ipv6 is not supported. \\\n            Execute: alter table zipkin_annotations add `endpoint_ipv6` BINARY(16)\\\n            \"\"\");\n        return false;\n      }\n      problemReading(e);\n    } catch (SQLException | RuntimeException e) {\n      problemReading(e);\n    }\n    return false;\n  }\n\n  static void problemReading(Exception e) {\n    LOG.log(Level.WARNING, \"problem reading zipkin_annotations.ipv6\", e);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/HasPreAggregatedDependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\n\nimport static org.jooq.impl.DSL.count;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\n/**\n * Returns true when the zipkin_dependencies table exists and has data in it, implying the spark job\n * has been run.\n */\nfinal class HasPreAggregatedDependencies {\n  private static final Logger LOG = Logger.getLogger(HasPreAggregatedDependencies.class.getName());\n\n  static boolean test(DataSource datasource, DSLContexts context) {\n    try (Connection conn = datasource.getConnection()) {\n      DSLContext dsl = context.get(conn);\n      return dsl.select(count()).from(ZIPKIN_DEPENDENCIES).fetchAny().value1() > 0;\n    } catch (DataAccessException e) {\n      if (e.sqlState().equals(\"42S02\")) {\n        LOG.warning(\n            \"\"\"\n            zipkin_dependencies doesn't exist, so pre-aggregated dependencies are not \\\n            supported. Execute mysql.sql located in this jar to add the table\\\n            \"\"\");\n        return false;\n      }\n      problemReading(e);\n    } catch (SQLException | RuntimeException e) {\n      problemReading(e);\n    }\n    return false;\n  }\n\n  static void problemReading(Exception e) {\n    LOG.log(Level.WARNING, \"problem reading zipkin_dependencies\", e);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/HasRemoteServiceName.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class HasRemoteServiceName {\n  static final Logger LOG = Logger.getLogger(HasRemoteServiceName.class.getName());\n  static final String MESSAGE =\n    \"\"\"\n    zipkin_spans.remote_service_name doesn't exist, so queries for remote service names will return empty.\n    Execute: ALTER TABLE zipkin_spans ADD `remote_service_name` VARCHAR(255);\n    ALTER TABLE zipkin_spans ADD INDEX `remote_service_name`;\\\n    \"\"\";\n\n  static boolean test(DataSource datasource, DSLContexts context) {\n    try (Connection conn = datasource.getConnection()) {\n      DSLContext dsl = context.get(conn);\n      dsl.select(ZIPKIN_SPANS.REMOTE_SERVICE_NAME).from(ZIPKIN_SPANS).limit(1).fetchAny();\n      return true;\n    } catch (DataAccessException e) {\n      if (e.sqlState().equals(\"42S22\")) {\n        LOG.warning(MESSAGE);\n        return false;\n      }\n      problemReading(e);\n    } catch (SQLException | RuntimeException e) {\n      problemReading(e);\n    }\n    return false;\n  }\n\n  static void problemReading(Exception e) {\n    LOG.log(Level.WARNING, \"problem reading zipkin_spans.remote_service_name\", e);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/HasTraceIdHigh.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.sql.DataSource;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class HasTraceIdHigh {\n  static final Logger LOG = Logger.getLogger(HasTraceIdHigh.class.getName());\n  static final String MESSAGE =\n      \"\"\"\n      zipkin_spans.trace_id_high doesn't exist, so 128-bit trace ids are not supported. \\\n      Execute: ALTER TABLE zipkin_spans ADD `trace_id_high` BIGINT NOT NULL DEFAULT 0;\n      ALTER TABLE zipkin_annotations ADD `trace_id_high` BIGINT NOT NULL DEFAULT 0;\n      ALTER TABLE zipkin_spans\\\n         DROP INDEX trace_id,\n         ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`);\n      ALTER TABLE zipkin_annotations\n         DROP INDEX trace_id,\n         ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`);\\\n      \"\"\";\n\n  static boolean test(DataSource datasource, DSLContexts context) {\n    try (Connection conn = datasource.getConnection()) {\n      DSLContext dsl = context.get(conn);\n      dsl.select(ZIPKIN_SPANS.TRACE_ID_HIGH).from(ZIPKIN_SPANS).limit(1).fetchAny();\n      return true;\n    } catch (DataAccessException e) {\n      if (e.sqlState().equals(\"42S22\")) {\n        LOG.warning(MESSAGE);\n        return false;\n      }\n      problemReading(e);\n    } catch (SQLException | RuntimeException e) {\n      problemReading(e);\n    }\n    return false;\n  }\n\n  static void problemReading(Exception e) {\n    LOG.log(Level.WARNING, \"problem reading zipkin_spans.trace_id_high\", e);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/MySQLAutocompleteTags.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport zipkin2.Call;\nimport zipkin2.storage.AutocompleteTags;\n\nfinal class MySQLAutocompleteTags implements AutocompleteTags {\n  final DataSourceCall.Factory dataSourceCallFactory;\n  final Schema schema;\n  final boolean enabled;\n  final LinkedHashSet<String> autocompleteKeys;\n  final Call<List<String>> keysCall;\n\n  MySQLAutocompleteTags(MySQLStorage storage, Schema schema) {\n    this.dataSourceCallFactory = storage.dataSourceCallFactory;\n    this.schema = schema;\n    enabled = storage.searchEnabled && !storage.autocompleteKeys.isEmpty();\n    autocompleteKeys = new LinkedHashSet<>(storage.autocompleteKeys);\n    keysCall = Call.create(storage.autocompleteKeys);\n  }\n\n  @Override public Call<List<String>> getKeys() {\n    if (!enabled) return Call.emptyList();\n    return keysCall.clone();\n  }\n\n  @Override public Call<List<String>> getValues(String key) {\n    if (key == null) throw new NullPointerException(\"key == null\");\n    if (key.isEmpty()) throw new IllegalArgumentException(\"key was empty\");\n    if (!enabled || !autocompleteKeys.contains(key)) return Call.emptyList();\n    return dataSourceCallFactory.create(new SelectAutocompleteValues(schema, key));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/MySQLSpanConsumer.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.jooq.DSLContext;\nimport org.jooq.InsertSetMoreStep;\nimport org.jooq.Query;\nimport org.jooq.Record;\nimport org.jooq.TableField;\nimport zipkin2.Call;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.v1.V1Annotation;\nimport zipkin2.v1.V1BinaryAnnotation;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V2SpanConverter;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class MySQLSpanConsumer implements SpanConsumer {\n  static final byte[] ONE = {1};\n\n  final DataSourceCall.Factory dataSourceCallFactory;\n  final Schema schema;\n\n  MySQLSpanConsumer(DataSourceCall.Factory dataSourceCallFactory, Schema schema) {\n    this.dataSourceCallFactory = dataSourceCallFactory;\n    this.schema = schema;\n  }\n\n  @Override\n  public Call<Void> accept(List<Span> spans) {\n    if (spans.isEmpty()) return Call.create(null);\n    return dataSourceCallFactory.create(new BatchInsertSpans(spans, schema));\n  }\n\n  static final class BatchInsertSpans implements Function<DSLContext, Void> {\n    final List<Span> spans;\n    final Schema schema;\n\n    BatchInsertSpans(List<Span> spans, Schema schema) {\n      this.spans = spans;\n      this.schema = schema;\n    }\n\n    @Override\n    public Void apply(DSLContext create) {\n      List<Query> inserts = new ArrayList<>();\n      V2SpanConverter v2SpanConverter = V2SpanConverter.create();\n\n      for (Span v2 : spans) {\n        Endpoint ep = v2.localEndpoint();\n        long timestamp = v2.timestampAsLong();\n\n        V1Span v1Span = v2SpanConverter.convert(v2);\n\n        long traceId, spanId;\n        InsertSetMoreStep<Record> insertSpan =\n            create\n                .insertInto(ZIPKIN_SPANS)\n                .set(ZIPKIN_SPANS.TRACE_ID, traceId = v1Span.traceId())\n                .set(ZIPKIN_SPANS.ID, spanId = v1Span.id())\n                .set(ZIPKIN_SPANS.DEBUG, v1Span.debug());\n\n        Map<TableField<Record, ?>, Object> updateFields = new LinkedHashMap<>();\n        if (timestamp != 0L) {\n          // tentatively we can use even a shared timestamp\n          insertSpan = insertSpan.set(ZIPKIN_SPANS.START_TS, timestamp);\n          // replace any tentative timestamp with the authoritative one.\n          if (!Boolean.TRUE.equals(v2.shared())) updateFields.put(ZIPKIN_SPANS.START_TS, timestamp);\n        }\n\n        insertSpan = updateName(v1Span.name(), ZIPKIN_SPANS.NAME, insertSpan, updateFields);\n        if (schema.hasRemoteServiceName) {\n          insertSpan = updateName(v2.remoteServiceName(), ZIPKIN_SPANS.REMOTE_SERVICE_NAME, insertSpan, updateFields);\n        }\n\n        long duration = v1Span.duration();\n        if (duration != 0L) {\n          insertSpan = insertSpan.set(ZIPKIN_SPANS.DURATION, duration);\n          updateFields.put(ZIPKIN_SPANS.DURATION, duration);\n        }\n\n        if (v1Span.parentId() != 0) {\n          insertSpan = insertSpan.set(ZIPKIN_SPANS.PARENT_ID, v1Span.parentId());\n          updateFields.put(ZIPKIN_SPANS.PARENT_ID, v1Span.parentId());\n        }\n\n        long traceIdHigh = schema.hasTraceIdHigh ? v1Span.traceIdHigh() : 0L;\n        if (traceIdHigh != 0L) {\n          insertSpan = insertSpan.set(ZIPKIN_SPANS.TRACE_ID_HIGH, traceIdHigh);\n        }\n\n        inserts.add(\n            updateFields.isEmpty()\n                ? insertSpan.onDuplicateKeyIgnore()\n                : insertSpan.onDuplicateKeyUpdate().set(updateFields));\n\n        int ipv4 =\n            ep != null && ep.ipv4Bytes() != null ? ByteBuffer.wrap(ep.ipv4Bytes()).getInt() : 0;\n        for (V1Annotation a : v1Span.annotations()) {\n          InsertSetMoreStep<Record> insert =\n              create\n                  .insertInto(ZIPKIN_ANNOTATIONS)\n                  .set(ZIPKIN_ANNOTATIONS.TRACE_ID, traceId)\n                  .set(ZIPKIN_ANNOTATIONS.SPAN_ID, spanId)\n                  .set(ZIPKIN_ANNOTATIONS.A_KEY, a.value())\n                  .set(ZIPKIN_ANNOTATIONS.A_TYPE, -1)\n                  .set(ZIPKIN_ANNOTATIONS.A_TIMESTAMP, a.timestamp());\n          if (traceIdHigh != 0L) {\n            insert = insert.set(ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, traceIdHigh);\n          }\n          insert = addEndpoint(insert, ep, ipv4);\n          inserts.add(insert.onDuplicateKeyIgnore());\n        }\n\n        for (V1BinaryAnnotation ba : v1Span.binaryAnnotations()) {\n          InsertSetMoreStep<Record> insert =\n              create\n                  .insertInto(ZIPKIN_ANNOTATIONS)\n                  .set(ZIPKIN_ANNOTATIONS.TRACE_ID, traceId)\n                  .set(ZIPKIN_ANNOTATIONS.SPAN_ID, spanId)\n                  .set(ZIPKIN_ANNOTATIONS.A_KEY, ba.key())\n                  .set(ZIPKIN_ANNOTATIONS.A_TYPE, ba.type())\n                  .set(ZIPKIN_ANNOTATIONS.A_TIMESTAMP, timestamp);\n          if (traceIdHigh != 0) {\n            insert = insert.set(ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, traceIdHigh);\n          }\n          if (ba.stringValue() != null) {\n            insert = insert.set(ZIPKIN_ANNOTATIONS.A_VALUE, ba.stringValue().getBytes(UTF_8));\n            insert = addEndpoint(insert, ep, ipv4);\n          } else { // add the address annotation\n            insert = insert.set(ZIPKIN_ANNOTATIONS.A_VALUE, ONE);\n            Endpoint nextEp = ba.endpoint();\n            insert = addEndpoint(\n                insert,\n                nextEp,\n                nextEp.ipv4Bytes() != null ? ByteBuffer.wrap(nextEp.ipv4Bytes()).getInt() : 0);\n          }\n          inserts.add(insert.onDuplicateKeyIgnore());\n        }\n      }\n      // TODO: See if DSLContext.batchMerge() can be used to avoid some of the complexity\n      // https://github.com/jOOQ/jOOQ/issues/3172\n      create.batch(inserts).execute();\n      return null;\n    }\n\n    InsertSetMoreStep<Record> addEndpoint(InsertSetMoreStep<Record> insert, Endpoint ep, int ipv4) {\n      if (ep == null) return insert;\n      // old code wrote empty service names\n      String serviceName = ep.serviceName() != null ? ep.serviceName() : \"\";\n      insert = insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME, serviceName);\n      if (ipv4 != 0) {\n        insert = insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_IPV4, ipv4);\n      }\n      if (ep.ipv6Bytes() != null && schema.hasIpv6) {\n        insert = insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_IPV6, ep.ipv6Bytes());\n      }\n      if (ep.portAsInt() != 0) {\n        insert = insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_PORT, (short) ep.portAsInt());\n      }\n      return insert;\n    }\n\n    @Override\n    public String toString() {\n      return \"BatchInsertSpansAndAnnotations{spans=\" + spans + \"}\";\n    }\n  }\n\n  static InsertSetMoreStep<Record> updateName(@Nullable String name, TableField<Record, String> column,\n    InsertSetMoreStep<Record> insertSpan, Map<TableField<Record, ?>, Object> updateFields) {\n    if (name != null && !name.equals(\"unknown\")) {\n      updateFields.put(column, name);\n      return insertSpan.set(column, name);\n    } else {\n      // old code wrote empty span name\n      return insertSpan.set(column, \"\");\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/MySQLSpanStore.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport zipkin2.Call;\nimport zipkin2.DependencyLink;\nimport zipkin2.Span;\nimport zipkin2.storage.GroupByTraceId;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StrictTraceId;\nimport zipkin2.storage.Traces;\n\nimport static zipkin2.internal.DateUtil.epochDays;\nimport static zipkin2.internal.HexCodec.lowerHexToUnsignedLong;\n\nfinal class MySQLSpanStore implements SpanStore, Traces, ServiceAndSpanNames {\n\n  final DataSourceCall.Factory dataSourceCallFactory;\n  final Schema schema;\n  final boolean strictTraceId, searchEnabled;\n  final SelectSpansAndAnnotations.Factory selectFromSpansAndAnnotationsFactory;\n  final Call.Mapper<List<Span>, List<List<Span>>> groupByTraceId;\n  final DataSourceCall<List<String>> getServiceNamesCall;\n\n  MySQLSpanStore(MySQLStorage storage, Schema schema) {\n    this.dataSourceCallFactory = storage.dataSourceCallFactory;\n    this.schema = schema;\n    this.strictTraceId = storage.strictTraceId;\n    this.searchEnabled = storage.searchEnabled;\n    this.selectFromSpansAndAnnotationsFactory =\n      new SelectSpansAndAnnotations.Factory(schema, strictTraceId);\n    this.groupByTraceId = GroupByTraceId.create(strictTraceId);\n    this.getServiceNamesCall = dataSourceCallFactory.create(new SelectAnnotationServiceNames());\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {\n    if (!searchEnabled) return Call.emptyList();\n\n    Call<List<List<Span>>> result =\n      dataSourceCallFactory\n        .create(selectFromSpansAndAnnotationsFactory.create(request))\n        .map(groupByTraceId);\n\n    return strictTraceId ? result.map(StrictTraceId.filterTraces(request)) : result;\n  }\n\n  @Override public Call<List<Span>> getTrace(String hexTraceId) {\n    // make sure we have a 16 or 32 character trace ID\n    hexTraceId = Span.normalizeTraceId(hexTraceId);\n    long traceIdHigh = hexTraceId.length() == 32 ? lowerHexToUnsignedLong(hexTraceId, 0) : 0L;\n    long traceId = lowerHexToUnsignedLong(hexTraceId);\n\n    DataSourceCall<List<Span>> result =\n      dataSourceCallFactory.create(\n        selectFromSpansAndAnnotationsFactory.create(traceIdHigh, traceId));\n    return strictTraceId ? result.map(StrictTraceId.filterSpans(hexTraceId)) : result;\n  }\n\n  @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {\n    Set<String> normalizedTraceIds = new LinkedHashSet<>();\n    Set<Pair> traceIdPairs = new LinkedHashSet<>();\n    for (String traceId : traceIds) {\n      // make sure we have a 16 or 32 character trace ID\n      String hexTraceId = Span.normalizeTraceId(traceId);\n      normalizedTraceIds.add(hexTraceId);\n      traceIdPairs.add(new Pair(\n          hexTraceId.length() == 32 ? lowerHexToUnsignedLong(hexTraceId, 0) : 0L,\n          lowerHexToUnsignedLong(hexTraceId)\n        )\n      );\n    }\n\n    if (traceIdPairs.isEmpty()) return Call.emptyList();\n    Call<List<List<Span>>> result = dataSourceCallFactory\n      .create(selectFromSpansAndAnnotationsFactory.create(traceIdPairs))\n      .map(groupByTraceId);\n\n    return strictTraceId ? result.map(StrictTraceId.filterTraces(normalizedTraceIds)) : result;\n  }\n\n  @Override public Call<List<String>> getServiceNames() {\n    if (!searchEnabled) return Call.emptyList();\n    return getServiceNamesCall.clone();\n  }\n\n  @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {\n    if (serviceName.isEmpty() || !searchEnabled || !schema.hasRemoteServiceName) {\n      return Call.emptyList();\n    }\n    return dataSourceCallFactory.create(new SelectRemoteServiceNames(schema, serviceName));\n  }\n\n  @Override public Call<List<String>> getSpanNames(String serviceName) {\n    if (serviceName.isEmpty() || !searchEnabled) return Call.emptyList();\n    return dataSourceCallFactory.create(new SelectSpanNames(schema, serviceName));\n  }\n\n  @Override public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {\n    if (endTs <= 0) throw new IllegalArgumentException(\"endTs <= 0\");\n    if (lookback <= 0) throw new IllegalArgumentException(\"lookback <= 0\");\n\n    if (schema.hasPreAggregatedDependencies) {\n      return dataSourceCallFactory.create(new SelectDependencies(schema, epochDays(endTs, lookback)));\n    }\n    return dataSourceCallFactory.create(\n      new AggregateDependencies(schema, endTs * 1000 - lookback * 1000, endTs * 1000));\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/MySQLStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Executor;\nimport javax.sql.DataSource;\nimport org.jooq.ExecuteListenerProvider;\nimport org.jooq.conf.Settings;\nimport zipkin2.Call;\nimport zipkin2.CheckResult;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.AutocompleteTags;\nimport zipkin2.storage.ServiceAndSpanNames;\nimport zipkin2.storage.SpanConsumer;\nimport zipkin2.storage.SpanStore;\nimport zipkin2.storage.StorageComponent;\nimport zipkin2.storage.Traces;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\npublic final class MySQLStorage extends StorageComponent {\n  public static Builder newBuilder() {\n    return new Builder();\n  }\n\n  public static final class Builder extends StorageComponent.Builder {\n    boolean strictTraceId = true, searchEnabled = true;\n    private DataSource datasource;\n    private Settings settings = new Settings().withRenderSchema(false);\n    private ExecuteListenerProvider listenerProvider;\n    private Executor executor;\n    List<String> autocompleteKeys = new ArrayList<>();\n\n    @Override public Builder strictTraceId(boolean strictTraceId) {\n      this.strictTraceId = strictTraceId;\n      return this;\n    }\n\n    @Override public Builder searchEnabled(boolean searchEnabled) {\n      this.searchEnabled = searchEnabled;\n      return this;\n    }\n\n    @Override public Builder autocompleteKeys(List<String> keys) {\n      if (keys == null) throw new NullPointerException(\"keys == null\");\n      this.autocompleteKeys = keys;\n      return this;\n    }\n\n    public Builder datasource(DataSource datasource) {\n      if (datasource == null) throw new NullPointerException(\"datasource == null\");\n      this.datasource = datasource;\n      return this;\n    }\n\n    public Builder settings(Settings settings) {\n      if (settings == null) throw new NullPointerException(\"settings == null\");\n      this.settings = settings;\n      return this;\n    }\n\n    public Builder listenerProvider(@Nullable ExecuteListenerProvider listenerProvider) {\n      this.listenerProvider = listenerProvider;\n      return this;\n    }\n\n    public Builder executor(Executor executor) {\n      if (executor == null) throw new NullPointerException(\"executor == null\");\n      this.executor = executor;\n      return this;\n    }\n\n    @Override public MySQLStorage build() {\n      return new MySQLStorage(this);\n    }\n\n    Builder() {\n    }\n  }\n\n  static {\n    System.setProperty(\"org.jooq.no-logo\", \"true\");\n  }\n\n  final DataSource datasource;\n  final DataSourceCall.Factory dataSourceCallFactory;\n  final DSLContexts context;\n  final boolean strictTraceId, searchEnabled;\n  final List<String> autocompleteKeys;\n  volatile Schema schema;\n\n  MySQLStorage(MySQLStorage.Builder builder) {\n    datasource = builder.datasource;\n    if (datasource == null) throw new NullPointerException(\"datasource == null\");\n    Executor executor = builder.executor;\n    if (executor == null) throw new NullPointerException(\"executor == null\");\n    context = new DSLContexts(builder.settings, builder.listenerProvider);\n    dataSourceCallFactory = new DataSourceCall.Factory(datasource, context, executor);\n    strictTraceId = builder.strictTraceId;\n    searchEnabled = builder.searchEnabled;\n    autocompleteKeys = builder.autocompleteKeys;\n  }\n\n  /** Returns the session in use by this storage component. */\n  public DataSource datasource() {\n    return datasource;\n  }\n\n  /** Lazy to avoid eager I/O */\n  Schema schema() {\n    if (schema == null) {\n      synchronized (this) {\n        if (schema == null) {\n          schema = new Schema(datasource, context, strictTraceId);\n        }\n      }\n    }\n    return schema;\n  }\n\n  @Override public SpanStore spanStore() {\n    return new MySQLSpanStore(this, schema());\n  }\n\n  @Override public Traces traces() {\n    return (Traces) spanStore();\n  }\n\n  @Override public ServiceAndSpanNames serviceAndSpanNames() {\n    return (ServiceAndSpanNames) spanStore();\n  }\n\n  @Override public AutocompleteTags autocompleteTags() {\n    return new MySQLAutocompleteTags(this, schema());\n  }\n\n  @Override public SpanConsumer spanConsumer() {\n    return new MySQLSpanConsumer(dataSourceCallFactory, schema());\n  }\n\n  @Override public CheckResult check() {\n    try (Connection conn = datasource.getConnection()) {\n      context.get(conn).select(ZIPKIN_SPANS.TRACE_ID).from(ZIPKIN_SPANS).limit(1).execute();\n    } catch (Throwable e) {\n      Call.propagateIfFatal(e);\n      return CheckResult.failed(e);\n    }\n    return CheckResult.OK;\n  }\n\n  @Override public final String toString() {\n    return \"MySQLStorage{datasource=\" + datasource + \"}\";\n  }\n\n  @Override public void close() {\n    // didn't open the DataSource or executor\n  }\n\n  /** Visible for testing */\n  void clear() {\n    try (Connection conn = datasource.getConnection()) {\n      context.get(conn).truncate(ZIPKIN_SPANS).execute();\n      context.get(conn).truncate(ZIPKIN_ANNOTATIONS).execute();\n      context.get(conn).truncate(ZIPKIN_DEPENDENCIES).execute();\n    } catch (SQLException | RuntimeException e) {\n      throw new AssertionError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/Pair.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nfinal class Pair {\n  final long left, right;\n\n  Pair(long left, long right) {\n    this.left = left;\n    this.right = right;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (o == this) return true;\n    if (!(o instanceof Pair)) return false;\n    Pair that = (Pair) o;\n    return left == that.left && right == that.right;\n  }\n\n  @Override\n  public int hashCode() {\n    int h$ = 1;\n    h$ *= 1000003;\n    h$ ^= (int) (h$ ^ ((left >>> 32) ^ left));\n    h$ *= 1000003;\n    h$ ^= (int) (h$ ^ ((right >>> 32) ^ right));\n    return h$;\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/PeekingIterator.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/** adapted from guava's {@code com.google.common.collect.AbstractIterator}. */\nclass PeekingIterator<T> implements Iterator<T> {\n\n  private final Iterator<T> delegate;\n  private PeekingIterator.State state = State.NOT_READY;\n  private T next;\n\n  PeekingIterator(Iterator<T> delegate) {\n    if (delegate == null) throw new NullPointerException(\"delegate == null\");\n    this.delegate = delegate;\n  }\n\n  protected T computeNext() {\n    if (delegate.hasNext()) {\n      return delegate.next();\n    }\n    return endOfData();\n  }\n\n  protected final T endOfData() {\n    state = State.DONE;\n    return null;\n  }\n\n  @Override\n  public final boolean hasNext() {\n    switch (state) {\n      case DONE:\n        return false;\n      case READY:\n        return true;\n      default:\n    }\n    return tryToComputeNext();\n  }\n\n  private boolean tryToComputeNext() {\n    next = computeNext();\n    if (state != State.DONE) {\n      state = State.READY;\n      return true;\n    }\n    return false;\n  }\n\n  @Override\n  public final T next() {\n    if (!hasNext()) {\n      throw new NoSuchElementException();\n    }\n    state = State.NOT_READY;\n    return next;\n  }\n\n  public T peek() {\n    if (!hasNext()) {\n      throw new NoSuchElementException();\n    }\n    return next;\n  }\n\n  @Override\n  public void remove() {\n    throw new UnsupportedOperationException();\n  }\n\n  private enum State {\n    /** We have computed the next element and haven't returned it yet. */\n    READY,\n\n    /** We haven't yet computed or have already returned the element. */\n    NOT_READY,\n\n    /** We have reached the end of the data and are finished. */\n    DONE,\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/Schema.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport javax.sql.DataSource;\nimport org.jooq.Condition;\nimport org.jooq.Field;\nimport org.jooq.Record;\nimport org.jooq.Result;\nimport org.jooq.Row2;\nimport org.jooq.SelectOffsetStep;\nimport org.jooq.TableField;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\n\nimport static org.jooq.impl.DSL.row;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class Schema {\n  final List<Field<?>> spanIdFields;\n  final List<Field<?>> spanFields;\n  final List<Field<?>> annotationFields;\n  final List<Field<?>> dependencyLinkerFields;\n  final List<Field<?>> dependencyLinkerGroupByFields;\n  final List<Field<?>> dependencyLinkFields;\n  final boolean hasTraceIdHigh;\n  final boolean hasPreAggregatedDependencies;\n  final boolean hasIpv6;\n  final boolean hasErrorCount;\n  final boolean hasRemoteServiceName;\n  final boolean strictTraceId;\n\n  Schema(DataSource datasource, DSLContexts context, boolean strictTraceId) {\n    hasTraceIdHigh = HasTraceIdHigh.test(datasource, context);\n    hasPreAggregatedDependencies = HasPreAggregatedDependencies.test(datasource, context);\n    hasIpv6 = HasIpv6.test(datasource, context);\n    hasErrorCount = HasErrorCount.test(datasource, context);\n    hasRemoteServiceName = HasRemoteServiceName.test(datasource, context);\n    this.strictTraceId = strictTraceId;\n\n    spanIdFields = list(ZIPKIN_SPANS.TRACE_ID_HIGH, ZIPKIN_SPANS.TRACE_ID);\n    spanFields = list(ZIPKIN_SPANS.fields());\n    spanIdFields.remove(ZIPKIN_SPANS.REMOTE_SERVICE_NAME); // not used to recreate the span\n    annotationFields = list(ZIPKIN_ANNOTATIONS.fields());\n    dependencyLinkFields = list(ZIPKIN_DEPENDENCIES.fields());\n    dependencyLinkerFields =\n        list(\n            ZIPKIN_SPANS.TRACE_ID_HIGH,\n            ZIPKIN_SPANS.TRACE_ID,\n            ZIPKIN_SPANS.PARENT_ID,\n            ZIPKIN_SPANS.ID,\n            ZIPKIN_ANNOTATIONS.A_KEY,\n            ZIPKIN_ANNOTATIONS.A_TYPE,\n            ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME);\n    dependencyLinkerGroupByFields = new ArrayList<>(dependencyLinkerFields);\n    dependencyLinkerGroupByFields.remove(ZIPKIN_SPANS.PARENT_ID);\n    if (!hasTraceIdHigh) {\n      spanIdFields.remove(ZIPKIN_SPANS.TRACE_ID_HIGH);\n      spanFields.remove(ZIPKIN_SPANS.TRACE_ID_HIGH);\n      annotationFields.remove(ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH);\n      dependencyLinkerFields.remove(ZIPKIN_SPANS.TRACE_ID_HIGH);\n      dependencyLinkerGroupByFields.remove(ZIPKIN_SPANS.TRACE_ID_HIGH);\n    }\n    if (!hasIpv6) {\n      annotationFields.remove(ZIPKIN_ANNOTATIONS.ENDPOINT_IPV6);\n    }\n    if (!hasErrorCount) {\n      dependencyLinkFields.remove(ZIPKIN_DEPENDENCIES.ERROR_COUNT);\n    }\n  }\n\n  Condition joinCondition(ZipkinAnnotations annotationTable) {\n    if (hasTraceIdHigh) {\n      return ZIPKIN_SPANS\n          .TRACE_ID_HIGH\n          .eq(annotationTable.TRACE_ID_HIGH)\n          .and(ZIPKIN_SPANS.TRACE_ID.eq(annotationTable.TRACE_ID))\n          .and(ZIPKIN_SPANS.ID.eq(annotationTable.SPAN_ID));\n    } else {\n      return ZIPKIN_SPANS\n          .TRACE_ID\n          .eq(annotationTable.TRACE_ID)\n          .and(ZIPKIN_SPANS.ID.eq(annotationTable.SPAN_ID));\n    }\n  }\n\n  /** Returns a mutable list */\n  static <T> List<T> list(T... elements) {\n    return new ArrayList<>(Arrays.asList(elements));\n  }\n\n  Condition spanTraceIdCondition(SelectOffsetStep<? extends Record> traceIdQuery) {\n    if (hasTraceIdHigh && strictTraceId) {\n      Result<? extends Record> result = traceIdQuery.fetch();\n      List<Row2<Long, Long>> traceIds = new ArrayList<>(result.size());\n      for (Record r : result) {\n        traceIds.add(row(r.get(ZIPKIN_SPANS.TRACE_ID_HIGH), r.get(ZIPKIN_SPANS.TRACE_ID)));\n      }\n      return row(ZIPKIN_SPANS.TRACE_ID_HIGH, ZIPKIN_SPANS.TRACE_ID).in(traceIds);\n    } else {\n      List<Long> traceIds = traceIdQuery.fetch(ZIPKIN_SPANS.TRACE_ID);\n      return ZIPKIN_SPANS.TRACE_ID.in(traceIds);\n    }\n  }\n\n  Condition spanTraceIdCondition(long traceIdHigh, long traceIdLow) {\n    return traceIdHigh != 0L && hasTraceIdHigh\n        ? row(ZIPKIN_SPANS.TRACE_ID_HIGH, ZIPKIN_SPANS.TRACE_ID).eq(traceIdHigh, traceIdLow)\n        : ZIPKIN_SPANS.TRACE_ID.eq(traceIdLow);\n  }\n\n  Condition spanTraceIdCondition(Set<Pair> traceIds) {\n    return traceIdCondition(ZIPKIN_SPANS.TRACE_ID_HIGH, ZIPKIN_SPANS.TRACE_ID, traceIds);\n  }\n\n  Condition annotationsTraceIdCondition(Set<Pair> traceIds) {\n    return traceIdCondition(ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, ZIPKIN_ANNOTATIONS.TRACE_ID, traceIds);\n  }\n\n  Condition traceIdCondition(\n    TableField<Record, Long> TRACE_ID_HIGH,\n    TableField<Record, Long> TRACE_ID, Set<Pair> traceIds\n  ) {\n    boolean hasTraceIdHigh = false;\n    for (Pair traceId : traceIds) {\n      if (traceId.left != 0) {\n        hasTraceIdHigh = true;\n        break;\n      }\n    }\n    if (hasTraceIdHigh && strictTraceId) {\n      Row2[] result = new Row2[traceIds.size()];\n      int i = 0;\n      for (Pair traceId128 : traceIds) {\n        result[i++] = row(traceId128.left, traceId128.right);\n      }\n      return row(TRACE_ID_HIGH, TRACE_ID).in(result);\n    } else {\n      Long[] result = new Long[traceIds.size()];\n      int i = 0;\n      for (Pair traceId128 : traceIds) {\n        result[i++] = traceId128.right;\n      }\n      return TRACE_ID.in(result);\n    }\n  }\n\n  /** returns the default value if the column doesn't exist or the result was null */\n  static <T> T maybeGet(Record record, TableField<Record, T> field, T defaultValue) {\n    if (record.fieldsRow().indexOf(field) < 0) {\n      return defaultValue;\n    } else {\n      T result = record.get(field);\n      return result != null ? result : defaultValue;\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectAnnotationServiceNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.Condition;\nimport org.jooq.DSLContext;\nimport zipkin2.v1.V1BinaryAnnotation;\n\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\nfinal class SelectAnnotationServiceNames implements Function<DSLContext, List<String>> {\n  @Override public List<String> apply(DSLContext context) {\n    return context\n      .selectDistinct(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME)\n      .from(ZIPKIN_ANNOTATIONS)\n      .where(localServiceNameCondition())\n      .orderBy(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME)\n      .fetch(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME);\n  }\n\n  static Condition localServiceNameCondition() {\n    return ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.isNotNull()\n      .and(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.ne(\"\")) // exclude address annotations\n      .and(ZIPKIN_ANNOTATIONS.A_TYPE.ne(V1BinaryAnnotation.TYPE_BOOLEAN));\n  }\n\n  @Override public String toString() {\n    return \"SelectAnnotationServiceNames{}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectAutocompleteValues.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.Converter;\nimport org.jooq.DSLContext;\nimport zipkin2.v1.V1BinaryAnnotation;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\nfinal class SelectAutocompleteValues implements Function<DSLContext, List<String>> {\n  final Schema schema;\n  final String autocompleteKey;\n\n  SelectAutocompleteValues(Schema schema, String autocompleteKey) {\n    this.schema = schema;\n    this.autocompleteKey = autocompleteKey;\n  }\n\n  @Override public List<String> apply(DSLContext context) {\n    return context.selectDistinct(ZIPKIN_ANNOTATIONS.A_VALUE)\n      .from(ZIPKIN_ANNOTATIONS)\n      .where(ZIPKIN_ANNOTATIONS.A_TYPE.eq(V1BinaryAnnotation.TYPE_STRING)\n        .and(ZIPKIN_ANNOTATIONS.A_KEY.eq(autocompleteKey)))\n      .fetch(ZIPKIN_ANNOTATIONS.A_VALUE, STRING_CONVERTER);\n  }\n\n  static final Converter<byte[], String> STRING_CONVERTER = new Converter<byte[], String>() {\n    @Override public String from(byte[] bytes) {\n      return new String(bytes, UTF_8);\n    }\n\n    @Override public byte[] to(String input) {\n      return input.getBytes(UTF_8);\n    }\n\n    @Override public Class<byte[]> fromType() {\n      return byte[].class;\n    }\n\n    @Override public Class<String> toType() {\n      return String.class;\n    }\n  };\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectDependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.DSLContext;\nimport org.jooq.Record;\nimport zipkin2.DependencyLink;\nimport zipkin2.internal.DependencyLinker;\n\nimport static zipkin2.storage.mysql.v1.Schema.maybeGet;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\nfinal class SelectDependencies implements Function<DSLContext, List<DependencyLink>> {\n  final Schema schema;\n  final List<Long> epochDays;\n\n  SelectDependencies(Schema schema, List<Long> epochDays) {\n    this.schema = schema;\n    this.epochDays = epochDays;\n  }\n\n  @Override\n  public List<DependencyLink> apply(DSLContext context) {\n    List<DependencyLink> unmerged =\n        context\n            .select(schema.dependencyLinkFields)\n            .from(ZIPKIN_DEPENDENCIES)\n            .where(ZIPKIN_DEPENDENCIES.DAY.in(epochDays))\n            .fetch(\n                (Record l) ->\n                    DependencyLink.newBuilder()\n                        .parent(l.get(ZIPKIN_DEPENDENCIES.PARENT))\n                        .child(l.get(ZIPKIN_DEPENDENCIES.CHILD))\n                        .callCount(l.get(ZIPKIN_DEPENDENCIES.CALL_COUNT))\n                        .errorCount(maybeGet(l, ZIPKIN_DEPENDENCIES.ERROR_COUNT, 0L))\n                        .build());\n    return DependencyLinker.merge(unmerged);\n  }\n\n  @Override\n  public String toString() {\n    return \"SelectDependencies{epochDays=\" + epochDays + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectRemoteServiceNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.DSLContext;\n\nimport static zipkin2.storage.mysql.v1.SelectAnnotationServiceNames.localServiceNameCondition;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class SelectRemoteServiceNames implements Function<DSLContext, List<String>> {\n  final Schema schema;\n  final String serviceName;\n\n  SelectRemoteServiceNames(Schema schema, String serviceName) {\n    this.schema = schema;\n    this.serviceName = serviceName;\n  }\n\n  @Override\n  public List<String> apply(DSLContext context) {\n    return context\n      .selectDistinct(ZIPKIN_SPANS.REMOTE_SERVICE_NAME)\n      .from(ZIPKIN_SPANS)\n      .join(ZIPKIN_ANNOTATIONS)\n      .on(schema.joinCondition(ZIPKIN_ANNOTATIONS))\n      .where(\n        localServiceNameCondition().and(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.eq(serviceName)))\n      .and(ZIPKIN_SPANS.REMOTE_SERVICE_NAME.notEqual(\"\"))\n      .orderBy(ZIPKIN_SPANS.REMOTE_SERVICE_NAME)\n      .fetch(ZIPKIN_SPANS.REMOTE_SERVICE_NAME);\n  }\n\n  @Override\n  public String toString() {\n    return \"SelectRemoteServiceNames{serviceName=\" + serviceName + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectSpanNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport org.jooq.DSLContext;\n\nimport static zipkin2.storage.mysql.v1.SelectAnnotationServiceNames.localServiceNameCondition;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nfinal class SelectSpanNames implements Function<DSLContext, List<String>> {\n  final Schema schema;\n  final String serviceName;\n\n  SelectSpanNames(Schema schema, String serviceName) {\n    this.schema = schema;\n    this.serviceName = serviceName;\n  }\n\n  @Override\n  public List<String> apply(DSLContext context) {\n    return context\n      .selectDistinct(ZIPKIN_SPANS.NAME)\n      .from(ZIPKIN_SPANS)\n      .join(ZIPKIN_ANNOTATIONS)\n      .on(schema.joinCondition(ZIPKIN_ANNOTATIONS))\n      .where(\n        localServiceNameCondition().and(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.eq(serviceName)))\n      .and(ZIPKIN_SPANS.NAME.notEqual(\"\"))\n      .orderBy(ZIPKIN_SPANS.NAME)\n      .fetch(ZIPKIN_SPANS.NAME);\n  }\n\n  @Override\n  public String toString() {\n    return \"SelectSpanNames{serviceName=\" + serviceName + \"}\";\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/SelectSpansAndAnnotations.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.jooq.Condition;\nimport org.jooq.DSLContext;\nimport org.jooq.Record;\nimport org.jooq.Row3;\nimport org.jooq.SelectConditionStep;\nimport org.jooq.SelectField;\nimport org.jooq.SelectOffsetStep;\nimport org.jooq.TableOnConditionStep;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.internal.Nullable;\nimport zipkin2.storage.QueryRequest;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.v1.V1BinaryAnnotation;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.stream.Collectors.groupingBy;\nimport static org.jooq.impl.DSL.max;\nimport static org.jooq.impl.DSL.row;\nimport static zipkin2.storage.mysql.v1.Schema.maybeGet;\nimport static zipkin2.storage.mysql.v1.SelectAnnotationServiceNames.localServiceNameCondition;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS;\n\nabstract class SelectSpansAndAnnotations implements Function<DSLContext, List<Span>> {\n  static final class Factory {\n    final Schema schema;\n    final boolean strictTraceId;\n\n    Factory(Schema schema, boolean strictTraceId) {\n      this.schema = schema;\n      this.strictTraceId = strictTraceId;\n    }\n\n    SelectSpansAndAnnotations create(long traceIdHigh, long traceIdLow) {\n      if (traceIdHigh != 0L && !strictTraceId) traceIdHigh = 0L;\n      long finalTraceIdHigh = traceIdHigh;\n      return new SelectSpansAndAnnotations(schema) {\n        @Override\n        Condition traceIdCondition(DSLContext context) {\n          return schema.spanTraceIdCondition(finalTraceIdHigh, traceIdLow);\n        }\n      };\n    }\n\n    SelectSpansAndAnnotations create(Set<Pair> traceIdPairs) {\n      return new SelectSpansAndAnnotations(schema) {\n        @Override Condition traceIdCondition(DSLContext context) {\n          return schema.spanTraceIdCondition(traceIdPairs);\n        }\n      };\n    }\n\n    SelectSpansAndAnnotations create(QueryRequest request) {\n      if (request.remoteServiceName() != null && !schema.hasRemoteServiceName) {\n        throw new IllegalArgumentException(\"remoteService=\" + request.remoteServiceName()\n          + \" unsupported due to missing column zipkin_spans.remote_service_name\");\n      }\n      return new SelectSpansAndAnnotations(schema) {\n        @Override\n        Condition traceIdCondition(DSLContext context) {\n          return schema.spanTraceIdCondition(toTraceIdQuery(context, request));\n        }\n      };\n    }\n  }\n\n  final Schema schema;\n\n  SelectSpansAndAnnotations(Schema schema) {\n    this.schema = schema;\n  }\n\n  abstract Condition traceIdCondition(DSLContext context);\n\n  @Override\n  public List<Span> apply(DSLContext context) {\n    final Map<Pair, List<V1Span.Builder>> spansWithoutAnnotations;\n    final Map<Row3<Long, Long, Long>, List<Record>> dbAnnotations;\n\n    spansWithoutAnnotations =\n      context\n        .select(schema.spanFields)\n        .from(ZIPKIN_SPANS)\n        .where(traceIdCondition(context))\n        .stream()\n        .map(\n          r ->\n            V1Span.newBuilder()\n              .traceIdHigh(maybeGet(r, ZIPKIN_SPANS.TRACE_ID_HIGH, 0L))\n              .traceId(r.getValue(ZIPKIN_SPANS.TRACE_ID))\n              .name(r.getValue(ZIPKIN_SPANS.NAME))\n              .id(r.getValue(ZIPKIN_SPANS.ID))\n              .parentId(maybeGet(r, ZIPKIN_SPANS.PARENT_ID, 0L))\n              .timestamp(maybeGet(r, ZIPKIN_SPANS.START_TS, 0L))\n              .duration(maybeGet(r, ZIPKIN_SPANS.DURATION, 0L))\n              .debug(r.getValue(ZIPKIN_SPANS.DEBUG)))\n        .collect(\n          groupingBy(\n            s -> new Pair(s.traceIdHigh(), s.traceId()),\n            LinkedHashMap::new,\n            Collectors.toList()));\n\n    dbAnnotations =\n      context\n        .select(schema.annotationFields)\n        .from(ZIPKIN_ANNOTATIONS)\n        .where(schema.annotationsTraceIdCondition(spansWithoutAnnotations.keySet()))\n        .orderBy(ZIPKIN_ANNOTATIONS.A_TIMESTAMP.asc(), ZIPKIN_ANNOTATIONS.A_KEY.asc())\n        .stream()\n        .collect(\n          groupingBy(\n            (Record a) ->\n              row(\n                maybeGet(a, ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, 0L),\n                a.getValue(ZIPKIN_ANNOTATIONS.TRACE_ID),\n                a.getValue(ZIPKIN_ANNOTATIONS.SPAN_ID)),\n            LinkedHashMap::new,\n            Collectors.toList())); // LinkedHashMap preserves order while grouping\n\n    V1SpanConverter converter = V1SpanConverter.create();\n    List<Span> allSpans = new ArrayList<>(spansWithoutAnnotations.size());\n    for (List<V1Span.Builder> spans : spansWithoutAnnotations.values()) {\n      for (V1Span.Builder span : spans) {\n        Row3<Long, Long, Long> key = row(span.traceIdHigh(), span.traceId(), span.id());\n        if (dbAnnotations.containsKey(key)) {\n          for (Record a : dbAnnotations.get(key)) {\n            Endpoint endpoint = endpoint(a);\n            processAnnotationRecord(a, span, endpoint);\n          }\n        }\n        converter.convert(span.build(), allSpans);\n      }\n    }\n    return allSpans;\n  }\n\n  static void processAnnotationRecord(Record a, V1Span.Builder span, @Nullable Endpoint endpoint) {\n    Integer type = a.getValue(ZIPKIN_ANNOTATIONS.A_TYPE);\n    if (type == null) return;\n    if (type == -1) {\n      span.addAnnotation(\n        a.getValue(ZIPKIN_ANNOTATIONS.A_TIMESTAMP),\n        a.getValue(ZIPKIN_ANNOTATIONS.A_KEY),\n        endpoint);\n    } else {\n      switch (type) {\n        case V1BinaryAnnotation.TYPE_STRING:\n          span.addBinaryAnnotation(\n            a.getValue(ZIPKIN_ANNOTATIONS.A_KEY),\n            new String(a.getValue(ZIPKIN_ANNOTATIONS.A_VALUE), UTF_8),\n            endpoint);\n          break;\n        case V1BinaryAnnotation.TYPE_BOOLEAN:\n          // address annotations require an endpoint\n          if (endpoint == null) break;\n          String aKey = a.getValue(ZIPKIN_ANNOTATIONS.A_KEY);\n          // ensure we are only processing address annotations\n          if (!aKey.equals(\"sa\") && !aKey.equals(\"ca\") && !aKey.equals(\"ma\")) break;\n          byte[] value = a.getValue(ZIPKIN_ANNOTATIONS.A_VALUE);\n          // address annotations are a single byte of 1\n          if (value == null || value.length != 1 || value[0] != 1) break;\n          span.addBinaryAnnotation(a.getValue(ZIPKIN_ANNOTATIONS.A_KEY), endpoint);\n          break;\n        default:\n          // other values unsupported\n      }\n    }\n  }\n\n  SelectOffsetStep<? extends Record> toTraceIdQuery(DSLContext context, QueryRequest request) {\n    long endTs = request.endTs() * 1000;\n\n    TableOnConditionStep<?> table =\n      ZIPKIN_SPANS.join(ZIPKIN_ANNOTATIONS).on(schema.joinCondition(ZIPKIN_ANNOTATIONS));\n\n    int i = 0;\n    for (Map.Entry<String, String> kv : request.annotationQuery().entrySet()) {\n      ZipkinAnnotations aTable = ZIPKIN_ANNOTATIONS.as(\"a\" + i++);\n      if (kv.getValue().isEmpty()) {\n        table =\n          maybeOnService(\n            table\n              .join(aTable)\n              .on(schema.joinCondition(aTable))\n              .and(aTable.A_KEY.eq(kv.getKey())),\n            aTable,\n            request.serviceName());\n      } else {\n        table =\n          maybeOnService(\n            table\n              .join(aTable)\n              .on(schema.joinCondition(aTable))\n              .and(aTable.A_TYPE.eq(V1BinaryAnnotation.TYPE_STRING))\n              .and(aTable.A_KEY.eq(kv.getKey()))\n              .and(aTable.A_VALUE.eq(kv.getValue().getBytes(UTF_8))),\n            aTable,\n            request.serviceName());\n      }\n    }\n\n    List<SelectField<?>> distinctFields = new ArrayList<>(schema.spanIdFields);\n    distinctFields.add(max(ZIPKIN_SPANS.START_TS));\n    SelectConditionStep<Record> dsl = context.selectDistinct(distinctFields)\n      .from(table)\n      .where(ZIPKIN_SPANS.START_TS.between(endTs - request.lookback() * 1000, endTs));\n\n    if (request.serviceName() != null) {\n      dsl = dsl.and(localServiceNameCondition()\n        .and(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.eq(request.serviceName())));\n    }\n\n    if (request.remoteServiceName() != null) {\n      dsl = dsl.and(ZIPKIN_SPANS.REMOTE_SERVICE_NAME.eq(request.remoteServiceName()));\n    }\n\n    if (request.spanName() != null) {\n      dsl = dsl.and(ZIPKIN_SPANS.NAME.eq(request.spanName()));\n    }\n\n    if (request.minDuration() != null && request.maxDuration() != null) {\n      dsl = dsl.and(ZIPKIN_SPANS.DURATION.between(request.minDuration(), request.maxDuration()));\n    } else if (request.minDuration() != null) {\n      dsl = dsl.and(ZIPKIN_SPANS.DURATION.greaterOrEqual(request.minDuration()));\n    }\n    return dsl.groupBy(schema.spanIdFields)\n      .orderBy(max(ZIPKIN_SPANS.START_TS).desc())\n      .limit(request.limit());\n  }\n\n  static TableOnConditionStep<?> maybeOnService(\n    TableOnConditionStep<Record> table, ZipkinAnnotations aTable, String serviceName) {\n    if (serviceName == null) return table;\n    return table.and(aTable.ENDPOINT_SERVICE_NAME.eq(serviceName));\n  }\n\n  static Endpoint endpoint(Record a) {\n    Endpoint.Builder result =\n      Endpoint.newBuilder()\n        .serviceName(a.getValue(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME))\n        .port(Schema.maybeGet(a, ZIPKIN_ANNOTATIONS.ENDPOINT_PORT, (short) 0));\n    int ipv4 = maybeGet(a, ZIPKIN_ANNOTATIONS.ENDPOINT_IPV4, 0);\n    if (ipv4 != 0) {\n      result.parseIp( // allocation is ok here as Endpoint.ipv4Bytes would anyway\n        new byte[] {\n          (byte) (ipv4 >> 24 & 0xff),\n          (byte) (ipv4 >> 16 & 0xff),\n          (byte) (ipv4 >> 8 & 0xff),\n          (byte) (ipv4 & 0xff)\n        });\n    }\n    result.parseIp(Schema.maybeGet(a, ZIPKIN_ANNOTATIONS.ENDPOINT_IPV6, null));\n    return result.build();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/DefaultCatalog.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated;\n\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.jooq.Constants;\nimport org.jooq.Schema;\nimport org.jooq.impl.CatalogImpl;\n\n\n/**\n * This class is generated by jOOQ.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class DefaultCatalog extends CatalogImpl {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * The reference instance of <code>DEFAULT_CATALOG</code>\n     */\n    public static final DefaultCatalog DEFAULT_CATALOG = new DefaultCatalog();\n\n    /**\n     * The schema <code>zipkin</code>.\n     */\n    public final Zipkin ZIPKIN = Zipkin.ZIPKIN;\n\n    /**\n     * No further instances allowed\n     */\n    private DefaultCatalog() {\n        super(\"\");\n    }\n\n    @Override\n    public final List<Schema> getSchemas() {\n        return Arrays.asList(\n            Zipkin.ZIPKIN\n        );\n    }\n\n    /**\n     * A reference to the 3.19 minor release of the code generator. If this\n     * doesn't compile, it's because the runtime library uses an older minor\n     * release, namely: 3.19. You can turn off the generation of this reference\n     * by specifying /configuration/generator/generate/jooqVersionReference\n     */\n    private static final String REQUIRE_RUNTIME_JOOQ_VERSION = Constants.VERSION_3_19;\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/Indexes.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated;\n\n\nimport org.jooq.Index;\nimport org.jooq.OrderField;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.Internal;\n\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\n\n\n/**\n * A class modelling indexes of tables in zipkin.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class Indexes {\n\n    // -------------------------------------------------------------------------\n    // INDEX definitions\n    // -------------------------------------------------------------------------\n\n    public static final Index ZIPKIN_ANNOTATIONS_A_KEY = Internal.createIndex(DSL.name(\"a_key\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_KEY }, false);\n    public static final Index ZIPKIN_ANNOTATIONS_A_TYPE = Internal.createIndex(DSL.name(\"a_type\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_TYPE }, false);\n    public static final Index ZIPKIN_ANNOTATIONS_ENDPOINT_SERVICE_NAME = Internal.createIndex(DSL.name(\"endpoint_service_name\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME }, false);\n    public static final Index ZIPKIN_SPANS_NAME = Internal.createIndex(DSL.name(\"name\"), ZipkinSpans.ZIPKIN_SPANS, new OrderField[] { ZipkinSpans.ZIPKIN_SPANS.NAME }, false);\n    public static final Index ZIPKIN_SPANS_REMOTE_SERVICE_NAME = Internal.createIndex(DSL.name(\"remote_service_name\"), ZipkinSpans.ZIPKIN_SPANS, new OrderField[] { ZipkinSpans.ZIPKIN_SPANS.REMOTE_SERVICE_NAME }, false);\n    public static final Index ZIPKIN_SPANS_START_TS = Internal.createIndex(DSL.name(\"start_ts\"), ZipkinSpans.ZIPKIN_SPANS, new OrderField[] { ZipkinSpans.ZIPKIN_SPANS.START_TS }, false);\n    public static final Index ZIPKIN_ANNOTATIONS_TRACE_ID = Internal.createIndex(DSL.name(\"trace_id\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.SPAN_ID, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_KEY }, false);\n    public static final Index ZIPKIN_SPANS_TRACE_ID_HIGH = Internal.createIndex(DSL.name(\"trace_id_high\"), ZipkinSpans.ZIPKIN_SPANS, new OrderField[] { ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH, ZipkinSpans.ZIPKIN_SPANS.TRACE_ID }, false);\n    public static final Index ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH_2 = Internal.createIndex(DSL.name(\"trace_id_high_2\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.SPAN_ID }, false);\n    public static final Index ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH_3 = Internal.createIndex(DSL.name(\"trace_id_high_3\"), ZipkinAnnotations.ZIPKIN_ANNOTATIONS, new OrderField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID }, false);\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/Keys.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated;\n\n\nimport org.jooq.Record;\nimport org.jooq.TableField;\nimport org.jooq.UniqueKey;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.Internal;\n\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\n\n\n/**\n * A class modelling foreign key relationships and constraints of tables in\n * zipkin.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class Keys {\n\n    // -------------------------------------------------------------------------\n    // UNIQUE and PRIMARY KEY definitions\n    // -------------------------------------------------------------------------\n\n    public static final UniqueKey<Record> KEY_ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH = Internal.createUniqueKey(ZipkinAnnotations.ZIPKIN_ANNOTATIONS, DSL.name(\"KEY_zipkin_annotations_trace_id_high\"), new TableField[] { ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID_HIGH, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.TRACE_ID, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.SPAN_ID, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_KEY, ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_TIMESTAMP }, true);\n    public static final UniqueKey<Record> KEY_ZIPKIN_DEPENDENCIES_PRIMARY = Internal.createUniqueKey(ZipkinDependencies.ZIPKIN_DEPENDENCIES, DSL.name(\"KEY_zipkin_dependencies_PRIMARY\"), new TableField[] { ZipkinDependencies.ZIPKIN_DEPENDENCIES.DAY, ZipkinDependencies.ZIPKIN_DEPENDENCIES.PARENT, ZipkinDependencies.ZIPKIN_DEPENDENCIES.CHILD }, true);\n    public static final UniqueKey<Record> KEY_ZIPKIN_SPANS_PRIMARY = Internal.createUniqueKey(ZipkinSpans.ZIPKIN_SPANS, DSL.name(\"KEY_zipkin_spans_PRIMARY\"), new TableField[] { ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH, ZipkinSpans.ZIPKIN_SPANS.TRACE_ID, ZipkinSpans.ZIPKIN_SPANS.ID }, true);\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/Tables.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated;\n\n\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\n\n\n/**\n * Convenience access to all tables in zipkin.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class Tables {\n\n    /**\n     * The table <code>zipkin.zipkin_annotations</code>.\n     */\n    public static final ZipkinAnnotations ZIPKIN_ANNOTATIONS = ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\n    /**\n     * The table <code>zipkin.zipkin_dependencies</code>.\n     */\n    public static final ZipkinDependencies ZIPKIN_DEPENDENCIES = ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\n    /**\n     * The table <code>zipkin.zipkin_spans</code>.\n     */\n    public static final ZipkinSpans ZIPKIN_SPANS = ZipkinSpans.ZIPKIN_SPANS;\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/Zipkin.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated;\n\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.jooq.Catalog;\nimport org.jooq.Table;\nimport org.jooq.impl.SchemaImpl;\n\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\n\n\n/**\n * This class is generated by jOOQ.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class Zipkin extends SchemaImpl {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * The reference instance of <code>zipkin</code>\n     */\n    public static final Zipkin ZIPKIN = new Zipkin();\n\n    /**\n     * The table <code>zipkin.zipkin_annotations</code>.\n     */\n    public final ZipkinAnnotations ZIPKIN_ANNOTATIONS = ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\n    /**\n     * The table <code>zipkin.zipkin_dependencies</code>.\n     */\n    public final ZipkinDependencies ZIPKIN_DEPENDENCIES = ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\n    /**\n     * The table <code>zipkin.zipkin_spans</code>.\n     */\n    public final ZipkinSpans ZIPKIN_SPANS = ZipkinSpans.ZIPKIN_SPANS;\n\n    /**\n     * No further instances allowed\n     */\n    private Zipkin() {\n        super(\"zipkin\", null);\n    }\n\n\n    @Override\n    public Catalog getCatalog() {\n        return DefaultCatalog.DEFAULT_CATALOG;\n    }\n\n    @Override\n    public final List<Table<?>> getTables() {\n        return Arrays.asList(\n            ZipkinAnnotations.ZIPKIN_ANNOTATIONS,\n            ZipkinDependencies.ZIPKIN_DEPENDENCIES,\n            ZipkinSpans.ZIPKIN_SPANS\n        );\n    }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/tables/ZipkinAnnotations.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated.tables;\n\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.jooq.Condition;\nimport org.jooq.Field;\nimport org.jooq.Index;\nimport org.jooq.Name;\nimport org.jooq.PlainSQL;\nimport org.jooq.QueryPart;\nimport org.jooq.Record;\nimport org.jooq.SQL;\nimport org.jooq.Schema;\nimport org.jooq.Select;\nimport org.jooq.Stringly;\nimport org.jooq.Table;\nimport org.jooq.TableField;\nimport org.jooq.TableOptions;\nimport org.jooq.UniqueKey;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.SQLDataType;\nimport org.jooq.impl.TableImpl;\n\nimport zipkin2.storage.mysql.v1.internal.generated.Indexes;\nimport zipkin2.storage.mysql.v1.internal.generated.Keys;\nimport zipkin2.storage.mysql.v1.internal.generated.Zipkin;\n\n\n/**\n * This class is generated by jOOQ.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class ZipkinAnnotations extends TableImpl<Record> {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * The reference instance of <code>zipkin.zipkin_annotations</code>\n     */\n    public static final ZipkinAnnotations ZIPKIN_ANNOTATIONS = new ZipkinAnnotations();\n\n    /**\n     * The class holding records for this type\n     */\n    @Override\n    public Class<Record> getRecordType() {\n        return Record.class;\n    }\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.trace_id_high</code>. If non\n     * zero, this means the trace uses 128 bit traceIds instead of 64 bit\n     */\n    public final TableField<Record, Long> TRACE_ID_HIGH = createField(DSL.name(\"trace_id_high\"), SQLDataType.BIGINT.nullable(false).defaultValue(DSL.inline(\"0\", SQLDataType.BIGINT)), this, \"If non zero, this means the trace uses 128 bit traceIds instead of 64 bit\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.trace_id</code>. coincides\n     * with zipkin_spans.trace_id\n     */\n    public final TableField<Record, Long> TRACE_ID = createField(DSL.name(\"trace_id\"), SQLDataType.BIGINT.nullable(false), this, \"coincides with zipkin_spans.trace_id\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.span_id</code>. coincides with\n     * zipkin_spans.id\n     */\n    public final TableField<Record, Long> SPAN_ID = createField(DSL.name(\"span_id\"), SQLDataType.BIGINT.nullable(false), this, \"coincides with zipkin_spans.id\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.a_key</code>.\n     * BinaryAnnotation.key or Annotation.value if type == -1\n     */\n    public final TableField<Record, String> A_KEY = createField(DSL.name(\"a_key\"), SQLDataType.VARCHAR(255).nullable(false), this, \"BinaryAnnotation.key or Annotation.value if type == -1\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.a_value</code>.\n     * BinaryAnnotation.value(), which must be smaller than 64KB\n     */\n    public final TableField<Record, byte[]> A_VALUE = createField(DSL.name(\"a_value\"), SQLDataType.BLOB, this, \"BinaryAnnotation.value(), which must be smaller than 64KB\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.a_type</code>.\n     * BinaryAnnotation.type() or -1 if Annotation\n     */\n    public final TableField<Record, Integer> A_TYPE = createField(DSL.name(\"a_type\"), SQLDataType.INTEGER.nullable(false), this, \"BinaryAnnotation.type() or -1 if Annotation\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.a_timestamp</code>. Used to\n     * implement TTL; Annotation.timestamp or zipkin_spans.timestamp\n     */\n    public final TableField<Record, Long> A_TIMESTAMP = createField(DSL.name(\"a_timestamp\"), SQLDataType.BIGINT, this, \"Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.endpoint_ipv4</code>. Null\n     * when Binary/Annotation.endpoint is null\n     */\n    public final TableField<Record, Integer> ENDPOINT_IPV4 = createField(DSL.name(\"endpoint_ipv4\"), SQLDataType.INTEGER, this, \"Null when Binary/Annotation.endpoint is null\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.endpoint_ipv6</code>. Null\n     * when Binary/Annotation.endpoint is null, or no IPv6 address\n     */\n    public final TableField<Record, byte[]> ENDPOINT_IPV6 = createField(DSL.name(\"endpoint_ipv6\"), SQLDataType.BINARY(16), this, \"Null when Binary/Annotation.endpoint is null, or no IPv6 address\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.endpoint_port</code>. Null\n     * when Binary/Annotation.endpoint is null\n     */\n    public final TableField<Record, Short> ENDPOINT_PORT = createField(DSL.name(\"endpoint_port\"), SQLDataType.SMALLINT, this, \"Null when Binary/Annotation.endpoint is null\");\n\n    /**\n     * The column <code>zipkin.zipkin_annotations.endpoint_service_name</code>.\n     * Null when Binary/Annotation.endpoint is null\n     */\n    public final TableField<Record, String> ENDPOINT_SERVICE_NAME = createField(DSL.name(\"endpoint_service_name\"), SQLDataType.VARCHAR(255), this, \"Null when Binary/Annotation.endpoint is null\");\n\n    private ZipkinAnnotations(Name alias, Table<Record> aliased) {\n        this(alias, aliased, (Field<?>[]) null, null);\n    }\n\n    private ZipkinAnnotations(Name alias, Table<Record> aliased, Field<?>[] parameters, Condition where) {\n        super(alias, null, aliased, parameters, DSL.comment(\"\"), TableOptions.table(), where);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_annotations</code> table reference\n     */\n    public ZipkinAnnotations(String alias) {\n        this(DSL.name(alias), ZIPKIN_ANNOTATIONS);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_annotations</code> table reference\n     */\n    public ZipkinAnnotations(Name alias) {\n        this(alias, ZIPKIN_ANNOTATIONS);\n    }\n\n    /**\n     * Create a <code>zipkin.zipkin_annotations</code> table reference\n     */\n    public ZipkinAnnotations() {\n        this(DSL.name(\"zipkin_annotations\"), null);\n    }\n\n    @Override\n    public Schema getSchema() {\n        return aliased() ? null : Zipkin.ZIPKIN;\n    }\n\n    @Override\n    public List<Index> getIndexes() {\n        return Arrays.asList(Indexes.ZIPKIN_ANNOTATIONS_A_KEY, Indexes.ZIPKIN_ANNOTATIONS_A_TYPE, Indexes.ZIPKIN_ANNOTATIONS_ENDPOINT_SERVICE_NAME, Indexes.ZIPKIN_ANNOTATIONS_TRACE_ID, Indexes.ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH_2, Indexes.ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH_3);\n    }\n\n    @Override\n    public List<UniqueKey<Record>> getUniqueKeys() {\n        return Arrays.asList(Keys.KEY_ZIPKIN_ANNOTATIONS_TRACE_ID_HIGH);\n    }\n\n    @Override\n    public ZipkinAnnotations as(String alias) {\n        return new ZipkinAnnotations(DSL.name(alias), this);\n    }\n\n    @Override\n    public ZipkinAnnotations as(Name alias) {\n        return new ZipkinAnnotations(alias, this);\n    }\n\n    @Override\n    public ZipkinAnnotations as(Table<?> alias) {\n        return new ZipkinAnnotations(alias.getQualifiedName(), this);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinAnnotations rename(String name) {\n        return new ZipkinAnnotations(DSL.name(name), null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinAnnotations rename(Name name) {\n        return new ZipkinAnnotations(name, null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinAnnotations rename(Table<?> name) {\n        return new ZipkinAnnotations(name.getQualifiedName(), null);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations where(Condition condition) {\n        return new ZipkinAnnotations(getQualifiedName(), aliased() ? this : null, null, condition);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations where(Collection<? extends Condition> conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations where(Condition... conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations where(Field<Boolean> condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinAnnotations where(SQL condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinAnnotations where(@Stringly.SQL String condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinAnnotations where(@Stringly.SQL String condition, Object... binds) {\n        return where(DSL.condition(condition, binds));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinAnnotations where(@Stringly.SQL String condition, QueryPart... parts) {\n        return where(DSL.condition(condition, parts));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations whereExists(Select<?> select) {\n        return where(DSL.exists(select));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinAnnotations whereNotExists(Select<?> select) {\n        return where(DSL.notExists(select));\n    }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/tables/ZipkinDependencies.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated.tables;\n\n\nimport java.time.LocalDate;\nimport java.util.Collection;\n\nimport org.jooq.Condition;\nimport org.jooq.Field;\nimport org.jooq.Name;\nimport org.jooq.PlainSQL;\nimport org.jooq.QueryPart;\nimport org.jooq.Record;\nimport org.jooq.SQL;\nimport org.jooq.Schema;\nimport org.jooq.Select;\nimport org.jooq.Stringly;\nimport org.jooq.Table;\nimport org.jooq.TableField;\nimport org.jooq.TableOptions;\nimport org.jooq.UniqueKey;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.SQLDataType;\nimport org.jooq.impl.TableImpl;\n\nimport zipkin2.storage.mysql.v1.internal.generated.Keys;\nimport zipkin2.storage.mysql.v1.internal.generated.Zipkin;\n\n\n/**\n * This class is generated by jOOQ.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class ZipkinDependencies extends TableImpl<Record> {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * The reference instance of <code>zipkin.zipkin_dependencies</code>\n     */\n    public static final ZipkinDependencies ZIPKIN_DEPENDENCIES = new ZipkinDependencies();\n\n    /**\n     * The class holding records for this type\n     */\n    @Override\n    public Class<Record> getRecordType() {\n        return Record.class;\n    }\n\n    /**\n     * The column <code>zipkin.zipkin_dependencies.day</code>.\n     */\n    public final TableField<Record, LocalDate> DAY = createField(DSL.name(\"day\"), SQLDataType.LOCALDATE.nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_dependencies.parent</code>.\n     */\n    public final TableField<Record, String> PARENT = createField(DSL.name(\"parent\"), SQLDataType.VARCHAR(255).nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_dependencies.child</code>.\n     */\n    public final TableField<Record, String> CHILD = createField(DSL.name(\"child\"), SQLDataType.VARCHAR(255).nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_dependencies.call_count</code>.\n     */\n    public final TableField<Record, Long> CALL_COUNT = createField(DSL.name(\"call_count\"), SQLDataType.BIGINT, this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_dependencies.error_count</code>.\n     */\n    public final TableField<Record, Long> ERROR_COUNT = createField(DSL.name(\"error_count\"), SQLDataType.BIGINT, this, \"\");\n\n    private ZipkinDependencies(Name alias, Table<Record> aliased) {\n        this(alias, aliased, (Field<?>[]) null, null);\n    }\n\n    private ZipkinDependencies(Name alias, Table<Record> aliased, Field<?>[] parameters, Condition where) {\n        super(alias, null, aliased, parameters, DSL.comment(\"\"), TableOptions.table(), where);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_dependencies</code> table reference\n     */\n    public ZipkinDependencies(String alias) {\n        this(DSL.name(alias), ZIPKIN_DEPENDENCIES);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_dependencies</code> table reference\n     */\n    public ZipkinDependencies(Name alias) {\n        this(alias, ZIPKIN_DEPENDENCIES);\n    }\n\n    /**\n     * Create a <code>zipkin.zipkin_dependencies</code> table reference\n     */\n    public ZipkinDependencies() {\n        this(DSL.name(\"zipkin_dependencies\"), null);\n    }\n\n    @Override\n    public Schema getSchema() {\n        return aliased() ? null : Zipkin.ZIPKIN;\n    }\n\n    @Override\n    public UniqueKey<Record> getPrimaryKey() {\n        return Keys.KEY_ZIPKIN_DEPENDENCIES_PRIMARY;\n    }\n\n    @Override\n    public ZipkinDependencies as(String alias) {\n        return new ZipkinDependencies(DSL.name(alias), this);\n    }\n\n    @Override\n    public ZipkinDependencies as(Name alias) {\n        return new ZipkinDependencies(alias, this);\n    }\n\n    @Override\n    public ZipkinDependencies as(Table<?> alias) {\n        return new ZipkinDependencies(alias.getQualifiedName(), this);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinDependencies rename(String name) {\n        return new ZipkinDependencies(DSL.name(name), null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinDependencies rename(Name name) {\n        return new ZipkinDependencies(name, null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinDependencies rename(Table<?> name) {\n        return new ZipkinDependencies(name.getQualifiedName(), null);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies where(Condition condition) {\n        return new ZipkinDependencies(getQualifiedName(), aliased() ? this : null, null, condition);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies where(Collection<? extends Condition> conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies where(Condition... conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies where(Field<Boolean> condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinDependencies where(SQL condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinDependencies where(@Stringly.SQL String condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinDependencies where(@Stringly.SQL String condition, Object... binds) {\n        return where(DSL.condition(condition, binds));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinDependencies where(@Stringly.SQL String condition, QueryPart... parts) {\n        return where(DSL.condition(condition, parts));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies whereExists(Select<?> select) {\n        return where(DSL.exists(select));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinDependencies whereNotExists(Select<?> select) {\n        return where(DSL.notExists(select));\n    }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/java/zipkin2/storage/mysql/v1/internal/generated/tables/ZipkinSpans.java",
    "content": "/*\n * This file is generated by jOOQ.\n */\npackage zipkin2.storage.mysql.v1.internal.generated.tables;\n\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.jooq.Condition;\nimport org.jooq.Field;\nimport org.jooq.Index;\nimport org.jooq.Name;\nimport org.jooq.PlainSQL;\nimport org.jooq.QueryPart;\nimport org.jooq.Record;\nimport org.jooq.SQL;\nimport org.jooq.Schema;\nimport org.jooq.Select;\nimport org.jooq.Stringly;\nimport org.jooq.Table;\nimport org.jooq.TableField;\nimport org.jooq.TableOptions;\nimport org.jooq.UniqueKey;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.SQLDataType;\nimport org.jooq.impl.TableImpl;\n\nimport zipkin2.storage.mysql.v1.internal.generated.Indexes;\nimport zipkin2.storage.mysql.v1.internal.generated.Keys;\nimport zipkin2.storage.mysql.v1.internal.generated.Zipkin;\n\n\n/**\n * This class is generated by jOOQ.\n */\n@SuppressWarnings({ \"all\", \"unchecked\", \"rawtypes\" })\npublic class ZipkinSpans extends TableImpl<Record> {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * The reference instance of <code>zipkin.zipkin_spans</code>\n     */\n    public static final ZipkinSpans ZIPKIN_SPANS = new ZipkinSpans();\n\n    /**\n     * The class holding records for this type\n     */\n    @Override\n    public Class<Record> getRecordType() {\n        return Record.class;\n    }\n\n    /**\n     * The column <code>zipkin.zipkin_spans.trace_id_high</code>. If non zero,\n     * this means the trace uses 128 bit traceIds instead of 64 bit\n     */\n    public final TableField<Record, Long> TRACE_ID_HIGH = createField(DSL.name(\"trace_id_high\"), SQLDataType.BIGINT.nullable(false).defaultValue(DSL.inline(\"0\", SQLDataType.BIGINT)), this, \"If non zero, this means the trace uses 128 bit traceIds instead of 64 bit\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.trace_id</code>.\n     */\n    public final TableField<Record, Long> TRACE_ID = createField(DSL.name(\"trace_id\"), SQLDataType.BIGINT.nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.id</code>.\n     */\n    public final TableField<Record, Long> ID = createField(DSL.name(\"id\"), SQLDataType.BIGINT.nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.name</code>.\n     */\n    public final TableField<Record, String> NAME = createField(DSL.name(\"name\"), SQLDataType.VARCHAR(255).nullable(false), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.remote_service_name</code>.\n     */\n    public final TableField<Record, String> REMOTE_SERVICE_NAME = createField(DSL.name(\"remote_service_name\"), SQLDataType.VARCHAR(255), this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.parent_id</code>.\n     */\n    public final TableField<Record, Long> PARENT_ID = createField(DSL.name(\"parent_id\"), SQLDataType.BIGINT, this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.debug</code>.\n     */\n    public final TableField<Record, Boolean> DEBUG = createField(DSL.name(\"debug\"), SQLDataType.BIT, this, \"\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.start_ts</code>. Span.timestamp():\n     * epoch micros used for endTs query and to implement TTL\n     */\n    public final TableField<Record, Long> START_TS = createField(DSL.name(\"start_ts\"), SQLDataType.BIGINT, this, \"Span.timestamp(): epoch micros used for endTs query and to implement TTL\");\n\n    /**\n     * The column <code>zipkin.zipkin_spans.duration</code>. Span.duration():\n     * micros used for minDuration and maxDuration query\n     */\n    public final TableField<Record, Long> DURATION = createField(DSL.name(\"duration\"), SQLDataType.BIGINT, this, \"Span.duration(): micros used for minDuration and maxDuration query\");\n\n    private ZipkinSpans(Name alias, Table<Record> aliased) {\n        this(alias, aliased, (Field<?>[]) null, null);\n    }\n\n    private ZipkinSpans(Name alias, Table<Record> aliased, Field<?>[] parameters, Condition where) {\n        super(alias, null, aliased, parameters, DSL.comment(\"\"), TableOptions.table(), where);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_spans</code> table reference\n     */\n    public ZipkinSpans(String alias) {\n        this(DSL.name(alias), ZIPKIN_SPANS);\n    }\n\n    /**\n     * Create an aliased <code>zipkin.zipkin_spans</code> table reference\n     */\n    public ZipkinSpans(Name alias) {\n        this(alias, ZIPKIN_SPANS);\n    }\n\n    /**\n     * Create a <code>zipkin.zipkin_spans</code> table reference\n     */\n    public ZipkinSpans() {\n        this(DSL.name(\"zipkin_spans\"), null);\n    }\n\n    @Override\n    public Schema getSchema() {\n        return aliased() ? null : Zipkin.ZIPKIN;\n    }\n\n    @Override\n    public List<Index> getIndexes() {\n        return Arrays.asList(Indexes.ZIPKIN_SPANS_NAME, Indexes.ZIPKIN_SPANS_REMOTE_SERVICE_NAME, Indexes.ZIPKIN_SPANS_START_TS, Indexes.ZIPKIN_SPANS_TRACE_ID_HIGH);\n    }\n\n    @Override\n    public UniqueKey<Record> getPrimaryKey() {\n        return Keys.KEY_ZIPKIN_SPANS_PRIMARY;\n    }\n\n    @Override\n    public ZipkinSpans as(String alias) {\n        return new ZipkinSpans(DSL.name(alias), this);\n    }\n\n    @Override\n    public ZipkinSpans as(Name alias) {\n        return new ZipkinSpans(alias, this);\n    }\n\n    @Override\n    public ZipkinSpans as(Table<?> alias) {\n        return new ZipkinSpans(alias.getQualifiedName(), this);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinSpans rename(String name) {\n        return new ZipkinSpans(DSL.name(name), null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinSpans rename(Name name) {\n        return new ZipkinSpans(name, null);\n    }\n\n    /**\n     * Rename this table\n     */\n    @Override\n    public ZipkinSpans rename(Table<?> name) {\n        return new ZipkinSpans(name.getQualifiedName(), null);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans where(Condition condition) {\n        return new ZipkinSpans(getQualifiedName(), aliased() ? this : null, null, condition);\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans where(Collection<? extends Condition> conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans where(Condition... conditions) {\n        return where(DSL.and(conditions));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans where(Field<Boolean> condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinSpans where(SQL condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinSpans where(@Stringly.SQL String condition) {\n        return where(DSL.condition(condition));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinSpans where(@Stringly.SQL String condition, Object... binds) {\n        return where(DSL.condition(condition, binds));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    @PlainSQL\n    public ZipkinSpans where(@Stringly.SQL String condition, QueryPart... parts) {\n        return where(DSL.condition(condition, parts));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans whereExists(Select<?> select) {\n        return where(DSL.exists(select));\n    }\n\n    /**\n     * Create an inline derived table from this table\n     */\n    @Override\n    public ZipkinSpans whereNotExists(Select<?> select) {\n        return where(DSL.notExists(select));\n    }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/main/resources/mysql.sql",
    "content": "--\n-- Copyright The OpenZipkin Authors\n-- SPDX-License-Identifier: Apache-2.0\n--\n\nCREATE TABLE IF NOT EXISTS zipkin_spans (\n  `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',\n  `trace_id` BIGINT NOT NULL,\n  `id` BIGINT NOT NULL,\n  `name` VARCHAR(255) NOT NULL,\n  `remote_service_name` VARCHAR(255),\n  `parent_id` BIGINT,\n  `debug` BIT(1),\n  `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',\n  `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',\n  PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)\n) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;\n\nALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';\nALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';\nALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';\nALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';\n\nCREATE TABLE IF NOT EXISTS zipkin_annotations (\n  `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',\n  `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',\n  `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',\n  `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',\n  `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',\n  `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',\n  `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',\n  `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',\n  `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',\n  `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',\n  `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'\n) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;\n\nALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';\nALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';\nALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';\nALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';\nALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';\nALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';\nALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';\n\nCREATE TABLE IF NOT EXISTS zipkin_dependencies (\n  `day` DATE NOT NULL,\n  `parent` VARCHAR(255) NOT NULL,\n  `child` VARCHAR(255) NOT NULL,\n  `call_count` BIGINT,\n  `error_count` BIGINT,\n  PRIMARY KEY (`day`, `parent`, `child`)\n) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/DependencyLinkV2SpanIteratorTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.util.List;\nimport org.jooq.Record;\nimport org.jooq.Record7;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Span;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations;\nimport zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinSpans;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.entry;\nimport static zipkin2.storage.mysql.v1.Schema.maybeGet;\nimport static zipkin2.v1.V1BinaryAnnotation.TYPE_BOOLEAN;\nimport static zipkin2.v1.V1BinaryAnnotation.TYPE_STRING;\n\nclass DependencyLinkV2SpanIteratorTest {\n  Long traceIdHigh = null;\n  long traceId = 1L;\n  Long parentId = null;\n  long spanId = 1L;\n\n  /** You cannot make a dependency link unless you know the the local or peer endpoint. */\n  @Test void whenNoServiceLabelsExist_kindIsUnknown() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, null));\n\n    Span span = iterator.next();\n    assertThat(span.kind()).isNull();\n    assertThat(span.localEndpoint()).isNull();\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  @Test void whenOnlyAddressLabelsExist_kindIsNull() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"s1\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sa\", TYPE_BOOLEAN, \"s2\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isNull();\n    assertThat(span.localServiceName()).isEqualTo(\"s1\");\n    assertThat(span.remoteServiceName()).isEqualTo(\"s2\");\n  }\n\n  /**\n   * The linker is biased towards server spans, or client spans that know the peer localEndpoint().\n   */\n  @Test void whenServerLabelsAreMissing_kindIsUnknownAndLabelsAreCleared() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"s1\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isNull();\n    assertThat(span.localEndpoint()).isNull();\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  /** \"sr\" is only applied when the local span is acting as a server */\n  @Test void whenSrServiceExists_kindIsServer() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"service\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"service\");\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  @Test void errorAnnotationIgnored() {\n    DependencyLinkV2SpanIterator iterator =\n      iterator(\n        newRecord().values(traceIdHigh, traceId, parentId, spanId, \"error\", -1, \"service\"));\n    Span span = iterator.next();\n\n    assertThat(span.tags()).isEmpty();\n    assertThat(span.annotations()).isEmpty();\n  }\n\n  @Test void errorTagAdded() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord()\n                .values(traceIdHigh, traceId, parentId, spanId, \"error\", TYPE_STRING, \"foo\"));\n    Span span = iterator.next();\n\n    assertThat(span.tags()).containsOnly(entry(\"error\", \"\"));\n  }\n\n  /** \"ca\" indicates the peer, which is a client in the case of a server span */\n  @Test void whenSrAndCaServiceExists_caIsThePeer() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"s1\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"s2\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"s2\");\n    assertThat(span.remoteServiceName()).isEqualTo(\"s1\");\n  }\n\n  /** \"cs\" indicates the peer, which is a client in the case of a server span */\n  @Test void whenSrAndCsServiceExists_caIsThePeer() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, \"s1\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"s2\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"s2\");\n    assertThat(span.remoteServiceName()).isEqualTo(\"s1\");\n  }\n\n  /** \"ca\" is more authoritative than \"cs\" */\n  @Test void whenCrAndCaServiceExists_caIsThePeer() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, \"foo\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"s1\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"s2\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"s2\");\n    assertThat(span.remoteServiceName()).isEqualTo(\"s1\");\n  }\n\n  /**\n   * Finagle labels two sides of the same socket \"ca\", V1BinaryAnnotation.TYPE_BOOLEAN, \"sa\" with\n   * the local endpoint name\n   */\n  @Test void specialCasesFinagleLocalSocketLabeling_client() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, \"service\"),\n            newRecord()\n                .values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"service\"),\n            newRecord()\n                .values(traceIdHigh, traceId, parentId, spanId, \"sa\", TYPE_BOOLEAN, \"service\"));\n    Span span = iterator.next();\n\n    // When there's no \"sr\" annotation, we assume it is a client.\n    assertThat(span.kind()).isEqualTo(Span.Kind.CLIENT);\n    assertThat(span.localEndpoint()).isNull();\n    assertThat(span.remoteServiceName()).isEqualTo(\"service\");\n  }\n\n  @Test void specialCasesFinagleLocalSocketLabeling_server() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord()\n                .values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"service\"),\n            newRecord()\n                .values(traceIdHigh, traceId, parentId, spanId, \"sa\", TYPE_BOOLEAN, \"service\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"service\"));\n    Span span = iterator.next();\n\n    // When there is an \"sr\" annotation, we know it is a server\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"service\");\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  /**\n   * Dependency linker works backwards: it is easier to treat a \"cs\" as a server span lacking its\n   * caller, than a client span lacking its receiver.\n   */\n  @Test void csWithoutSaIsServer() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, \"s1\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isEqualTo(Span.Kind.SERVER);\n    assertThat(span.localServiceName()).isEqualTo(\"s1\");\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  /**\n   * Service links to empty string are confusing and offer no value.\n   */\n  @Test void emptyToNull() {\n    DependencyLinkV2SpanIterator iterator =\n        iterator(\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"ca\", TYPE_BOOLEAN, \"\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"cs\", -1, \"\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sa\", TYPE_BOOLEAN, \"\"),\n            newRecord().values(traceIdHigh, traceId, parentId, spanId, \"sr\", -1, \"\"));\n    Span span = iterator.next();\n\n    assertThat(span.kind()).isNull();\n    assertThat(span.localEndpoint()).isNull();\n    assertThat(span.remoteEndpoint()).isNull();\n  }\n\n  static DependencyLinkV2SpanIterator iterator(Record... records) {\n    return new DependencyLinkV2SpanIterator(\n        new PeekingIterator<>(List.of(records).iterator()),\n        maybeGet(records[0], ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH, 0L),\n        records[0].get(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID));\n  }\n\n  static Record7<Long, Long, Long, Long, String, Integer, String> newRecord() {\n    return DSL.using(SQLDialect.MYSQL)\n        .newRecord(\n            ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH,\n            ZipkinSpans.ZIPKIN_SPANS.TRACE_ID,\n            ZipkinSpans.ZIPKIN_SPANS.PARENT_ID,\n            ZipkinSpans.ZIPKIN_SPANS.ID,\n            ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_KEY,\n            ZipkinAnnotations.ZIPKIN_ANNOTATIONS.A_TYPE,\n            ZipkinAnnotations.ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/ITMySQLStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.ZoneId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.jooq.DSLContext;\nimport org.jooq.Query;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Tag;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport zipkin2.DependencyLink;\nimport zipkin2.storage.StorageComponent;\n\nimport static zipkin2.storage.ITDependencies.aggregateLinks;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinDependencies.ZIPKIN_DEPENDENCIES;\n\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\n@Tag(\"docker\")\nclass ITMySQLStorage {\n\n  @RegisterExtension static MySQLExtension mysql = new MySQLExtension();\n\n  @Nested\n  class ITTraces extends zipkin2.storage.ITTraces<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override @Test @Disabled(\"v1 format is lossy in conversion when rows as upsert\")\n    protected void getTrace_differentiatesDebugFromShared(TestInfo testInfo) {\n    }\n\n    @Override @Test @Disabled(\"v1 format is lossy in conversion when rows as upsert\")\n    protected void getTraces_differentiatesDebugFromShared(TestInfo testInfo) {\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override\n    @Test\n    @Disabled(\"No consumer-side span deduplication\")\n    public void getTrace_deduplicates(TestInfo testInfo) {\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStore extends zipkin2.storage.ITSpanStore<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override @Test @Disabled(\"v1 format is lossy in conversion when rows as upsert\")\n    protected void getTraces_differentiatesDebugFromShared(TestInfo testInfo) {\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStoreHeavy extends zipkin2.storage.ITSpanStoreHeavy<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITStrictTraceIdFalse extends zipkin2.storage.ITStrictTraceIdFalse<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSearchEnabledFalse extends zipkin2.storage.ITSearchEnabledFalse<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITServiceAndSpanNames extends zipkin2.storage.ITServiceAndSpanNames<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITAutocompleteTags extends zipkin2.storage.ITAutocompleteTags<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependenciesOnDemand extends zipkin2.storage.ITDependencies<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependenciesHeavyOnDemand extends zipkin2.storage.ITDependenciesHeavy<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependenciesPreAggregated extends zipkin2.storage.ITDependencies<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n\n    /**\n     * The current implementation does not include dependency aggregation. It includes retrieval of\n     * pre-aggregated links, usually made via zipkin-dependencies\n     */\n    @Override protected void processDependencies(List<zipkin2.Span> spans) throws Exception {\n      aggregateDependencies(storage, spans);\n    }\n  }\n\n  @Nested\n  class ITDependenciesHeavyPreAggregated extends zipkin2.storage.ITDependenciesHeavy<MySQLStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return mysql.computeStorageBuilder();\n    }\n\n    @Override protected boolean returnsRawSpans() {\n      return false;\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n\n    /**\n     * The current implementation does not include dependency aggregation. It includes retrieval of\n     * pre-aggregated links, usually made via zipkin-dependencies\n     */\n    @Override protected void processDependencies(List<zipkin2.Span> spans) throws Exception {\n      aggregateDependencies(storage, spans);\n    }\n  }\n\n  static void aggregateDependencies(MySQLStorage storage, List<zipkin2.Span> spans)\n    throws SQLException {\n    try (Connection conn = storage.datasource.getConnection()) {\n      DSLContext context = storage.context.get(conn);\n\n      // batch insert the rows at timestamp midnight\n      List<Query> inserts = new ArrayList<>();\n      aggregateLinks(spans).forEach((midnight, links) -> {\n\n        LocalDate day = Instant.ofEpochMilli(midnight)\n          .atZone(ZoneId.of(\"UTC\"))\n          .toLocalDate();\n\n        for (DependencyLink link : links) {\n          inserts.add(context.insertInto(ZIPKIN_DEPENDENCIES)\n            .set(ZIPKIN_DEPENDENCIES.DAY, day)\n            .set(ZIPKIN_DEPENDENCIES.PARENT, link.parent())\n            .set(ZIPKIN_DEPENDENCIES.CHILD, link.child())\n            .set(ZIPKIN_DEPENDENCIES.CALL_COUNT, link.callCount())\n            .set(ZIPKIN_DEPENDENCIES.ERROR_COUNT, link.errorCount())\n            .onDuplicateKeyIgnore());\n        }\n      });\n      context.batch(inserts).execute();\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/MySQLExtension.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport javax.sql.DataSource;\nimport org.junit.jupiter.api.extension.AfterAllCallback;\nimport org.junit.jupiter.api.extension.BeforeAllCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.mariadb.jdbc.MariaDbDataSource;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.output.Slf4jLogConsumer;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.jdbc.ContainerLessJdbcDelegate;\nimport zipkin2.CheckResult;\n\nimport static org.junit.jupiter.api.Assumptions.assumeTrue;\nimport static org.testcontainers.ext.ScriptUtils.runInitScript;\nimport static org.testcontainers.utility.DockerImageName.parse;\n\nclass MySQLExtension implements BeforeAllCallback, AfterAllCallback {\n  static final Logger LOGGER = LoggerFactory.getLogger(MySQLExtension.class);\n\n  final MySQLContainer container = new MySQLContainer();\n\n  @Override public void beforeAll(ExtensionContext context) throws Exception {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.start();\n    LOGGER.info(\"Using hostPort {}:{}\", host(), port());\n\n    try (MySQLStorage result = computeStorageBuilder().build()) {\n      CheckResult check = result.check();\n      assumeTrue(check.ok(), () -> \"Could not connect to storage, skipping test: \"\n        + check.error().getMessage());\n\n      dropAndRecreateSchema(result.datasource);\n    }\n  }\n\n  /**\n   * MySQL doesn't auto-install schema. However, we may have changed it since the last time this\n   * image was published. So, we drop and re-create the schema before running any tests.\n   */\n  static void dropAndRecreateSchema(DataSource datasource) throws SQLException {\n    String[] scripts = {\n      // Drop all previously created tables in zipkin.*\n      \"drop_zipkin_tables.sql\",\n\n      // Populate the schema\n      \"mysql.sql\"\n    };\n\n    try (Connection connection = datasource.getConnection()) {\n      for (String scriptPath : scripts) {\n        runInitScript(new ContainerLessJdbcDelegate(connection), scriptPath);\n      }\n    }\n  }\n\n  @Override public void afterAll(ExtensionContext context) {\n    if (context.getRequiredTestClass().getEnclosingClass() != null) {\n      // Only run once in outermost scope.\n      return;\n    }\n\n    container.stop();\n  }\n\n  MySQLStorage.Builder computeStorageBuilder() {\n    final MariaDbDataSource dataSource;\n    try {\n      dataSource = new MariaDbDataSource(\n        \"jdbc:mariadb://%s:%s/zipkin?autoReconnect=true&useUnicode=yes&characterEncoding=UTF-8\".formatted(\n        host(), port()));\n      dataSource.setUser(\"zipkin\");\n      dataSource.setPassword(\"zipkin\");\n    } catch (SQLException e) {\n      throw new AssertionError(e);\n    }\n\n    return new MySQLStorage.Builder()\n      .datasource(dataSource)\n      .executor(Runnable::run);\n  }\n\n  String host() {\n    return container.getHost();\n  }\n\n  int port() {\n    return container.getMappedPort(3306);\n  }\n\n  // mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537\n  static final class MySQLContainer extends GenericContainer<MySQLContainer> {\n    MySQLContainer() {\n      super(parse(\"ghcr.io/openzipkin/zipkin-mysql:3.4.3\"));\n      addExposedPort(3306);\n      waitStrategy = Wait.forHealthcheck();\n      withLogConsumer(new Slf4jLogConsumer(LOGGER));\n    }\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/MySQLStorageTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.SQLException;\nimport java.util.List;\nimport javax.sql.DataSource;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.CheckResult;\nimport zipkin2.Component;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nclass MySQLStorageTest {\n\n  @Test void check_failsInsteadOfThrowing() throws SQLException {\n    DataSource dataSource = mock(DataSource.class);\n    when(dataSource.getConnection()).thenThrow(new SQLException(\"foo\"));\n\n    CheckResult result = storage(dataSource).check();\n\n    assertThat(result.ok()).isFalse();\n    assertThat(result.error())\n      .isInstanceOf(SQLException.class);\n  }\n\n  @Test void returns_whitelisted_autocompletekey() throws Exception {\n    DataSource dataSource = mock(DataSource.class);\n    assertThat(storage(dataSource).autocompleteTags().getKeys().execute())\n      .containsOnlyOnce(\"http.method\");\n  }\n\n  static MySQLStorage storage(DataSource dataSource) {\n    return MySQLStorage.newBuilder()\n      .strictTraceId(false)\n      .executor(Runnable::run)\n      .datasource(dataSource)\n      .autocompleteKeys(List.of(\"http.method\"))\n      .build();\n  }\n\n  /**\n   * The {@code toString()} of {@link Component} implementations appear in health check endpoints.\n   * Since these are likely to be exposed in logs and other monitoring tools, care should be taken\n   * to ensure {@code toString()} output is a reasonable length and does not contain sensitive\n   * information.\n   */\n  @Test void toStringContainsOnlySummaryInformation() {\n    DataSource datasource = mock(DataSource.class);\n    when(datasource.toString()).thenReturn(\"Blamo\");\n\n    assertThat(storage(datasource)).hasToString(\"MySQLStorage{datasource=Blamo}\");\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/SchemaTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport java.sql.SQLException;\nimport java.sql.SQLSyntaxErrorException;\nimport javax.sql.DataSource;\nimport org.jooq.conf.Settings;\nimport org.jooq.exception.DataAccessException;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nclass SchemaTest {\n  DataSource dataSource = mock(DataSource.class);\n  Schema schema = new Schema(\n    dataSource,\n    new DSLContexts(new Settings().withRenderSchema(false), null),\n    true\n  );\n\n  @Test void hasIpv6_falseWhenKnownSQLState() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n        \"Unknown column 'zipkin_annotations.endpoint_ipv6' in 'field list'\",\n        \"42S22\", 1054);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n        new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasIpv6).isFalse();\n  }\n\n  /**\n   * This returns false instead of failing when the SQLState code doesn't imply the column is\n   * missing. This is to prevent zipkin from crashing due to scenarios we haven't thought up, yet.\n   * The root error goes into the log in this case.\n   */\n  @Test void hasIpv6_falseWhenUnknownSQLState() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n        \"java.sql.SQLSyntaxErrorException: Table 'zipkin.zipkin_annotations' doesn't exist\",\n        \"42S02\", 1146);\n    DataSource dataSource = mock(DataSource.class);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n        new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasIpv6).isFalse();\n  }\n\n  @Test void hasErrorCount_falseWhenKnownSQLState() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n      \"Unknown column 'zipkin_dependencies.error_count' in 'field list'\",\n      \"42S22\", 1054);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n      new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasErrorCount).isFalse();\n  }\n\n  /**\n   * This returns false instead of failing when the SQLState code doesn't imply the column is\n   * missing. This is to prevent zipkin from crashing due to scenarios we haven't thought up, yet.\n   * The root error goes into the log in this case.\n   */\n  @Test void hasErrorCount_falseWhenUnknownSQLState() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n      \"java.sql.SQLSyntaxErrorException: Table 'zipkin.zipkin_dependencies' doesn't exist\",\n      \"42S02\", 1146);\n    DataSource dataSource = mock(DataSource.class);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n      new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasErrorCount).isFalse();\n  }\n\n  @Test void hasDependencies_missing() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n        \"\"\"\n        SQL [select count(*) from `zipkin_dependencies`]; Table 'zipkin.zipkin_dependencies' doesn't exist\n          Query is : select count(*) from `zipkin_dependencies`\\\n        \"\"\",\n        \"42S02\", 1146);\n    DataSource dataSource = mock(DataSource.class);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n        new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasPreAggregatedDependencies).isFalse();\n  }\n\n  @Test void hasRemoteServiceName_falseWhenKnownSQLState() throws SQLException {\n    SQLSyntaxErrorException sqlException = new SQLSyntaxErrorException(\n      \"Unknown column 'zipkin_spans.remote_serviceName' in 'field list'\",\n      \"42S22\", 1054);\n\n    // cheats to lower mock count: this exception is really thrown during execution of the query\n    when(dataSource.getConnection()).thenThrow(\n      new DataAccessException(sqlException.getMessage(), sqlException));\n\n    assertThat(schema.hasRemoteServiceName).isFalse();\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/java/zipkin2/storage/mysql/v1/SelectSpansAndAnnotationsTest.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage.mysql.v1;\n\nimport org.jooq.Record4;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.junit.jupiter.api.Test;\nimport zipkin2.Endpoint;\nimport zipkin2.v1.V1Annotation;\nimport zipkin2.v1.V1BinaryAnnotation;\nimport zipkin2.v1.V1Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.storage.mysql.v1.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS;\n\nclass SelectSpansAndAnnotationsTest {\n  @Test void processAnnotationRecord_nulls() {\n    Record4<Integer, Long, String, byte[]> annotationRecord =\n        annotationRecord(null, null, null, null);\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, null);\n\n    assertThat(builder)\n      .usingRecursiveComparison().isEqualTo(V1Span.newBuilder().traceId(1).id(1));\n  }\n\n  @Test void processAnnotationRecord_annotation() {\n    Record4<Integer, Long, String, byte[]> annotationRecord = annotationRecord(-1, 0L, \"foo\", null);\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, null);\n\n    assertThat(builder.build().annotations().get(0))\n        .isEqualTo(V1Annotation.create(0L, \"foo\", null));\n  }\n\n  @Test void processAnnotationRecord_tag() {\n    Record4<Integer, Long, String, byte[]> annotationRecord =\n        annotationRecord(6, null, \"foo\", new byte[0]);\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, null);\n\n    assertThat(builder.build().binaryAnnotations().get(0))\n        .isEqualTo(V1BinaryAnnotation.createString(\"foo\", \"\", null));\n  }\n\n  @Test void processAnnotationRecord_address() {\n    Record4<Integer, Long, String, byte[]> annotationRecord =\n        annotationRecord(0, null, \"ca\", new byte[] {1});\n    Endpoint ep = Endpoint.newBuilder().serviceName(\"foo\").build();\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, ep);\n\n    assertThat(builder.build().binaryAnnotations().get(0))\n        .isEqualTo(V1BinaryAnnotation.createAddress(\"ca\", ep));\n  }\n\n  @Test void processAnnotationRecord_address_skipMissingEndpoint() {\n    Record4<Integer, Long, String, byte[]> annotationRecord =\n        annotationRecord(0, null, \"ca\", new byte[] {1});\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, null);\n\n    assertThat(builder.build().binaryAnnotations()).isEmpty();\n  }\n\n  @Test void processAnnotationRecord_address_skipWrongKey() {\n    Record4<Integer, Long, String, byte[]> annotationRecord =\n        annotationRecord(0, null, \"sr\", new byte[] {1});\n    Endpoint ep = Endpoint.newBuilder().serviceName(\"foo\").build();\n\n    V1Span.Builder builder = V1Span.newBuilder().traceId(1).id(1);\n    SelectSpansAndAnnotations.processAnnotationRecord(annotationRecord, builder, ep);\n\n    assertThat(builder.build().binaryAnnotations()).isEmpty();\n  }\n\n  static Record4<Integer, Long, String, byte[]> annotationRecord(\n      Integer type, Long timestamp, String key, byte[] value) {\n    return DSL.using(SQLDialect.MYSQL)\n        .newRecord(\n            ZIPKIN_ANNOTATIONS.A_TYPE,\n            ZIPKIN_ANNOTATIONS.A_TIMESTAMP,\n            ZIPKIN_ANNOTATIONS.A_KEY,\n            ZIPKIN_ANNOTATIONS.A_VALUE)\n        .value1(type)\n        .value2(timestamp)\n        .value3(key)\n        .value4(value);\n  }\n\n  @Test void endpoint_justIpv4() {\n    Record4<String, Integer, Short, byte[]> endpointRecord =\n        endpointRecord(\"\", 127 << 24 | 1, (short) 0, new byte[0]);\n\n    assertThat(SelectSpansAndAnnotations.endpoint(endpointRecord))\n        .isEqualTo(Endpoint.newBuilder().ip(\"127.0.0.1\").build());\n  }\n\n  static Record4<String, Integer, Short, byte[]> endpointRecord(\n      String serviceName, Integer ipv4, Short port, byte[] ipv6) {\n    return DSL.using(SQLDialect.MYSQL)\n        .newRecord(\n            ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME,\n            ZIPKIN_ANNOTATIONS.ENDPOINT_IPV4,\n            ZIPKIN_ANNOTATIONS.ENDPOINT_PORT,\n            ZIPKIN_ANNOTATIONS.ENDPOINT_IPV6)\n        .value1(serviceName)\n        .value2(ipv4)\n        .value3(port)\n        .value4(ipv6);\n  }\n}\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/resources/drop_zipkin_tables.sql",
    "content": "SET FOREIGN_KEY_CHECKS = 0;\nSET @tables = NULL;\nSELECT GROUP_CONCAT(table_schema, '.', table_name) INTO @tables FROM information_schema.tables WHERE table_schema = 'zipkin';\nSET @tables = CONCAT('DROP TABLE ', @tables);\nPREPARE stmt FROM @tables;\nEXECUTE stmt;\nDEALLOCATE PREPARE stmt;\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "zipkin-storage/mysql-v1/src/test/resources/simplelogger.properties",
    "content": "# See https://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html for the full list of config options\n\norg.slf4j.simpleLogger.logFile=System.out\norg.slf4j.simpleLogger.defaultLogLevel=warn\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS\n\n# stop huge spam\norg.slf4j.simpleLogger.log.org.testcontainers.dockerclient=off\n"
  },
  {
    "path": "zipkin-storage/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <groupId>io.zipkin.zipkin2</groupId>\n  <artifactId>zipkin-storage-parent</artifactId>\n  <name>Storage</name>\n  <packaging>pom</packaging>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n  </properties>\n\n  <modules>\n    <module>cassandra</module>\n    <module>mysql-v1</module>\n    <module>elasticsearch</module>\n  </modules>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <!-- Our json codec is shaded, but Intellij doesn't understand that when running tests. -->\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n      <version>${gson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin-tests</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-api</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-simple</artifactId>\n      <version>${slf4j.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n      <version>${testcontainers.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-tests/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n\n    Copyright The OpenZipkin Authors\n    SPDX-License-Identifier: Apache-2.0\n\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <parent>\n    <groupId>io.zipkin</groupId>\n    <artifactId>zipkin-parent</artifactId>\n    <version>3.6.0-SNAPSHOT</version>\n  </parent>\n\n  <groupId>io.zipkin.zipkin2</groupId>\n  <artifactId>zipkin-tests</artifactId>\n  <name>Zipkin Interop Tests</name>\n\n  <properties>\n    <main.basedir>${project.basedir}/..</main.basedir>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>${project.groupId}</groupId>\n      <artifactId>zipkin</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <version>${junit-jupiter.version}</version>\n      <scope>provided</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.assertj</groupId>\n      <artifactId>assertj-core</artifactId>\n      <version>${assertj.version}</version>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/TestObjects.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Calendar;\nimport java.util.List;\nimport java.util.Random;\nimport java.util.TimeZone;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\nimport zipkin2.Span.Kind;\nimport zipkin2.internal.WriteBuffer;\n\nimport static java.util.Arrays.asList;\nimport static zipkin2.Span.Kind.CLIENT;\n\npublic final class TestObjects {\n  public static final Charset UTF_8 = StandardCharsets.UTF_8;\n  /** Notably, the cassandra implementation has day granularity */\n  public static final long DAY = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS);\n\n  static final TimeZone UTC = TimeZone.getTimeZone(\"UTC\");\n\n  // Use real time, as most span-stores have TTL logic which looks back several days.\n  public static final long TODAY = midnightUTC(System.currentTimeMillis());\n\n  public static final Endpoint FRONTEND =\n    Endpoint.newBuilder().serviceName(\"frontend\").ip(\"127.0.0.1\").build();\n  public static final Endpoint BACKEND =\n    Endpoint.newBuilder().serviceName(\"backend\").ip(\"192.168.99.101\").port(9000).build();\n  public static final Endpoint DB =\n    Endpoint.newBuilder().serviceName(\"db\").ip(\"2001:db8::c001\").port(3036).build();\n  public static final Endpoint KAFKA = Endpoint.newBuilder().serviceName(\"kafka\").build();\n\n  /** For bucketed data floored to the day. For example, dependency links. */\n  public static long midnightUTC(long epochMillis) {\n    Calendar day = Calendar.getInstance(UTC);\n    day.setTimeInMillis(epochMillis);\n    day.set(Calendar.MILLISECOND, 0);\n    day.set(Calendar.SECOND, 0);\n    day.set(Calendar.MINUTE, 0);\n    day.set(Calendar.HOUR_OF_DAY, 0);\n    return day.getTimeInMillis();\n  }\n\n  /** Only for unit tests, not integration tests. Integration tests should use random trace IDs. */\n  public static final Span CLIENT_SPAN = Span.newBuilder()\n    .traceId(\"7180c278b62e8f6a216a2aea45d08fc9\")\n    .parentId(\"1\")\n    .id(\"2\")\n    .name(\"get\")\n    .kind(Kind.CLIENT)\n    .localEndpoint(FRONTEND)\n    .remoteEndpoint(BACKEND)\n    .timestamp((TODAY + 50L) * 1000L)\n    .duration(200 * 1000L)\n    .addAnnotation((TODAY + 100) * 1000L, \"foo\")\n    .putTag(\"http.path\", \"/api\")\n    .putTag(\"clnt/finagle.version\", \"6.45.0\")\n    .build();\n\n  /** Only for unit tests, not integration tests. Integration tests should use random trace IDs. */\n  public static final List<Span> TRACE = newTrace(CLIENT_SPAN.traceId(), \"\");\n\n  // storage query units are milliseconds, while trace data is microseconds\n  public static long startTs(List<Span> trace) {\n    return trace.get(0).timestampAsLong() / 1000L;\n  }\n\n  public static long endTs(List<Span> trace) {\n    return startTs(trace) + trace.get(0).durationAsLong() / 1000L;\n  }\n\n  static final Span.Builder SPAN_BUILDER = newSpanBuilder();\n\n  /** Reuse a builder as it is significantly slows tests to create 100000 of these! */\n  static Span.Builder newSpanBuilder() {\n    return Span.newBuilder().name(\"get\")\n      .timestamp(TODAY * 1000L + 100L)\n      .duration(200 * 1000L)\n      .localEndpoint(FRONTEND)\n      .putTag(\"environment\", \"test\");\n  }\n\n  /**\n   * Zipkin trace ids are random 64bit numbers. This creates a relatively large input to avoid\n   * flaking out due to PRNG nuance.\n   */\n  public static final Span[] LOTS_OF_SPANS =\n    new Random().longs(100_000).mapToObj(TestObjects::span).toArray(Span[]::new);\n\n  public static Span span(long traceId) {\n    return SPAN_BUILDER.traceId(0L, traceId).id(traceId).build();\n  }\n\n  public static Span newClientSpan(String serviceNameSuffix) {\n    return spanBuilder(serviceNameSuffix).kind(CLIENT)\n      .remoteEndpoint(BACKEND.toBuilder().serviceName(\"backend\" + serviceNameSuffix).build())\n      .name(\"get /foo\")\n      .clearTags()\n      .putTag(\"http.method\", \"GET\")\n      .putTag(\"http.path\", \"/foo\")\n      .build();\n  }\n\n  public static Span.Builder spanBuilder(String serviceNameSuffix) {\n    Endpoint frontend = suffixServiceName(FRONTEND, serviceNameSuffix);\n    return SPAN_BUILDER.clone().localEndpoint(frontend).traceId(newTraceId());\n  }\n\n  public static String appendSuffix(String serviceName, String serviceNameSuffix) {\n    if (serviceNameSuffix == null) throw new NullPointerException(\"serviceNameSuffix == null\");\n    if (serviceNameSuffix.isEmpty()) return serviceName;\n    return serviceName + \"_\" + serviceNameSuffix;\n  }\n\n  public static Endpoint suffixServiceName(Endpoint endpoint, String serviceNameSuffix) {\n    String prefixed = appendSuffix(endpoint.serviceName(), serviceNameSuffix);\n    if (endpoint.serviceName().equals(prefixed)) return endpoint;\n    return endpoint.toBuilder().serviceName(prefixed).build();\n  }\n\n  public static List<Span> newTrace(String serviceNameSuffix) {\n    return newTrace(newTraceId(), serviceNameSuffix);\n  }\n\n  static List<Span> newTrace(String traceId, String serviceNameSuffix) {\n    Endpoint frontend = suffixServiceName(FRONTEND, serviceNameSuffix);\n    Endpoint backend = suffixServiceName(BACKEND, serviceNameSuffix);\n    Endpoint db = suffixServiceName(DB, serviceNameSuffix);\n\n    return asList(\n      Span.newBuilder().traceId(traceId).id(\"1\")\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(frontend)\n        .timestamp(TODAY * 1000L)\n        .duration(350 * 1000L)\n        .build(),\n      CLIENT_SPAN.toBuilder()\n        .traceId(traceId)\n        .localEndpoint(frontend)\n        .remoteEndpoint(backend)\n        .build(),\n      Span.newBuilder().traceId(traceId)\n        .parentId(CLIENT_SPAN.parentId()).id(CLIENT_SPAN.id()).shared(true)\n        .name(\"get\")\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .timestamp((TODAY + 100L) * 1000L)\n        .duration(150 * 1000L)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"2\").id(\"3\")\n        .name(\"query\")\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .remoteEndpoint(db)\n        .timestamp((TODAY + 150L) * 1000L)\n        .duration(50 * 1000L)\n        .addAnnotation((TODAY + 190) * 1000L, \"⻩\")\n        .putTag(\"error\", \"\\uD83D\\uDCA9\")\n        .build()\n    );\n  }\n\n  public static String newTraceId() {\n    byte[] traceId = new byte[32];\n    WriteBuffer buffer = WriteBuffer.wrap(traceId);\n    buffer.writeLongHex(ThreadLocalRandom.current().nextLong());\n    buffer.writeLongHex(ThreadLocalRandom.current().nextLong());\n    return new String(traceId, UTF_8);\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITAutocompleteTags.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base test for when {@link StorageComponent.Builder#autocompleteKeys(List)} has values.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n */\npublic abstract class ITAutocompleteTags<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    storage.autocompleteKeys(List.of(\"http.host\"));\n  }\n\n  @Test\n  protected void ignores_when_key_not_in_autocompleteTags(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    accept(spanBuilder(testSuffix)\n      .putTag(\"http.method\", \"GET\")\n      .build());\n\n    assertThat(storage.autocompleteTags().getKeys().execute()).doesNotContain(\"http.method\");\n\n    assertThat(storage.autocompleteTags().getValues(\"http.method\").execute()).isEmpty();\n  }\n\n  @Test protected void getTagsAndValues(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    for (int i = 0; i < 2; i++) {\n      accept(spanBuilder(testSuffix)\n        .putTag(\"http.method\", \"GET\")\n        .putTag(\"http.host\", \"host1\")\n        .build());\n    }\n\n    assertThat(storage.autocompleteTags().getKeys().execute())\n      .containsOnlyOnce(\"http.host\");\n\n    assertThat(storage.autocompleteTags().getValues(\"http.host\").execute())\n      .containsOnlyOnce(\"host1\");\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITDependencies.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ThreadLocalRandom;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport zipkin2.Annotation;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\nimport zipkin2.TestObjects;\nimport zipkin2.internal.DependencyLinker;\nimport zipkin2.v1.V1Span;\nimport zipkin2.v1.V1SpanConverter;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.appendSuffix;\nimport static zipkin2.TestObjects.endTs;\nimport static zipkin2.TestObjects.midnightUTC;\nimport static zipkin2.TestObjects.newTrace;\nimport static zipkin2.TestObjects.newTraceId;\nimport static zipkin2.TestObjects.startTs;\nimport static zipkin2.TestObjects.suffixServiceName;\n\n/**\n * Base test for {@link SpanStore} implementations that support dependency aggregation. Subtypes\n * should create a connection to a real backend, even if that backend is in-process.\n *\n * <p>This is a replacement for {@code zipkin.storage.DependenciesTest}. There is some redundancy\n * as {@code zipkin2.internal.DependencyLinkerTest} also defines many of these tests. The redundancy\n * helps ensure integrated storage doesn't fail due to mismapping of data, for example.\n */\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic abstract class ITDependencies<T extends StorageComponent> extends ITStorage<T> {\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  /**\n   * Override if dependency processing is a separate job: it should complete before returning from\n   * this method.\n   */\n  protected void processDependencies(List<Span> spans) throws Exception {\n    assertThat(spans).isNotEmpty(); // bug if it were!\n\n    storage.spanConsumer().accept(spans).execute();\n    blockWhileInFlight();\n  }\n\n  /**\n   * Normally, the root-span is where trace id == span id and parent id == null. The default is to\n   * look back one day from today.\n   */\n  @Test protected void getDependencies(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n  }\n\n  /**\n   * This tests that dependency linking ignores the high-bits of the trace ID when grouping spans\n   * for dependency links. This allows environments with 64-bit instrumentation to participate in\n   * the same trace as 128-bit instrumentation.\n   */\n  @Test protected void getDependencies_linksMixedTraceId(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    List<Span> mixedTrace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"1\").name(\"get\")\n        .kind(Kind.SERVER)\n        .timestamp(TODAY * 1000L)\n        .duration(350 * 1000L)\n        .localEndpoint(frontend)\n        .build(),\n      // the server dropped traceIdHigh\n      Span.newBuilder().traceId(traceId.substring(16)).parentId(\"1\").id(\"2\").name(\"get\")\n        .kind(Kind.SERVER).shared(true)\n        .timestamp((TODAY + 100) * 1000L)\n        .duration(250 * 1000L)\n        .localEndpoint(backend)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"1\").id(\"2\")\n        .kind(Kind.CLIENT)\n        .timestamp((TODAY + 50) * 1000L)\n        .duration(300 * 1000L)\n        .localEndpoint(frontend)\n        .build()\n    );\n\n    processDependencies(mixedTrace);\n\n    assertThat(store().getDependencies(endTs(mixedTrace), DAY).execute())\n      .containsOnly(DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1).build()\n      );\n  }\n\n  /** It should be safe to run dependency link jobs twice */\n  @Test protected void replayOverwrites(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n  }\n\n  /** Edge-case when there are no spans, or instrumentation isn't logging annotations properly. */\n  @Test protected void empty() throws Exception {\n    assertThat(store().getDependencies(TODAY, DAY).execute())\n      .isEmpty();\n  }\n\n  /**\n   * When all servers are instrumented, they all record {@link Kind#SERVER} and the {@link\n   * Span#localEndpoint()} indicates the service.\n   */\n  @Test protected void getDependenciesAllInstrumented(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    String frontend = appendSuffix(TestObjects.FRONTEND.serviceName(), testSuffix);\n    String backend = appendSuffix(TestObjects.BACKEND.serviceName(), testSuffix);\n    String db = appendSuffix(TestObjects.DB.serviceName(), testSuffix);\n\n    Endpoint one = Endpoint.newBuilder().serviceName(frontend).ip(\"127.0.0.1\").build();\n    Endpoint onePort3001 = one.toBuilder().port(3001).build();\n    Endpoint two = Endpoint.newBuilder().serviceName(backend).ip(\"127.0.0.2\").build();\n    Endpoint twoPort3002 = two.toBuilder().port(3002).build();\n    Endpoint three = Endpoint.newBuilder().serviceName(db).ip(\"127.0.0.3\").build();\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\").name(\"get\")\n        .kind(Kind.SERVER)\n        .timestamp(TODAY * 1000L)\n        .duration(350 * 1000L)\n        .localEndpoint(one)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"20\").name(\"get\")\n        .kind(Kind.CLIENT)\n        .timestamp((TODAY + 50) * 1000L)\n        .duration(250 * 1000L)\n        .localEndpoint(onePort3001)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"20\").name(\"get\").shared(true)\n        .kind(Kind.SERVER)\n        .timestamp((TODAY + 100) * 1000L)\n        .duration(150 * 1000L)\n        .localEndpoint(two)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"30\").name(\"query\")\n        .kind(Kind.CLIENT)\n        .timestamp((TODAY + 150) * 1000L)\n        .duration(50 * 1000L)\n        .localEndpoint(twoPort3002)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"30\").name(\"query\").shared(true)\n        .kind(Kind.SERVER)\n        .timestamp((TODAY + 160) * 1000L)\n        .duration(20 * 1000L)\n        .localEndpoint(three)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend)\n        .child(backend)\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend)\n        .child(db)\n        .callCount(1)\n        .build()\n    );\n  }\n\n  @Test protected void dependencies_loopback(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    List<Span> traceWithLoopback = List.of(\n      trace.get(0),\n      trace.get(1).toBuilder().remoteEndpoint(trace.get(0).localEndpoint()).build()\n    );\n\n    processDependencies(traceWithLoopback);\n\n    String frontend = appendSuffix(TestObjects.FRONTEND.serviceName(), testSuffix);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder().parent(frontend).child(frontend).callCount(1).build()\n    );\n  }\n\n  /**\n   * Some systems log a different trace id than the root span. This seems \"headless\", as we won't\n   * see a span whose id is the same as the trace id.\n   */\n  @Test protected void dependencies_headlessTrace(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    ArrayList<Span> trace = new ArrayList<>(newTrace(testSuffix));\n    trace.remove(0);\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n  }\n\n  @Test protected void looksBackIndefinitely(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n  }\n\n  /** Ensure complete traces are aggregated, even if they complete after endTs */\n  @Test protected void endTsInsideTheTrace(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(startTs(trace) + 100, 200).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n  }\n\n  @Test protected void endTimeBeforeData(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(startTs(trace) - 1000L, 1000L).execute())\n      .isEmpty();\n  }\n\n  @Test protected void lookbackAfterData(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(TODAY + 2 * DAY, DAY).execute())\n      .isEmpty();\n  }\n\n  /**\n   * This test confirms that the span store can detect dependency indicated by local and remote\n   * endpoint. Specifically, this detects an uninstrumented client before the trace and an\n   * uninstrumented server at the end of it.\n   */\n  @Test protected void notInstrumentedClientAndServer(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint kafka = suffixServiceName(TestObjects.KAFKA, testSuffix);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n        .timestamp(TODAY * 1000L).duration(350L * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(frontend)\n        .remoteEndpoint(kafka)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"21\").name(\"get\")\n        .timestamp((TODAY + 50L) * 1000L).duration(250L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(frontend)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"21\").name(\"get\").shared(true)\n        .timestamp((TODAY + 250) * 1000L).duration(50L * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 150L) * 1000L).duration(50L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .remoteEndpoint(db)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(kafka.serviceName())\n        .child(frontend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend.serviceName())\n        .child(db.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /** This tests we error prior to executing the call. */\n  @Test protected void endTsAndLookbackMustBePositive() {\n    SpanStore store = store();\n    assertThatThrownBy(() -> store.getDependencies(0L, DAY))\n      .isInstanceOf(IllegalArgumentException.class)\n      .hasMessage(\"endTs <= 0\");\n\n    assertThatThrownBy(() -> store.getDependencies(TODAY, 0L))\n      .isInstanceOf(IllegalArgumentException.class)\n      .hasMessage(\"lookback <= 0\");\n  }\n\n  @Test protected void instrumentedClientAndServer(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\").name(\"get\")\n        .timestamp((TODAY + 50L) * 1000L).duration(250L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(frontend)\n        .build(),\n      Span.newBuilder().traceId(traceId).id(\"10\").name(\"get\").shared(true)\n        .timestamp((TODAY + 100) * 1000L).duration(150L * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"11\").name(\"get\")\n        .timestamp((TODAY + 150L) * 1000L).duration(50L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .remoteEndpoint(db)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend.serviceName())\n        .child(db.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  @Test protected void instrumentedProducerAndConsumer(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint kafka = suffixServiceName(TestObjects.KAFKA, testSuffix);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\").name(\"send\")\n        .timestamp((TODAY + 50L) * 1000L).duration(1)\n        .kind(Kind.PRODUCER)\n        .localEndpoint(frontend)\n        .remoteEndpoint(kafka)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"11\").name(\"receive\")\n        .timestamp((TODAY + 100) * 1000L).duration(1)\n        .kind(Kind.CONSUMER)\n        .remoteEndpoint(kafka)\n        .localEndpoint(backend)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(kafka.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(kafka.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /** This shows a missing parent still results in a dependency link when local endpoints change */\n  @Test protected void missingIntermediateSpan(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n        .timestamp(TODAY * 1000L).duration(350L * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(frontend)\n        .build(),\n      // missing an intermediate span\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 150L) * 1000L).duration(50L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /**\n   * This test shows that dependency links can be filtered at daily granularity. This allows the UI\n   * to look for dependency intervals besides TODAY.\n   */\n  @Test protected void canSearchForIntervalsBesidesToday(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    // Let's pretend we have two days of data processed\n    //  - Note: calling this twice allows test implementations to consider timestamps\n    processDependencies(subtractDay(trace));\n    processDependencies(trace);\n\n    // A user looks at today's links.\n    //  - Note: Using the smallest lookback avoids bumping into implementation around windowing.\n    long lookback = trace.get(0).durationAsLong() / 1000L;\n    assertThat(store().getDependencies(endTs(trace), lookback).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n\n    // A user compares the links from those a day ago.\n    assertThat(store().getDependencies(endTs(trace) - DAY, DAY).execute())\n      .containsExactlyInAnyOrderElementsOf(links(testSuffix));\n\n    // A user looks at all links since data started\n    String frontend = appendSuffix(TestObjects.FRONTEND.serviceName(), testSuffix);\n    String backend = appendSuffix(TestObjects.BACKEND.serviceName(), testSuffix);\n    String db = appendSuffix(TestObjects.DB.serviceName(), testSuffix);\n\n    assertThat(store().getDependencies(endTs(trace), DAY * 2).execute()).containsOnly(\n      DependencyLink.newBuilder().parent(frontend).child(backend).callCount(2L).build(),\n      DependencyLink.newBuilder().parent(backend).child(db).callCount(2L).errorCount(2L).build()\n    );\n  }\n\n  @Test\n  protected void spanKindIsNotRequiredWhenEndpointsArePresent(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint kafka = suffixServiceName(TestObjects.KAFKA, testSuffix);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n        .timestamp(TODAY * 1000L).duration(350L * 1000L)\n        .localEndpoint(kafka)\n        .remoteEndpoint(frontend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"21\").name(\"get\")\n        .timestamp((TODAY + 50) * 1000L).duration(250L * 1000L)\n        .localEndpoint(frontend)\n        .remoteEndpoint(backend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 150) * 1000L).duration(50L * 1000L)\n        .localEndpoint(backend)\n        .remoteEndpoint(db).build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(TODAY + 1000, 1000L).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(kafka.serviceName())\n        .child(frontend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend.serviceName())\n        .child(db.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  @Test protected void unnamedEndpointsAreSkipped(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n        .timestamp(TODAY * 1000L).duration(350L * 1000L)\n        .localEndpoint(Endpoint.newBuilder().ip(\"172.17.0.4\").build())\n        .remoteEndpoint(frontend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"21\").name(\"get\")\n        .timestamp((TODAY + 50) * 1000L).duration(250L * 1000L)\n        .localEndpoint(frontend)\n        .remoteEndpoint(backend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 150) * 1000L).duration(50L * 1000L)\n        .localEndpoint(backend)\n        .remoteEndpoint(db).build()\n    );\n\n    processDependencies(trace);\n\n    // note there is no empty string service names\n    assertThat(store().getDependencies(TODAY + 1000, 1000L).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend.serviceName())\n        .child(db.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /**\n   * This test confirms that the span store can process trace with intermediate spans like the below\n   * properly.\n   * <p>\n   * span1: SR SS span2: intermediate call span3: CS SR SS CR: Dependency 1\n   */\n  @Test protected void intermediateSpans(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n        .timestamp(TODAY * 1000L).duration(350L * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(frontend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"20\").id(\"21\").name(\"call\")\n        .timestamp((TODAY + 25) * 1000L).duration(325L * 1000L)\n        .localEndpoint(frontend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 50) * 1000L).duration(250L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(frontend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n        .timestamp((TODAY + 100) * 1000L).duration(150 * 1000L).shared(true)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend).build(),\n      Span.newBuilder().traceId(traceId).parentId(\"22\").id(23L).name(\"call\")\n        .timestamp((TODAY + 110) * 1000L).duration(130L * 1000L)\n        .name(\"depth4\")\n        .localEndpoint(backend).build(),\n      Span.newBuilder().traceId(traceId).parentId(23L).id(24L).name(\"call\")\n        .timestamp((TODAY + 125) * 1000L).duration(105L * 1000L)\n        .name(\"depth5\")\n        .localEndpoint(backend).build(),\n      Span.newBuilder().traceId(traceId).parentId(24L).id(25L).name(\"get\")\n        .timestamp((TODAY + 150) * 1000L).duration(50L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(backend)\n        .remoteEndpoint(db).build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(TODAY + 1000, 1000L).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build(),\n      DependencyLink.newBuilder()\n        .parent(backend.serviceName())\n        .child(db.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /**\n   * This test confirms that the span store can process trace with intermediate spans like the below\n   * properly.\n   * <p>\n   * span1: SR SS span2: intermediate call span3: CS SR SS CR: Dependency 1\n   */\n  @Test protected void duplicateAddress(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    V1SpanConverter converter = V1SpanConverter.create();\n    List<Span> trace = new ArrayList<>();\n    converter.convert(V1Span.newBuilder().traceId(traceId).id(\"20\").name(\"get\")\n      .timestamp(TODAY * 1000L).duration(350L * 1000L)\n      .addAnnotation(TODAY * 1000, \"sr\", frontend)\n      .addAnnotation((TODAY + 350) * 1000, \"ss\", frontend)\n      .addBinaryAnnotation(\"ca\", frontend)\n      .addBinaryAnnotation(\"sa\", frontend).build(), trace);\n    converter.convert(V1Span.newBuilder().traceId(traceId).parentId(\"21\").id(\"22\").name(\"get\")\n      .timestamp((TODAY + 50) * 1000L).duration(250L * 1000L)\n      .addAnnotation((TODAY + 50) * 1000, \"cs\", frontend)\n      .addAnnotation((TODAY + 300) * 1000, \"cr\", frontend)\n      .addBinaryAnnotation(\"ca\", backend)\n      .addBinaryAnnotation(\"sa\", backend).build(), trace);\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(TODAY + 1000, 1000L).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /**\n   * Span starts on one host and ends on the other. In both cases, a response is neither sent nor\n   * received.\n   */\n  @Test protected void oneway(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\")\n        .timestamp((TODAY + 50) * 1000)\n        .kind(Kind.CLIENT)\n        .localEndpoint(frontend)\n        .build(),\n      Span.newBuilder().traceId(traceId).id(\"10\").shared(true)\n        .timestamp((TODAY + 100) * 1000)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /** A timeline annotation named error is not a failed span. A tag/binary annotation is. */\n  @Test protected void annotationNamedErrorIsntError(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\")\n        .timestamp((TODAY + 50) * 1000)\n        .kind(Kind.CLIENT)\n        .localEndpoint(frontend)\n        .build(),\n      Span.newBuilder().traceId(traceId).id(\"10\").shared(true)\n        .timestamp((TODAY + 100) * 1000)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .addAnnotation((TODAY + 72) * 1000, \"error\")\n        .build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(frontend.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /** Async span starts from an uninstrumented source. */\n  @Test protected void oneway_noClient(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint kafka = suffixServiceName(TestObjects.KAFKA, testSuffix);\n\n    List<Span> trace = List.of(\n      Span.newBuilder().traceId(traceId).id(\"10\").name(\"receive\")\n        .timestamp(TODAY * 1000)\n        .kind(Kind.SERVER)\n        .localEndpoint(backend)\n        .remoteEndpoint(kafka)\n        .build(),\n      Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"11\").name(\"process\")\n        .timestamp((TODAY + 25) * 1000L).duration(325L * 1000L)\n        .localEndpoint(backend).build()\n    );\n\n    processDependencies(trace);\n\n    assertThat(store().getDependencies(endTs(trace), DAY).execute()).containsOnly(\n      DependencyLink.newBuilder()\n        .parent(kafka.serviceName())\n        .child(backend.serviceName())\n        .callCount(1)\n        .build()\n    );\n  }\n\n  /** rebases a trace backwards a day with different trace. */\n  List<Span> subtractDay(List<Span> trace) {\n    long random = ThreadLocalRandom.current().nextLong();\n    return trace.stream()\n      .map(s -> {\n          Span.Builder b = s.toBuilder().traceId(0L, random);\n          if (s.timestampAsLong() != 0L) b.timestamp(s.timestampAsLong() - (DAY * 1000L));\n          s.annotations().forEach(a -> b.addAnnotation(a.timestamp() - (DAY * 1000L), a.value()));\n          return b.build();\n        }\n      ).collect(toList());\n  }\n\n  /** Returns links aggregated by midnight */\n  public static Map<Long, List<DependencyLink>> aggregateLinks(List<Span> spans) {\n    Map<Long, DependencyLinker> midnightToLinker = new LinkedHashMap<>();\n    for (List<Span> trace : GroupByTraceId.create(false).map(spans)) {\n      long midnightOfTrace = flooredTraceTimestamp(trace);\n      DependencyLinker linker = midnightToLinker.get(midnightOfTrace);\n      if (linker == null) midnightToLinker.put(midnightOfTrace, (linker = new DependencyLinker()));\n      linker.putTrace(trace);\n    }\n    Map<Long, List<DependencyLink>> result = new LinkedHashMap<>();\n    midnightToLinker.forEach((midnight, linker) -> result.put(midnight, linker.link()));\n    return result;\n  }\n\n  /** Default links produced by {@link TestObjects#newTrace(String)} */\n  static Iterable<? extends DependencyLink> links(String testSuffix) {\n    String frontend = appendSuffix(TestObjects.FRONTEND.serviceName(), testSuffix);\n    String backend = appendSuffix(TestObjects.BACKEND.serviceName(), testSuffix);\n    String db = appendSuffix(TestObjects.DB.serviceName(), testSuffix);\n    return List.of(\n      DependencyLink.newBuilder().parent(frontend).child(backend).callCount(1L).build(),\n      DependencyLink.newBuilder().parent(backend).child(db).callCount(1L).errorCount(1L).build()\n    );\n  }\n\n  /** gets the timestamp in milliseconds floored to midnight */\n  static long flooredTraceTimestamp(List<Span> trace) {\n    long midnightOfTrace = Long.MAX_VALUE;\n    for (Span span : trace) {\n      long currentTs = guessTimestamp(span);\n      if (currentTs != 0L && currentTs < midnightOfTrace) {\n        midnightOfTrace = midnightUTC(currentTs / 1000);\n      }\n    }\n    assertThat(midnightOfTrace).isNotEqualTo(Long.MAX_VALUE);\n    return midnightOfTrace;\n  }\n\n  static long guessTimestamp(Span span) {\n    if (span.timestampAsLong() != 0L) return span.timestampAsLong();\n    for (Annotation annotation : span.annotations()) {\n      if (0L < annotation.timestamp()) {\n        return annotation.timestamp();\n      }\n    }\n    return 0L; // return a timestamp that won't match a query\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITDependenciesHeavy.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInstance;\nimport zipkin2.DependencyLink;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.Span.Kind;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.DB;\nimport static zipkin2.TestObjects.FRONTEND;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.endTs;\nimport static zipkin2.TestObjects.newTraceId;\n\n/**\n * Base heavy tests for {@link SpanStore} implementations that support dependency aggregation.\n * Subtypes should create a connection to a real backend, even if that backend is in-process.\n *\n * <p>As these tests create a lot of data, implementations may wish to isolate them from other\n * integration tests such as {@link ITDependencies}\n */\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic abstract class ITDependenciesHeavy<T extends StorageComponent> extends ITStorage<T> {\n  @Override protected boolean initializeStoragePerTest() {\n    return true;\n  }\n\n  @Override protected void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  /**\n   * Override if dependency processing is a separate job: it should complete before returning from\n   * this method.\n   */\n  protected void processDependencies(List<Span> spans) throws Exception {\n    storage.spanConsumer().accept(spans).execute();\n    blockWhileInFlight();\n  }\n\n  /** Ensure there's no query limit problem around links */\n  @Test protected void manyLinks() throws Exception {\n    int count = 256; // Larger than 10, which is the default ES search limit that tripped this\n    List<Span> spans = new ArrayList<>(count);\n    for (int i = 1; i <= count; i++) {\n      String traceId = newTraceId();\n\n      Endpoint web = FRONTEND.toBuilder().serviceName(\"web-\" + i).build();\n      Endpoint app = BACKEND.toBuilder().serviceName(\"app-\" + i).build();\n      Endpoint db = DB.toBuilder().serviceName(\"db-\" + i).build();\n\n      spans.add(Span.newBuilder().traceId(traceId).id(\"10\").name(\"get\")\n        .timestamp((TODAY + 50L) * 1000L).duration(250L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(web)\n        .build()\n      );\n      spans.add(Span.newBuilder().traceId(traceId).id(\"10\").name(\"get\").shared(true)\n        .timestamp((TODAY + 100) * 1000L).duration(150 * 1000L)\n        .kind(Kind.SERVER)\n        .localEndpoint(app)\n        .build()\n      );\n      spans.add(Span.newBuilder().traceId(traceId).parentId(\"10\").id(\"11\").name(\"get\")\n        .timestamp((TODAY + 150L) * 1000L).duration(50L * 1000L)\n        .kind(Kind.CLIENT)\n        .localEndpoint(app)\n        .remoteEndpoint(db)\n        .build()\n      );\n    }\n\n    processDependencies(spans);\n\n    List<DependencyLink> links = store().getDependencies(endTs(spans), DAY).execute();\n    assertThat(links).hasSize(count * 2); // web-? -> app-?, app-? -> db-?\n    assertThat(links).extracting(DependencyLink::callCount)\n      .allSatisfy(callCount -> assertThat(callCount).isEqualTo(1));\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITSearchEnabledFalse.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.newClientSpan;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base test for when {@link StorageComponent.Builder#searchEnabled(boolean) searchEnabled ==\n * false}.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n */\npublic abstract class ITSearchEnabledFalse<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    storage.searchEnabled(false);\n  }\n\n  @Test protected void getTraces_indexDataReturnsNothing(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(clientSpan.localServiceName()).build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().spanName(clientSpan.name()).build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().annotationQuery(clientSpan.tags()).build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().minDuration(clientSpan.duration()).build());\n  }\n\n  @Test protected void getServiceNames_isEmpty(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    accept(spanBuilder(testSuffix).build());\n\n    assertThat(names().getServiceNames().execute()).isEmpty();\n  }\n\n  @Test protected void getRemoteServiceNames_isEmpty(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    accept(span);\n\n    assertThat(names().getRemoteServiceNames(span.localServiceName()).execute()).isEmpty();\n  }\n\n  @Test protected void getSpanNames_isEmpty(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    accept(span);\n\n    assertThat(names().getSpanNames(span.localServiceName()).execute()).isEmpty();\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITServiceAndSpanNames.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.BACKEND;\nimport static zipkin2.TestObjects.newClientSpan;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base test for {@link ServiceAndSpanNames}.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n */\npublic abstract class ITServiceAndSpanNames<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  @Test\n  protected void getLocalServiceNames_includesLocalServiceName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    assertThat(names().getServiceNames().execute())\n      .isEmpty();\n\n    accept(clientSpan);\n\n    assertThat(names().getServiceNames().execute())\n      .containsOnly(clientSpan.localServiceName());\n  }\n\n  @Test protected void getLocalServiceNames_noServiceName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    accept(spanBuilder(testSuffix).localEndpoint(null).build());\n\n    assertThat(names().getServiceNames().execute()).isEmpty();\n  }\n\n  @Test protected void getRemoteServiceNames(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    assertThat(names().getRemoteServiceNames(clientSpan.localServiceName()).execute())\n      .isEmpty();\n\n    accept(clientSpan);\n\n    assertThat(names().getRemoteServiceNames(clientSpan.localServiceName() + 1).execute())\n      .isEmpty();\n\n    assertThat(names().getRemoteServiceNames(clientSpan.localServiceName()).execute())\n      .contains(clientSpan.remoteServiceName());\n  }\n\n  @Test protected void getRemoteServiceNames_allReturned(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    // Assure a default store limit isn't hit by assuming if 50 are returned, all are returned\n    List<Span> spans = IntStream.rangeClosed(0, 50)\n      .mapToObj(i -> {\n        String suffix = i < 10 ? \"0\" + i : String.valueOf(i);\n        return spanBuilder(testSuffix)\n          .id(i + 1)\n          .remoteEndpoint(Endpoint.newBuilder().serviceName(\"yak\" + suffix + testSuffix).build())\n          .build();\n      })\n      .collect(Collectors.toList());\n    accept(spans);\n\n    assertThat(names().getRemoteServiceNames(spans.get(0).localServiceName()).execute())\n      .containsExactlyInAnyOrderElementsOf(spans.stream().map(Span::remoteServiceName)::iterator);\n  }\n\n  /** Ensures the service name index returns distinct results */\n  @Test protected void getRemoteServiceNames_dedupes(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> spans = IntStream.rangeClosed(0, 50)\n      .mapToObj(i -> spanBuilder(testSuffix).remoteEndpoint(BACKEND).build())\n      .collect(Collectors.toList());\n    accept(spans);\n\n    assertThat(names().getRemoteServiceNames(spans.get(0).localServiceName()).execute())\n      .containsExactly(BACKEND.serviceName());\n  }\n\n  @Test\n  protected void getRemoteServiceNames_noRemoteServiceName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n    accept(span);\n\n    assertThat(names().getRemoteServiceNames(span.localServiceName()).execute()).isEmpty();\n  }\n\n  @Test\n  protected void getRemoteServiceNames_serviceNameGoesLowercase(TestInfo testInfo)\n    throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    String uppercase = clientSpan.localServiceName().toUpperCase(Locale.ROOT);\n    assertThat(names().getRemoteServiceNames(uppercase).execute())\n      .containsExactly(clientSpan.remoteServiceName());\n  }\n\n  @Test\n  protected void getSpanNames_doesNotMapNameToRemoteServiceName(TestInfo testInfo)\n    throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    assertThat(names().getSpanNames(clientSpan.remoteServiceName()).execute())\n      .isEmpty();\n  }\n\n  @Test protected void getSpanNames(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    assertThat(names().getSpanNames(span.localServiceName()).execute())\n      .isEmpty();\n\n    accept(span);\n\n    assertThat(names().getSpanNames(span.localServiceName() + 1).execute())\n      .isEmpty();\n\n    assertThat(names().getSpanNames(span.localServiceName()).execute())\n      .contains(span.name());\n  }\n\n  @Test protected void getSpanNames_allReturned(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    // Assure a default store limit isn't hit by assuming if 50 are returned, all are returned\n    List<Span> spans = IntStream.rangeClosed(0, 50)\n      .mapToObj(i -> {\n        String suffix = i < 10 ? \"0\" + i : String.valueOf(i);\n        return spanBuilder(testSuffix).name(\"yak\" + suffix).build();\n      })\n      .collect(Collectors.toList());\n    accept(spans);\n\n    assertThat(names().getSpanNames(spans.get(0).localServiceName()).execute())\n      .containsExactlyInAnyOrderElementsOf(spans.stream().map(Span::name)::iterator);\n  }\n\n  /** Ensures the span name index returns distinct results */\n  @Test protected void getSpanNames_dedupes(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> spans = IntStream.rangeClosed(0, 50)\n      .mapToObj(i -> spanBuilder(testSuffix).build())\n      .collect(Collectors.toList());\n    accept(spans);\n\n    assertThat(names().getSpanNames(spans.get(0).localServiceName()).execute())\n      .containsExactly(spans.get(0).name());\n  }\n\n  @Test protected void getSpanNames_noSpanName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).name(null).build();\n    accept(span);\n\n    assertThat(names().getSpanNames(span.localServiceName()).execute()).isEmpty();\n  }\n\n  @Test protected void getSpanNames_serviceNameGoesLowercase(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n    accept(span);\n\n    String uppercase = span.localServiceName().toUpperCase(Locale.ROOT);\n    assertThat(names().getSpanNames(uppercase).execute())\n      .containsExactly(span.name());\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITSpanStore.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Call;\nimport zipkin2.Callback;\nimport zipkin2.Endpoint;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\n\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static zipkin2.Span.Kind.CLIENT;\nimport static zipkin2.Span.Kind.SERVER;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.appendSuffix;\nimport static zipkin2.TestObjects.endTs;\nimport static zipkin2.TestObjects.newClientSpan;\nimport static zipkin2.TestObjects.newTrace;\nimport static zipkin2.TestObjects.newTraceId;\nimport static zipkin2.TestObjects.spanBuilder;\nimport static zipkin2.TestObjects.suffixServiceName;\n\n/**\n * Base test for {@link SpanStore}.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n */\npublic abstract class ITSpanStore<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  /** This would only happen when the store layer is bootstrapping, or has been purged. */\n  @Test protected void allShouldWorkWhenEmpty() throws Exception {\n    QueryRequest.Builder q = requestBuilder().serviceName(\"service\");\n    assertGetTracesReturnsEmpty(q.build());\n    assertGetTracesReturnsEmpty(q.remoteServiceName(\"remotey\").build());\n    assertGetTracesReturnsEmpty(q.spanName(\"methodcall\").build());\n    assertGetTracesReturnsEmpty(q.parseAnnotationQuery(\"custom\").build());\n    assertGetTracesReturnsEmpty(q.parseAnnotationQuery(\"BAH=BEH\").build());\n  }\n\n  /** This is unlikely and means instrumentation sends empty spans by mistake. */\n  @Test protected void allShouldWorkWhenNoIndexableDataYet() throws Exception {\n    accept(Span.newBuilder().traceId(newTraceId()).id(\"1\").build());\n\n    allShouldWorkWhenEmpty();\n  }\n\n  @Test protected void consumer_implementsCall_execute(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    Call<Void> call = storage.spanConsumer().accept(List.of(span));\n\n    // Ensure the implementation didn't accidentally do I/O at assembly time.\n    assertGetTraceReturnsEmpty(span.traceId());\n    call.execute();\n    blockWhileInFlight();\n\n    assertGetTraceReturns(span);\n\n    assertThatThrownBy(call::execute)\n      .isInstanceOf(IllegalStateException.class);\n\n    // no problem to clone a call\n    call.clone().execute();\n  }\n\n  @Test protected void consumer_implementsCall_submit(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    Call<Void> call = storage.spanConsumer().accept(List.of(span));\n    // Ensure the implementation didn't accidentally do I/O at assembly time.\n    assertGetTraceReturnsEmpty(span.traceId());\n\n    CountDownLatch latch = new CountDownLatch(1);\n    Callback<Void> callback = new Callback<Void>() {\n      @Override public void onSuccess(Void value) {\n        latch.countDown();\n      }\n\n      @Override public void onError(Throwable t) {\n        latch.countDown();\n      }\n    };\n\n    call.enqueue(callback);\n    latch.await();\n    blockWhileInFlight();\n\n    assertGetTraceReturns(span);\n\n    assertThatThrownBy(() -> call.enqueue(callback))\n      .isInstanceOf(IllegalStateException.class);\n\n    // no problem to clone a call\n    call.clone().execute();\n  }\n\n  @Test protected void getTraces_groupsTracesTogether(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span traceASpan1 = spanBuilder(testSuffix).timestamp((TODAY + 1) * 1000L).build();\n    Span traceASpan2 = traceASpan1.toBuilder().id(\"2\").timestamp((TODAY + 2) * 1000L).build();\n\n    String traceId2 = newTraceId();\n    Span traceBSpan1 = traceASpan1.toBuilder().traceId(traceId2).build();\n    Span traceBSpan2 = traceASpan2.toBuilder().traceId(traceId2).build();\n\n    accept(traceASpan1, traceBSpan1, traceASpan2, traceBSpan2);\n\n    assertGetTracesReturns(\n      requestBuilder().build(),\n      List.of(traceASpan1, traceASpan2), List.of(traceBSpan1, traceBSpan2)\n    );\n  }\n\n  @Test protected void getTraces_considersBitsAbove64bit(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String traceId = newTraceId();\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n\n    // 64-bit trace ID\n    Span span1 = Span.newBuilder().traceId(traceId.substring(16)).id(\"1\")\n      .putTag(\"foo\", \"1\")\n      .timestamp(TODAY * 1000L)\n      .localEndpoint(frontend)\n      .build();\n    // 128-bit trace ID prefixed by above\n    Span span2 = span1.toBuilder().traceId(traceId).putTag(\"foo\", \"2\").build();\n    // Different 128-bit trace ID prefixed by above\n    Span span3 = span1.toBuilder().traceId(\"1\" + span1.traceId()).putTag(\"foo\", \"3\").build();\n\n    accept(span1, span2, span3);\n\n    for (Span span : List.of(span1, span2, span3)) {\n      assertGetTracesReturns(\n        requestBuilder().serviceName(frontend.serviceName())\n          .parseAnnotationQuery(\"foo=\" + span.tags().get(\"foo\"))\n          .build(),\n        List.of(span));\n    }\n  }\n\n  @Test protected void getTraces_filteringMatchesMostRecentTraces(TestInfo testInfo)\n    throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Endpoint> endpoints = IntStream.rangeClosed(1, 10)\n      .mapToObj(i -> Endpoint.newBuilder()\n        .serviceName(appendSuffix(\"service\" + i, testSuffix))\n        .ip(\"127.0.0.1\")\n        .build())\n      .collect(Collectors.toList());\n\n    long gapBetweenSpans = 100;\n    Span[] earlySpans = IntStream.rangeClosed(1, 10).mapToObj(i -> Span.newBuilder().name(\"early\")\n      .traceId(newTraceId()).id(Integer.toHexString(i))\n      .timestamp((TODAY - i) * 1000L).duration(1L)\n      .localEndpoint(endpoints.get(i - 1)).build()).toArray(Span[]::new);\n\n    Span[] lateSpans = IntStream.rangeClosed(1, 10).mapToObj(i -> Span.newBuilder().name(\"late\")\n      .traceId(newTraceId()).id(Integer.toHexString(i + 10))\n      .timestamp((TODAY + gapBetweenSpans - i) * 1000L).duration(1L)\n      .localEndpoint(endpoints.get(i - 1)).build()).toArray(Span[]::new);\n\n    accept(earlySpans);\n    accept(lateSpans);\n\n    List<Span>[] earlyTraces =\n      Stream.of(earlySpans).map(List::of).toArray(List[]::new);\n    List<Span>[] lateTraces =\n      Stream.of(lateSpans).map(List::of).toArray(List[]::new);\n\n    assertGetTracesReturnsCount(requestBuilder().build(), 20);\n\n    assertGetTracesReturns(\n      requestBuilder().limit(10).build(),\n      lateTraces);\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + gapBetweenSpans).lookback(gapBetweenSpans).build(),\n      lateTraces);\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY).build(),\n      earlyTraces);\n  }\n\n  @Test protected void getTraces_serviceNames(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    getTraces_serviceNames(newClientSpan(testSuffix));\n  }\n\n  void getTraces_serviceNames(Span clientSpan) throws Exception {\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(clientSpan.localServiceName() + 1).build());\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(clientSpan.localServiceName()).build(),\n      List.of(clientSpan));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .remoteServiceName(clientSpan.remoteServiceName() + 1)\n        .build());\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .remoteServiceName(clientSpan.remoteServiceName())\n        .build(),\n      List.of(clientSpan));\n  }\n\n  @Test protected void getTraces_serviceNames_mixedTraceIdLength(TestInfo testInfo)\n    throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    // add a trace with the same trace ID truncated to 64 bits, except different service names.\n    accept(spanBuilder(testSuffix)\n      .traceId(clientSpan.traceId().substring(16))\n      .localEndpoint(Endpoint.newBuilder().serviceName(appendSuffix(\"foo\", testSuffix)).build())\n      .remoteEndpoint(Endpoint.newBuilder().serviceName(appendSuffix(\"bar\", testSuffix)).build())\n      .build());\n\n    getTraces_serviceNames(clientSpan);\n  }\n\n  @Test protected void getTraces_spanName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    getTraces_spanName(newClientSpan(testSuffix));\n  }\n\n  void getTraces_spanName(Span clientSpan) throws Exception {\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().spanName(clientSpan.name() + 1).build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .spanName(clientSpan.name() + 1)\n        .build());\n\n    assertGetTracesReturns(\n      requestBuilder().spanName(clientSpan.name()).build(),\n      List.of(clientSpan));\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .spanName(clientSpan.name())\n        .build(),\n      List.of(clientSpan));\n  }\n\n  @Test protected void getTraces_spanName_mixedTraceIdLength(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    // add a trace with the same trace ID truncated to 64 bits, except the span name.\n    accept(clientSpan.toBuilder()\n      .traceId(clientSpan.traceId().substring(16))\n      .name(\"bar\")\n      .build());\n\n    getTraces_spanName(clientSpan);\n  }\n\n  @Test protected void getTraces_tags(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().annotationQuery(Map.of(\"foo\", \"bar\")).build());\n\n    assertGetTracesReturns(\n      requestBuilder().annotationQuery(clientSpan.tags()).build(),\n      List.of(clientSpan));\n  }\n\n  @Test protected void getTraces_minDuration(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().minDuration(clientSpan.durationAsLong() + 1).build());\n\n    assertGetTracesReturns(\n      requestBuilder().minDuration(clientSpan.durationAsLong()).build(),\n      List.of(clientSpan));\n  }\n\n  // pretend we had a late update of only timestamp/duration info\n  @Test protected void getTraces_lateDuration(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n    Span missingDuration = clientSpan.toBuilder().duration(0L).build();\n    Span lateDuration = Span.newBuilder()\n      .traceId(clientSpan.traceId())\n      .id(clientSpan.id())\n      .timestamp(clientSpan.timestampAsLong())\n      .duration(clientSpan.durationAsLong())\n      .localEndpoint(clientSpan.localEndpoint())\n      .build();\n    accept(missingDuration);\n    accept(lateDuration);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().minDuration(clientSpan.durationAsLong() + 1).build());\n\n    assertGetTracesReturns(\n      requestBuilder().minDuration(clientSpan.durationAsLong()).build(),\n      List.of(lateDuration, missingDuration));\n  }\n\n  @Test protected void getTraces_maxDuration(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder()\n        .minDuration(clientSpan.durationAsLong() - 2)\n        .maxDuration(clientSpan.durationAsLong() - 1)\n        .build());\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .minDuration(clientSpan.durationAsLong())\n        .maxDuration(clientSpan.durationAsLong())\n        .build(),\n      List.of(clientSpan));\n  }\n\n  /**\n   * The following skeletal span is used in dependency linking.\n   *\n   * <p>Notably this guards empty tag values work\n   */\n  @Test protected void readback_minimalErrorSpan(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    String serviceName = appendSuffix(\"isao01\", testSuffix);\n    Span errorSpan = Span.newBuilder().traceId(newTraceId()).id(\"1\")\n      .timestamp(TODAY * 1000L)\n      .localEndpoint(Endpoint.newBuilder().serviceName(serviceName).build())\n      .kind(CLIENT)\n      .putTag(\"error\", \"\")\n      .build();\n    accept(errorSpan);\n\n    QueryRequest.Builder requestBuilder =\n      requestBuilder().serviceName(serviceName); // so this doesn't die on cassandra v1\n\n    assertGetTracesReturns(requestBuilder.build(), List.of(errorSpan));\n\n    assertGetTracesReturns(\n      requestBuilder.parseAnnotationQuery(\"error\").build(), List.of(errorSpan));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder.parseAnnotationQuery(\"error=1\").build());\n\n    assertGetTraceReturns(errorSpan);\n  }\n\n  /**\n   * While large spans are discouraged, and maybe not indexed, we should be able to read them back.\n   */\n  @Test protected void readsBackLargeValues(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    char[] kilobyteOfText = new char[1024];\n    Arrays.fill(kilobyteOfText, 'a');\n\n    // Make a span that's over 1KiB in size\n    Span span = spanBuilder(testSuffix).name(\"big\").putTag(\"a\", new String(kilobyteOfText)).build();\n\n    accept(span);\n\n    // read back to ensure the data wasn't truncated\n    assertGetTracesReturns(requestBuilder().build(), List.of(span));\n    assertGetTraceReturns(span);\n  }\n\n  /**\n   * This tests problematic data that can sometimes break storage:\n   *\n   * <ul>\n   *   <li>json in span name</li>\n   *   <li>tag with nested dots (can be confused as nested objects)</li>\n   * </ul>\n   */\n  @Test protected void spanWithProblematicData(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    // Intentionally store in two fragments to try to trigger storage problems with dots\n    Span part1 = spanBuilder(testSuffix)\n      .putTag(\"http.path\", \"/api\")\n      .build();\n    accept(part1);\n\n    String json = \"{\\\"foo\\\":\\\"bar\\\"}\";\n    Span part2 = part1.toBuilder()\n      .name(json)\n      .clearTags()\n      .putTag(\"http.path.morepath\", \"/api/api\")\n      .build();\n    accept(part2);\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(part1.localServiceName()).spanName(json).build(),\n      List.of(part2, part1)\n    );\n\n    assertGetTraceReturns(part1.traceId(), List.of(part2, part1));\n  }\n\n  /** Shows that duration queries go against the root span, not the child */\n  @Test protected void getTraces_duration(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    List<List<Span>> traces = setupDurationData(testInfo);\n    List<Span> trace1 = traces.get(0), trace2 = traces.get(1), trace3 = traces.get(2);\n\n    QueryRequest.Builder q = requestBuilder().endTs(TODAY).lookback(DAY); // instead of since epoch\n\n    // Min duration is inclusive and is applied by service.\n    assertGetTracesReturns(\n      q.serviceName(frontend.serviceName()).minDuration(200_000L).build(),\n      trace1);\n\n    assertGetTracesReturns(\n      q.serviceName(db.serviceName()).minDuration(200_000L).build(),\n      trace2);\n\n    // Duration bounds aren't limited to root spans: they apply to all spans by service in a trace\n    assertGetTracesReturns(\n      q.serviceName(backend.serviceName())\n        .minDuration(50_000L)\n        .maxDuration(150_000L)\n        .build(),\n      trace1, trace2, trace3);\n\n    // Remote service name should apply to the duration filter\n    assertGetTracesReturns(\n      q.serviceName(frontend.serviceName())\n        .remoteServiceName(backend.serviceName())\n        .maxDuration(50_000L)\n        .build(),\n      trace2);\n\n    // Span name should apply to the duration filter\n    assertGetTracesReturns(\n      q.serviceName(backend.serviceName()).spanName(\"zip\").maxDuration(50_000L).build(),\n      trace3);\n\n    // Max duration should filter our longer spans from the same service\n    assertGetTracesReturns(\n      q.serviceName(backend.serviceName())\n        .minDuration(50_000L)\n        .maxDuration(50_000L)\n        .build(),\n      trace3);\n  }\n\n  /**\n   * Spans and traces are meaningless unless they have a timestamp. While unlikely, this could\n   * happen if a binary annotation is logged before a timestamped one is.\n   */\n  @Test protected void getTraces_absentWhenNoTimestamp(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n    Span spanWithoutTimestamp = span.toBuilder().timestamp(0L).duration(0L).build();\n\n    // Index the service name but no timestamp of any sort\n    accept(spanWithoutTimestamp);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(span.localServiceName()).build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(span.localServiceName())\n        .spanName(span.remoteServiceName())\n        .build());\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(span.localServiceName())\n        .spanName(span.name())\n        .build());\n\n    // now store the timestamped span\n    accept(span);\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(span.localServiceName()).build(),\n      List.of(spanWithoutTimestamp, span));\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(span.localServiceName())\n        .remoteServiceName(span.remoteServiceName())\n        .build(),\n      List.of(spanWithoutTimestamp, span));\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(span.localServiceName())\n        .spanName(span.name())\n        .build(),\n      List.of(spanWithoutTimestamp, span));\n  }\n\n  /** Prevents subtle bugs which can result in mixed-length traces from linking. */\n  @Test protected void getTraces_differentiatesDebugFromShared(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix).toBuilder()\n      .debug(true)\n      .build();\n    Span serverSpan = clientSpan.toBuilder().kind(SERVER)\n      .debug(null).shared(true)\n      .build();\n\n    accept(clientSpan, serverSpan);\n\n    // assertGetTracesReturns does recursive comparison\n    assertGetTracesReturns(requestBuilder().build(), List.of(clientSpan, serverSpan));\n  }\n\n  @Test protected void getTraces_annotation(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix).toBuilder()\n      .addAnnotation(TODAY, \"foo\")\n      .build();\n\n    accept(clientSpan);\n\n    // fetch by time based annotation, find trace\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .parseAnnotationQuery(clientSpan.annotations().get(0).value())\n        .build(),\n      List.of(clientSpan));\n\n    // should find traces by a tag\n    Map.Entry<String, String> tag = clientSpan.tags().entrySet().iterator().next();\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .parseAnnotationQuery(tag.getKey() + \"=\" + tag.getValue())\n        .build(),\n      List.of(clientSpan));\n  }\n\n  @Test\n  protected void getTraces_multipleAnnotationsBecomeAndFilter(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n\n    Span foo = Span.newBuilder().traceId(newTraceId()).name(\"call1\").id(1)\n      .timestamp((TODAY + 1) * 1000L)\n      .localEndpoint(frontend)\n      .addAnnotation((TODAY + 1) * 1000L, \"foo\").build();\n    // would be foo bar, except lexicographically bar precedes foo\n    Span barAndFoo = Span.newBuilder().traceId(newTraceId()).name(\"call2\").id(2)\n      .timestamp((TODAY + 2) * 1000L)\n      .localEndpoint(frontend)\n      .addAnnotation((TODAY + 2) * 1000L, \"bar\")\n      .addAnnotation((TODAY + 2) * 1000L, \"foo\").build();\n    Span fooAndBazAndQux = Span.newBuilder().traceId(newTraceId()).name(\"call3\").id(3)\n      .timestamp((TODAY + 3) * 1000L)\n      .localEndpoint(frontend)\n      .addAnnotation((TODAY + 3) * 1000L, \"foo\")\n      .putTag(\"baz\", \"qux\")\n      .build();\n    Span barAndFooAndBazAndQux = Span.newBuilder().traceId(newTraceId()).name(\"call4\").id(4)\n      .timestamp((TODAY + 4) * 1000L)\n      .localEndpoint(frontend)\n      .addAnnotation((TODAY + 4) * 1000L, \"bar\")\n      .addAnnotation((TODAY + 4) * 1000L, \"foo\")\n      .putTag(\"baz\", \"qux\")\n      .build();\n\n    accept(foo, barAndFoo, fooAndBazAndQux, barAndFooAndBazAndQux);\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"foo\").build(),\n      List.of(foo), List.of(barAndFoo), List.of(fooAndBazAndQux), List.of(barAndFooAndBazAndQux)\n    );\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName())\n        .parseAnnotationQuery(\"foo and bar\")\n        .build(),\n      List.of(barAndFoo), List.of(barAndFooAndBazAndQux)\n    );\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName())\n        .parseAnnotationQuery(\"foo and bar and baz=qux\")\n        .build(),\n      List.of(barAndFooAndBazAndQux));\n\n    // ensure we can search only by tag key\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"baz\").build(),\n      List.of(fooAndBazAndQux), List.of(barAndFooAndBazAndQux)\n    );\n  }\n\n  /** This test makes sure that annotation queries pay attention to which host recorded data */\n  @Test protected void getTraces_differentiateOnServiceName(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n\n    Span trace1 = Span.newBuilder().traceId(newTraceId()).name(\"1\").id(1)\n      .kind(CLIENT)\n      .timestamp((TODAY + 1) * 1000L)\n      .duration(3000L)\n      .localEndpoint(frontend)\n      .addAnnotation(((TODAY + 1) * 1000L) + 500, \"web\")\n      .putTag(\"local\", \"web\")\n      .putTag(\"web-b\", \"web\")\n      .build();\n\n    Span trace1Server = Span.newBuilder().traceId(trace1.traceId()).name(\"1\").id(1)\n      .kind(SERVER)\n      .shared(true)\n      .localEndpoint(backend)\n      .timestamp((TODAY + 2) * 1000L)\n      .duration(1000L)\n      .build();\n\n    Span trace2 = Span.newBuilder().traceId(newTraceId()).name(\"2\").id(2)\n      .timestamp((TODAY + 11) * 1000L)\n      .duration(3000L)\n      .kind(CLIENT)\n      .localEndpoint(backend)\n      .addAnnotation(((TODAY + 11) * 1000) + 500, \"app\")\n      .putTag(\"local\", \"app\")\n      .putTag(\"app-b\", \"app\")\n      .build();\n\n    Span trace2Server = Span.newBuilder().traceId(trace2.traceId()).name(\"2\").id(2)\n      .shared(true)\n      .kind(SERVER)\n      .localEndpoint(frontend)\n      .timestamp((TODAY + 12) * 1000L)\n      .duration(1000L).build();\n\n    accept(trace1, trace1Server, trace2, trace2Server);\n\n    // Sanity check\n    assertGetTraceReturns(trace1.traceId(), List.of(trace1, trace1Server));\n    assertGetTraceReturns(trace2.traceId(), List.of(trace2, trace2Server));\n    assertGetTracesReturns(requestBuilder().build(),\n      List.of(trace1, trace1Server), List.of(trace2, trace2Server));\n\n    // We only return traces where the service specified caused the data queried.\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"web\").build(),\n      List.of(trace1, trace1Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(backend.serviceName()).parseAnnotationQuery(\"web\").build());\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(backend.serviceName()).parseAnnotationQuery(\"app\").build(),\n      List.of(trace2, trace2Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"app\").build());\n\n    // tags are returned on annotation queries\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"web-b\").build(),\n      List.of(trace1, trace1Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(backend.serviceName()).parseAnnotationQuery(\"web-b\").build());\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(backend.serviceName()).parseAnnotationQuery(\"app-b\").build(),\n      List.of(trace2, trace2Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(frontend.serviceName()).parseAnnotationQuery(\"app-b\").build());\n\n    // We only return traces where the service specified caused the tag queried.\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend.serviceName())\n        .parseAnnotationQuery(\"local=web\")\n        .build(),\n      List.of(trace1, trace1Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(backend.serviceName())\n        .parseAnnotationQuery(\"local=web\")\n        .build());\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(backend.serviceName()).parseAnnotationQuery(\"local=app\").build(),\n      List.of(trace2, trace2Server));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().serviceName(frontend.serviceName())\n        .parseAnnotationQuery(\"local=app\")\n        .build());\n  }\n\n  /** limit should apply to traces closest to endTs */\n  @Test protected void getTraces_limit(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span1 = spanBuilder(testSuffix).build();\n    Span span2 = span1.toBuilder().traceId(newTraceId()).timestamp((TODAY + 2) * 1000L).build();\n    accept(span1, span2);\n\n    assertGetTracesReturns(\n      requestBuilder().serviceName(span1.localServiceName()).limit(1).build(),\n      List.of(span2));\n  }\n\n  /** Traces whose root span has timestamps between (endTs - lookback) and endTs are returned */\n  @Test protected void getTraces_endTsAndLookback(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span1 = spanBuilder(testSuffix).timestamp((TODAY + 1) * 1000L).build();\n    Span span2 = span1.toBuilder().traceId(newTraceId()).timestamp((TODAY + 2) * 1000L).build();\n    accept(span1, span2);\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().endTs(TODAY).build());\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 1).build(),\n      List.of(span1));\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 2).build(),\n      List.of(span1), List.of(span2));\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 3).build(),\n      List.of(span1), List.of(span2));\n\n    assertGetTracesReturnsEmpty(\n      requestBuilder().endTs(TODAY).build());\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 1).lookback(1).build(),\n      List.of(span1));\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 2).lookback(1).build(),\n      List.of(span1), List.of(span2));\n\n    assertGetTracesReturns(\n      requestBuilder().endTs(TODAY + 3).lookback(1).build(),\n      List.of(span2));\n  }\n\n  @Test protected void names_goLowercase(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    accept(clientSpan);\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .remoteServiceName(clientSpan.remoteServiceName().toUpperCase(Locale.ROOT))\n        .build(),\n      List.of(clientSpan));\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .spanName(clientSpan.name().toUpperCase(Locale.ROOT)).build(),\n      List.of(clientSpan));\n\n    assertGetTracesReturns(\n      requestBuilder()\n        .serviceName(clientSpan.localServiceName())\n        .remoteServiceName(clientSpan.remoteServiceName().toUpperCase(Locale.ROOT))\n        .build(),\n      List.of(clientSpan));\n  }\n\n  /** Ensure complete traces are aggregated, even if they complete after endTs */\n  @Test protected void getTraces_endTsInsideTheTrace(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n\n    accept(trace);\n\n    //  - Note: Using the smallest lookback avoids bumping into implementation around windowing.\n    long lookback = trace.get(0).durationAsLong() / 1000L;\n    assertGetTracesReturns(\n      requestBuilder().endTs(endTs(trace)).lookback(lookback).build(),\n      trace);\n  }\n\n  List<List<Span>> setupDurationData(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Endpoint frontend = suffixServiceName(TestObjects.FRONTEND, testSuffix);\n    Endpoint backend = suffixServiceName(TestObjects.BACKEND, testSuffix);\n    Endpoint db = suffixServiceName(TestObjects.DB, testSuffix);\n\n    String traceId1 = newTraceId(), traceId2 = newTraceId(), traceId3 = newTraceId();\n    long offsetMicros = (TODAY - 3) * 1000L; // to make sure queries look back properly\n    Span targz = Span.newBuilder().traceId(traceId1).id(1L)\n      .name(\"targz\").timestamp(offsetMicros + 100L).duration(200_000L)\n      .localEndpoint(frontend)\n      .remoteEndpoint(db)\n      .putTag(\"lc\", \"archiver\").build();\n    Span tar = Span.newBuilder().traceId(traceId1).id(2L).parentId(1L)\n      .name(\"tar\").timestamp(offsetMicros + 200L).duration(150_000L)\n      .localEndpoint(backend)\n      .remoteEndpoint(backend)\n      .putTag(\"lc\", \"archiver\").build();\n    Span gz = Span.newBuilder().traceId(traceId1).id(3L).parentId(1L)\n      .name(\"gz\").timestamp(offsetMicros + 250L).duration(50_000L)\n      .localEndpoint(db)\n      .remoteEndpoint(frontend)\n      .putTag(\"lc\", \"archiver\").build();\n    Span zip = Span.newBuilder().traceId(traceId3).id(3L)\n      .name(\"zip\").timestamp(offsetMicros + 130L).duration(50_000L)\n      .addAnnotation(offsetMicros + 130L, \"zip\")\n      .localEndpoint(backend)\n      .remoteEndpoint(backend)\n      .putTag(\"lc\", \"archiver\").build();\n\n    List<Span> trace1 = List.of(targz, tar, gz);\n    List<Span> trace2 = List.of(\n      targz.toBuilder().traceId(traceId2).timestamp(offsetMicros + 110L)\n        .localEndpoint(db)\n        .remoteEndpoint(frontend)\n        .putTag(\"lc\", \"archiver-v2\").build(),\n      tar.toBuilder().traceId(traceId2).timestamp(offsetMicros + 210L)\n        .localEndpoint(backend)\n        .remoteEndpoint(backend)\n        .putTag(\"lc\", \"archiver\").build(),\n      gz.toBuilder().traceId(traceId2).timestamp(offsetMicros + 260L)\n        .localEndpoint(frontend)\n        .remoteEndpoint(backend)\n        .putTag(\"lc\", \"archiver\").build());\n    List<Span> trace3 = List.of(zip);\n\n    accept(trace1);\n    accept(trace2);\n    accept(trace3);\n    return List.of(trace1, trace2, trace3);\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITSpanStoreHeavy.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Span;\n\nimport static zipkin2.TestObjects.TODAY;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base heavy tests for {@link SpanStore} implementations. Subtypes should create a connection to a\n * real backend, even if that backend is in-process.\n *\n * <p>As these tests create a lot of data, implementations may wish to isolate them from other\n * integration tests such as {@link ITSpanStore}\n */\npublic abstract class ITSpanStoreHeavy<T extends StorageComponent> extends ITStorage<T> {\n  @Override protected boolean initializeStoragePerTest() {\n    return true;\n  }\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  // Bugs have happened in the past where trace limit was mistaken for span count.\n  @Test protected void traceWithManySpans(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    int traceCount = 101;\n    Span[] spans = new Span[traceCount];\n    spans[0] = span;\n\n    IntStream.range(1, spans.length).forEach(i ->\n      spans[i] = span.toBuilder().parentId(span.id()).id(i)\n        .timestamp((TODAY + i) * 1000).duration(10L)\n        .build());\n\n    accept(spans);\n\n    assertGetTracesReturns(requestBuilder().build(), List.of(spans));\n    assertGetTraceReturns(span.traceId(), List.of(spans));\n  }\n\n  /**\n   * Formerly, a bug was present where cassandra didn't index more than bucket count traces per\n   * millisecond. This stores a lot of spans to ensure indexes work under high-traffic scenarios.\n   */\n  @Test protected void getTraces_manyTraces(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n    Map.Entry<String, String> tag = span.tags().entrySet().iterator().next();\n\n    int traceCount = 1000;\n    Span[] traces = new Span[traceCount];\n    traces[0] = span;\n\n    IntStream.range(1, traces.length).forEach(i ->\n      traces[i] = spanBuilder(testSuffix)\n        .timestamp((TODAY + i) * 1000).duration(10L)\n        .build());\n\n    accept(traces);\n\n    assertGetTracesReturnsCount(requestBuilder().limit(traceCount).build(), traceCount);\n\n    QueryRequest.Builder builder =\n      requestBuilder().limit(traceCount).serviceName(span.localServiceName());\n\n    assertGetTracesReturnsCount(\n      builder.build(), traceCount);\n\n    assertGetTracesReturnsCount(\n      builder.remoteServiceName(span.remoteServiceName()).build(), traceCount);\n\n    assertGetTracesReturnsCount(\n      builder.spanName(span.name()).build(), traceCount);\n\n    assertGetTracesReturnsCount(\n      builder.parseAnnotationQuery(tag.getKey() + \"=\" + tag.getValue()).build(), traceCount);\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.TestInfo;\nimport org.junit.jupiter.api.TestInstance;\nimport org.opentest4j.TestAbortedException;\nimport zipkin2.CheckResult;\nimport zipkin2.Span;\nimport zipkin2.internal.Trace;\n\nimport static java.lang.Boolean.TRUE;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static zipkin2.TestObjects.DAY;\nimport static zipkin2.TestObjects.TODAY;\n\n/** Base class for all {@link StorageComponent} integration tests. */\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\npublic abstract class ITStorage<T extends StorageComponent> {\n  protected T storage;\n\n  @BeforeAll void initializeStorage(TestInfo testInfo) {\n    if (initializeStoragePerTest()) return;\n    doInitializeStorage(testInfo);\n  }\n\n  @BeforeEach void initializeStorageForTest(TestInfo testInfo) {\n    if (!initializeStoragePerTest()) return;\n    doInitializeStorage(testInfo);\n  }\n\n  void doInitializeStorage(TestInfo testInfo) {\n    StorageComponent.Builder builder = newStorageBuilder(testInfo);\n    configureStorageForTest(builder);\n    // TODO(anuraaga): It wouldn't be difficult to allow storage builders to be parameterized by\n    // their storage type.\n    @SuppressWarnings(\"unchecked\")\n    T storage = (T) builder.build();\n    this.storage = storage;\n    checkStorage();\n  }\n\n  protected void checkStorage() {\n    CheckResult check = storage.check();\n    if (!check.ok()) {\n      throw new TestAbortedException(\"Could not connect to storage, skipping test: \"\n        + check.error().getMessage(), check.error());\n    }\n  }\n\n  @AfterAll void closeStorage() throws Exception {\n    if (initializeStoragePerTest()) return;\n    storage.close();\n  }\n\n  @AfterEach void closeStorageForTest() throws Exception {\n    if (!initializeStoragePerTest()) return;\n    storage.close();\n  }\n\n  @AfterEach void clearStorage() throws Exception {\n    clear();\n  }\n\n  /**\n   * Sets the test to initialise the {@link StorageComponent} before each test rather than the test\n   * class. Generally, tests will run faster if the storage is initialized as infrequently as\n   * possibly while clearing data between runs, but for certain backends like Cassandra, it's\n   * difficult to reliably clear data between runs and tends to be very slow anyways.\n   */\n  protected boolean initializeStoragePerTest() {\n    return false;\n  }\n\n  /**\n   * Returns a new {@link StorageComponent.Builder} for connecting to the backend for the test.\n   */\n  protected abstract StorageComponent.Builder newStorageBuilder(TestInfo testInfo);\n\n  /**\n   * Configures a {@link StorageComponent.Builder} with parameters for the test being executed.\n   */\n  protected abstract void configureStorageForTest(StorageComponent.Builder storage);\n\n  protected Traces traces() {\n    return storage.traces();\n  }\n\n  protected SpanStore store() {\n    return storage.spanStore();\n  }\n\n  protected ServiceAndSpanNames names() {\n    return storage.serviceAndSpanNames();\n  }\n\n  protected final void accept(Span... spans) throws IOException {\n    accept(List.of(spans));\n  }\n\n  protected final void accept(List<Span> spans) throws IOException {\n    for (int i = 0, length = spans.size(); i < length; i += 100) {\n      storage.spanConsumer().accept(spans.subList(i, Math.min(length, i + 100))).execute();\n      blockWhileInFlight();\n    }\n  }\n\n  // Blocks between writes of 100 spans to help avoid readback problems.\n  protected void blockWhileInFlight() {\n  }\n\n  /** Clears store between tests. */\n  protected abstract void clear() throws Exception;\n\n  protected static QueryRequest.Builder requestBuilder() {\n    return QueryRequest.newBuilder().endTs(TODAY + DAY).lookback(DAY * 2).limit(100);\n  }\n\n  protected void assertGetTracesReturns(QueryRequest request, List<Span>... traces)\n    throws IOException {\n    assertThat(sortTraces(store().getTraces(request).execute()))\n      .usingRecursiveFieldByFieldElementComparator()\n      .containsAll(sortTraces(List.of(traces)));\n  }\n\n  protected void assertGetTraceReturns(Span onlySpan) throws IOException {\n    assertGetTraceReturns(onlySpan.traceId(), List.of(onlySpan));\n  }\n\n  protected void assertGetTraceReturns(String traceId, List<Span> trace) throws IOException {\n    assertThat(sortTrace(storage.traces().getTrace(traceId).execute()))\n      .usingRecursiveFieldByFieldElementComparator()\n      .containsAll(sortTrace(trace));\n  }\n\n  protected void assertGetTraceReturnsEmpty(String traceId)\n    throws IOException {\n    List<Span> results = sortTrace(storage.traces().getTrace(traceId).execute());\n    assertThat(results)\n      .withFailMessage(\"Expected no traces for traceId <%s>, but received <%s>\", traceId, results)\n      .isEmpty();\n  }\n\n  protected void assertGetTracesReturns(List<String> traceIds, List<Span>... traces)\n    throws IOException {\n    assertThat(sortTraces(storage.traces().getTraces(traceIds).execute()))\n      .usingRecursiveFieldByFieldElementComparator()\n      .containsAll(sortTraces(List.of(traces)));\n  }\n\n  protected void assertGetTracesReturnsEmpty(List<String> traceIds) throws IOException {\n    List<List<Span>> results = sortTraces(storage.traces().getTraces(traceIds).execute());\n    assertThat(results)\n      .withFailMessage(\"Expected no traces for traceIds <%s>, but received <%s>\", traceIds, results)\n      .isEmpty();\n  }\n\n  protected void assertGetTracesReturnsCount(QueryRequest request, int traceCount)\n    throws IOException {\n    int countReturned = store().getTraces(request).execute().size();\n    assertThat(countReturned)\n      .withFailMessage(\"Expected <%s> traces for request <%s>, but received <%s>\",\n        traceCount, request, countReturned)\n      .usingRecursiveComparison()\n      .isEqualTo(traceCount);\n  }\n\n  protected void assertGetTracesReturnsEmpty(QueryRequest request) throws IOException {\n    List<List<Span>> results = sortTraces(store().getTraces(request).execute());\n    assertThat(results)\n      .withFailMessage(\"Expected no traces for request <%s>, but received <%s>\", request, results)\n      .isEmpty();\n  }\n\n  List<List<Span>> sortTraces(List<List<Span>> traces) {\n    List<List<Span>> result = new ArrayList<>();\n    for (List<Span> trace : traces) {\n      result.add(sortTrace(trace));\n    }\n    return result;\n  }\n\n  /** Override for storage that does upserts and cannot return the original spans. */\n  protected boolean returnsRawSpans() {\n    return true;\n  }\n\n  /** Used to help tests from colliding too much */\n  protected static String testSuffix(TestInfo testInfo) {\n    String result;\n    if (testInfo.getTestMethod().isPresent()) {\n      result = testInfo.getTestMethod().get().getName();\n    } else {\n      assert testInfo.getTestClass().isPresent();\n      result = testInfo.getTestClass().get().getSimpleName();\n    }\n    result = result.toLowerCase();\n    return result.length() <= 48 ? result : result.substring(result.length() - 48);\n  }\n\n  protected List<Span> sortTrace(List<Span> trace) {\n    if (!returnsRawSpans()) trace = Trace.merge(trace);\n    List<Span> result = new ArrayList<>(trace);\n    // Sort so that tests aren't flakey. Spans are not required to be in any order by contract.\n    // However, when writing tests, we shouldn't use data that can appear in random order.\n    result.sort((l, r) -> {\n      int traceId = l.traceId().compareTo(r.traceId());\n      if (traceId != 0) return traceId;\n      int id = l.id().compareTo(r.id());\n      if (id != 0) return id;\n      int shared = Boolean.compare(TRUE.equals(l.shared()), TRUE.equals(r.shared()));\n      if (shared != 0) return shared;\n\n      if (l.name() != null && r.name() != null) {\n        int name = l.name().compareTo(r.name());\n        if (name != 0) return name;\n      }\n\n      int timestamp = Long.compare(l.timestampAsLong(), r.timestampAsLong());\n      if (timestamp != 0) return timestamp;\n\n      int duration = Long.compare(l.durationAsLong(), r.durationAsLong());\n      if (duration != 0) return duration;\n\n      throw new AssertionError(\"Don't use test data that results in indeterministic ordering:\\n\" +\n        \"l=\" + l + \", r=\" + r);\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITStrictTraceIdFalse.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Span;\nimport zipkin2.TestObjects;\n\nimport static zipkin2.TestObjects.appendSuffix;\nimport static zipkin2.TestObjects.newTrace;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base test for when {@link StorageComponent.Builder#strictTraceId(boolean) strictTraceId ==\n * false}.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n *\n * <p>This is a replacement for {@code zipkin.storage.StrictTraceIdFalseTest}.\n */\npublic abstract class ITStrictTraceIdFalse<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    storage.strictTraceId(false);\n  }\n\n  /** Ensures we can still lookup fully 128-bit traces when strict trace ID id disabled */\n  @Test protected void getTraces_128BitTraceId(TestInfo testInfo) throws Exception {\n    getTraces_128BitTraceId(accept128BitTrace(storage, testInfo), testInfo);\n  }\n\n  @Test protected void getTraces_128BitTraceId_mixed(TestInfo testInfo) throws Exception {\n    getTraces_128BitTraceId(acceptMixedTrace(testInfo), testInfo);\n  }\n\n  protected void getTraces_128BitTraceId(List<Span> trace, TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    assertGetTracesReturns(requestBuilder().build(), trace);\n\n    String frontend = appendSuffix(TestObjects.FRONTEND.serviceName(), testSuffix);\n    String backend = appendSuffix(TestObjects.BACKEND.serviceName(), testSuffix);\n\n    // search by 128-bit side's service and data\n    assertGetTracesReturns(\n      requestBuilder().serviceName(frontend).parseAnnotationQuery(\"foo\").build(),\n      trace);\n\n    // search by 64-bit side's service and data\n    assertGetTracesReturns(\n      requestBuilder().serviceName(backend).parseAnnotationQuery(\"error\").build(),\n      trace);\n  }\n\n  @Test protected void getTrace_retrievesBy64Or128BitTraceId(TestInfo testInfo) throws Exception {\n    List<Span> trace = accept128BitTrace(storage, testInfo);\n\n    retrievesBy64Or128BitTraceId(trace);\n  }\n\n  @Test\n  protected void getTrace_retrievesBy64Or128BitTraceId_mixed(TestInfo testInfo) throws Exception {\n    List<Span> trace = acceptMixedTrace(testInfo);\n\n    retrievesBy64Or128BitTraceId(trace);\n  }\n\n  void retrievesBy64Or128BitTraceId(List<Span> trace) throws IOException {\n    String traceId =\n      trace.stream().filter(t -> t.traceId().length() == 32).findAny().get().traceId();\n\n    assertGetTraceReturns(traceId, trace);\n    assertGetTraceReturns(traceId.substring(16), trace);\n  }\n\n  protected List<Span> accept128BitTrace(StorageComponent storage, TestInfo testInfo)\n    throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n    Collections.reverse(trace);\n    storage.spanConsumer().accept(trace).execute();\n    blockWhileInFlight();\n    return trace;\n  }\n\n  List<Span> acceptMixedTrace(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    List<Span> trace = newTrace(testSuffix);\n    String downgraded = trace.get(0).traceId().substring(16);\n    // iterate after the outbound client span, emulating a server that downgraded\n    for (int i = 2; i < trace.size(); i++) {\n      trace.set(i, trace.get(i).toBuilder().traceId(downgraded).build());\n    }\n    Collections.reverse(trace);\n    accept(trace.toArray(new Span[0]));\n    return sortTrace(trace);\n  }\n\n  /** current implementation cannot return exact form reported */\n  @Test protected void getTraces_retrievesBy64Or128BitTraceId(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span with128BitId1 = spanBuilder(testSuffix).build();\n    Span with64BitId1 = with128BitId1.toBuilder()\n      .traceId(with128BitId1.traceId().substring(16)).id(\"a\")\n      .timestamp(with128BitId1.timestampAsLong() + 1000L)\n      .build();\n\n    Span with128BitId2 = spanBuilder(testSuffix).build();\n    Span with64BitId2 = with128BitId2.toBuilder()\n      .traceId(with128BitId2.traceId().substring(16)).id(\"b\")\n      .timestamp(with128BitId2.timestampAsLong() + 1000L)\n      .build();\n\n    Span with128BitId3 = spanBuilder(testSuffix).build();\n    Span with64BitId3 = with128BitId3.toBuilder()\n      .traceId(with128BitId3.traceId().substring(16)).id(\"c\")\n      .timestamp(with128BitId3.timestampAsLong() + 1000L)\n      .build();\n\n    accept(with128BitId1, with64BitId1, with128BitId2, with64BitId2, with128BitId3, with64BitId3);\n\n    List<Span>[] trace1And3 =\n      new List[] {List.of(with128BitId1, with64BitId1), List.of(with128BitId3, with64BitId3)};\n\n    assertGetTracesReturns(\n      List.of(with128BitId1.traceId(), with64BitId1.traceId(), with128BitId3.traceId(),\n        with64BitId3.traceId()), trace1And3);\n\n    assertGetTracesReturns(\n      List.of(with64BitId1.traceId(), with64BitId3.traceId()), trace1And3);\n\n    assertGetTracesReturns(\n      List.of(with128BitId1.traceId(), with128BitId3.traceId()), trace1And3);\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/main/java/zipkin2/storage/ITTraces.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.TestInfo;\nimport zipkin2.Span;\n\nimport static zipkin2.Span.Kind.SERVER;\nimport static zipkin2.TestObjects.newClientSpan;\nimport static zipkin2.TestObjects.newTraceId;\nimport static zipkin2.TestObjects.spanBuilder;\n\n/**\n * Base test for {@link Traces}.\n *\n * <p>Subtypes should create a connection to a real backend, even if that backend is in-process.\n */\npublic abstract class ITTraces<T extends StorageComponent> extends ITStorage<T> {\n\n  @Override protected final void configureStorageForTest(StorageComponent.Builder storage) {\n    // Defaults are fine.\n  }\n\n  @Test protected void getTrace_returnsEmptyOnNotFound(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix);\n\n    assertGetTraceReturnsEmpty(clientSpan.traceId());\n\n    accept(clientSpan);\n\n    assertGetTraceReturns(clientSpan);\n\n    assertGetTraceReturnsEmpty(clientSpan.traceId().substring(16));\n  }\n\n  /** Prevents subtle bugs which can result in mixed-length traces from linking. */\n  @Test protected void getTrace_differentiatesDebugFromShared(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix).toBuilder()\n      .debug(true)\n      .build();\n    Span serverSpan = clientSpan.toBuilder().kind(SERVER)\n      .debug(null).shared(true)\n      .build();\n\n    accept(clientSpan, serverSpan);\n\n    // assertGetTraceReturns does recursive comparison\n    assertGetTraceReturns(clientSpan.traceId(), List.of(clientSpan, serverSpan));\n  }\n\n  @Test protected void getTraces_onlyReturnsTracesThatMatch(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span1 = spanBuilder(testSuffix).build(), span2 = spanBuilder(testSuffix).build();\n    List<String> traceIds = List.of(span1.traceId(), newTraceId());\n\n    assertGetTracesReturnsEmpty(traceIds);\n\n    accept(span1, span2);\n\n    assertGetTracesReturns(traceIds, List.of(span1));\n\n    List<String> shortTraceIds =\n      traceIds.stream().map(t -> t.substring(16)).collect(Collectors.toList());\n    assertGetTracesReturnsEmpty(shortTraceIds);\n  }\n\n  /** Prevents subtle bugs which can result in mixed-length traces from linking. */\n  @Test protected void getTraces_differentiatesDebugFromShared(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span clientSpan = newClientSpan(testSuffix).toBuilder()\n      .debug(true)\n      .build();\n    Span serverSpan = clientSpan.toBuilder().kind(SERVER)\n      .debug(null).shared(true)\n      .build();\n\n    accept(clientSpan, serverSpan);\n\n    // assertGetTracesReturns does recursive comparison\n    assertGetTracesReturns(List.of(clientSpan.traceId()), List.of(clientSpan, serverSpan));\n  }\n\n  @Test protected void getTraces_returnsEmptyOnNotFound(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span1 = spanBuilder(testSuffix).build(), span2 = spanBuilder(testSuffix).build();\n    List<String> traceIds = List.of(span1.traceId(), span2.traceId());\n\n    assertGetTracesReturnsEmpty(traceIds);\n\n    accept(span1, span2);\n\n    assertGetTracesReturns(traceIds, List.of(span1), List.of(span2));\n\n    List<String> shortTraceIds =\n      traceIds.stream().map(t -> t.substring(16)).collect(Collectors.toList());\n    assertGetTracesReturnsEmpty(shortTraceIds);\n  }\n\n  /**\n   * Ideally, storage backends can deduplicate identical documents as this will prevent some\n   * analysis problems such as double-counting dependency links or other statistics. While this test\n   * exists, it is known not all backends will be able to cheaply make it pass. In other words, it\n   * is optional.\n   */\n  @Test protected void getTrace_deduplicates(TestInfo testInfo) throws Exception {\n    String testSuffix = testSuffix(testInfo);\n    Span span = spanBuilder(testSuffix).build();\n\n    // simulate a re-processed message\n    accept(span);\n    accept(span);\n\n    assertGetTraceReturns(span);\n  }\n}\n"
  },
  {
    "path": "zipkin-tests/src/test/java/zipkin2/storage/ITInMemoryStorage.java",
    "content": "/*\n * Copyright The OpenZipkin Authors\n * SPDX-License-Identifier: Apache-2.0\n */\npackage zipkin2.storage;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.TestInfo;\n\nclass ITInMemoryStorage {\n  @Nested\n  class ITTraces extends zipkin2.storage.ITTraces<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStore extends zipkin2.storage.ITSpanStore<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSpanStoreHeavy extends zipkin2.storage.ITSpanStoreHeavy<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITSearchEnabledFalse extends zipkin2.storage.ITSearchEnabledFalse<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITStrictTraceIdFalse extends zipkin2.storage.ITStrictTraceIdFalse<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITAutocompleteTags extends zipkin2.storage.ITAutocompleteTags<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITServiceAndSpanNames extends zipkin2.storage.ITServiceAndSpanNames<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependencies extends zipkin2.storage.ITDependencies<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n\n  @Nested\n  class ITDependenciesHeavy extends zipkin2.storage.ITDependenciesHeavy<InMemoryStorage> {\n    @Override protected StorageComponent.Builder newStorageBuilder(TestInfo testInfo) {\n      return InMemoryStorage.newBuilder();\n    }\n\n    @Override public void clear() {\n      storage.clear();\n    }\n  }\n}\n"
  }
]