diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c4ff052ff --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +#To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/docker-tests.yml b/.github/workflows/docker-tests.yml index 6e639eb8e..c638f45c6 100644 --- a/.github/workflows/docker-tests.yml +++ b/.github/workflows/docker-tests.yml @@ -1,8 +1,6 @@ name: e2e test on: push: - branches: - - main paths-ignore: - '**/*.md' pull_request_target: @@ -42,6 +40,8 @@ jobs: uses: hypertrace/github-actions/gradle@main with: args: dockerBuildImages + env: + IMAGE_TAG: ${{ github.sha }} - name: Verify hypertrace image working-directory: ./.github/workflows/hypertrace-ingester @@ -88,6 +88,8 @@ jobs: uses: hypertrace/github-actions/gradle@main with: args: dockerBuildImages + env: + IMAGE_TAG: ${{ github.sha }} - name: Verify hypertrace image working-directory: ./.github/workflows/hypertrace-ingester diff --git a/.github/workflows/merge-publish.yml b/.github/workflows/merge-publish.yml deleted file mode 100644 index f4b5e83ad..000000000 --- a/.github/workflows/merge-publish.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: merge-publish -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - merge-publish: - runs-on: ubuntu-20.04 - steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - - name: Check out code - uses: actions/checkout@v2.3.4 - with: - fetch-depth: 0 - - - name: create checksum file - uses: hypertrace/github-actions/checksum@main - - - name: Cache packages - uses: actions/cache@v2 - with: - path: ~/.gradle - key: gradle-packages-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/checksum.txt') }} - restore-keys: | - gradle-packages-${{ runner.os }}-${{ github.job }} - gradle-packages-${{ runner.os }} - - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_READ_USER }} - password: ${{ secrets.DOCKERHUB_READ_TOKEN }} - - - name: push docker image - uses: hypertrace/github-actions/gradle@main - with: - args: dockerPushImages - env: - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUBLISH_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUBLISH_TOKEN }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 1104f43b9..9191fafb4 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -1,24 +1,21 @@ name: build and validate on: push: - branches: - - main pull_request_target: branches: - - main - + - rzp_main jobs: - build: + validate-avros: runs-on: ubuntu-20.04 steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - name: Check out code uses: actions/checkout@v2.3.4 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} fetch-depth: 0 - + - name: create checksum file uses: hypertrace/github-actions/checksum@main @@ -31,24 +28,17 @@ jobs: gradle-packages-${{ runner.os }}-${{ github.job }} gradle-packages-${{ runner.os }} - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_READ_USER }} - password: ${{ secrets.DOCKERHUB_READ_TOKEN }} - - - name: Build with Gradle + - name: validate avros uses: hypertrace/github-actions/gradle@main - with: - args: build -x avroCompatibilityCheck dockerBuildImages - + with: + args: avroCompatibilityCheck validate-helm-charts: runs-on: ubuntu-20.04 - container: + container: image: hypertrace/helm-gcs-packager:0.3.1 credentials: - username: ${{ secrets.DOCKERHUB_READ_USER }} - password: ${{ secrets.DOCKERHUB_READ_TOKEN }} + username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation steps: - name: Check out code @@ -60,29 +50,10 @@ jobs: - name: validate charts run: ./.github/workflows/helm.sh validate - - snyk-scan: - runs-on: ubuntu-20.04 - steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - - name: Check out code - uses: actions/checkout@v2.3.4 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - fetch-depth: 0 - - name: Setup snyk - uses: snyk/actions/setup@0.3.0 - - name: Snyk test - run: snyk test --all-sub-projects --org=hypertrace --severity-threshold=low --policy-path=.snyk --configuration-matching='^runtimeClasspath$' --remote-repo-url='${{ github.server_url }}/${{ github.repository }}.git' - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - GRADLE_OPTS: -Dorg.gradle.workers.max=1 - - validate-avros: + build: runs-on: ubuntu-20.04 steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - name: Check out code uses: actions/checkout@v2.3.4 with: @@ -102,7 +73,42 @@ jobs: gradle-packages-${{ runner.os }}-${{ github.job }} gradle-packages-${{ runner.os }} - - name: validate avros + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + + - name: Build with Gradle uses: hypertrace/github-actions/gradle@main with: - args: avroCompatibilityCheck + args: build dockerBuildImages + env: + IMAGE_TAG: ${{ github.sha }} + + - name: push docker image + uses: hypertrace/github-actions/gradle@main + with: + args: dockerPushImages + env: + DOCKER_USERNAME: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + IMAGE_TAG: ${{ github.sha }} + +# snyk-scan: +# runs-on: ubuntu-20.04 +# steps: +# # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation +# - name: Check out code +# uses: actions/checkout@v2.3.4 +# with: +# ref: ${{github.event.pull_request.head.ref}} +# repository: ${{github.event.pull_request.head.repo.full_name}} +# fetch-depth: 0 +# - name: Setup snyk +# uses: snyk/actions/setup@0.3.0 +# - name: Snyk test +# run: snyk test --all-sub-projects --org=hypertrace --severity-threshold=low --policy-path=.snyk --configuration-matching='^runtimeClasspath$' --remote-repo-url='${{ github.server_url }}/${{ github.repository }}.git' +# env: +# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} +# GRADLE_OPTS: -Dorg.gradle.workers.max=1 diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index cd21d30f4..d751c5d75 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -1,20 +1,20 @@ name: test on: push: + pull_request_target: branches: - - main - pull_request: - + - rzp_main +# disabling while testing ci flow jobs: test: runs-on: ubuntu-20.04 steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - name: Check out code uses: actions/checkout@v2.3.4 with: fetch-depth: 0 - + - name: create checksum file uses: hypertrace/github-actions/checksum@main @@ -29,7 +29,7 @@ jobs: - name: Unit test uses: hypertrace/github-actions/gradle@main - with: + with: args: jacocoTestReport - name: Upload coverage to Codecov @@ -38,9 +38,10 @@ jobs: name: unit test reports flags: unit + - name: copy test reports uses: hypertrace/github-actions/gradle@main - with: + with: args: copyAllReports --output-dir=/tmp/test-reports - name: Archive test reports @@ -49,10 +50,10 @@ jobs: name: test-reports path: /tmp/test-reports if: always() - + - name: Publish Unit Test Results uses: docker://ghcr.io/enricomi/publish-unit-test-result-action:v1.6 if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository with: github_token: ${{ secrets.GITHUB_TOKEN }} - files: ./**/build/test-results/**/*.xml + files: ./**/build/test-results/**/*.xml \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 2dfeb5a5e..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Publish artifacts -on: -# Will only run when release is published. - release: - types: - - created - workflow_dispatch: - -jobs: - publish-artifacts: - runs-on: ubuntu-20.04 - steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - - name: Check out code - uses: actions/checkout@v2.3.4 - with: - fetch-depth: 0 - - - name: create checksum file - uses: hypertrace/github-actions/checksum@main - - - name: Cache packages - uses: actions/cache@v2 - with: - path: ~/.gradle - key: gradle-packages-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/checksum.txt') }} - restore-keys: | - gradle-packages-${{ runner.os }}-${{ github.job }} - gradle-packages-${{ runner.os }} - - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_READ_USER }} - password: ${{ secrets.DOCKERHUB_READ_TOKEN }} - - - name: publish docker image - uses: hypertrace/github-actions/gradle@main - with: - args: publish dockerPushImages - env: - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUBLISH_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUBLISH_TOKEN }} - ORG_GRADLE_PROJECT_artifactory_contextUrl: ${{ secrets.ARTIFACTORY_CONTEXT_URL }} - ORG_GRADLE_PROJECT_artifactory_user: ${{ secrets.ARTIFACTORY_PUBLISH_USER }} - ORG_GRADLE_PROJECT_artifactory_password: ${{ secrets.ARTIFACTORY_PUBLISH_TOKEN }} - - publish-helm-charts: - runs-on: ubuntu-20.04 - needs: publish-artifacts - container: - image: hypertrace/helm-gcs-packager:0.3.1 - credentials: - username: ${{ secrets.DOCKERHUB_READ_USER }} - password: ${{ secrets.DOCKERHUB_READ_TOKEN }} - steps: - # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation - - name: Checkout Repository - uses: actions/checkout@v2.3.4 - with: - fetch-depth: 0 - - - name: package and release charts - env: - HELM_GCS_CREDENTIALS: ${{ secrets.HELM_GCS_CREDENTIALS }} - HELM_GCS_REPOSITORY: ${{ secrets.HELM_GCS_REPOSITORY }} - run: | - ./.github/workflows/helm.sh package - ./.github/workflows/helm.sh publish - - publish-release-notes: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2.3.4 - with: - fetch-depth: 0 - - uses: hypertrace/github-actions/release-notes@main - with: - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..246a21f15 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release artifacts +on: +# Will run when a comment is added in PR e.g. /release v0.6.40-0.1.0-beta.1 + issue_comment: + types: [ created ] + workflow_dispatch: + +jobs: + publish-artifacts: + if: contains(github.event.comment.html_url, '/pull') && startsWith(github.event.comment.body, '/release v') + runs-on: ubuntu-20.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Check out code + uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + + - name: Set ENV variable + env: + RELEASE_VERSION_COMMENT: ${{ github.event.comment.body }} + run: | + echo "VERSION=${RELEASE_VERSION_COMMENT##/release\ v}" >> $GITHUB_ENV + echo "Setting tag version: ${VERSION}" + + - name: Build with Gradle + uses: hypertrace/github-actions/gradle@main + with: + args: build dockerBuildImages + env: + DOCKER_USERNAME: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + IMAGE_TAG: ${{ env.VERSION }} + + - name: push docker image + uses: hypertrace/github-actions/gradle@main + with: + args: dockerPushImages + env: + DOCKER_USERNAME: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + IMAGE_TAG: ${{ env.VERSION }} \ No newline at end of file diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..c73f68172 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,52 @@ +name: Security Checks +on: + workflow_dispatch: + pull_request: {} + push: + branches: ["rzp_main"] + schedule: + - cron: '30 20 * * *' +jobs: + semgrep: + name: Scan + runs-on: [ubuntu-latest] #nosemgrep zklW + steps: + - uses: actions/checkout@v2 + - uses: returntocorp/semgrep-action@v1 + with: + publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} + publishDeployment: 339 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + workflow_status: + runs-on: [ ubuntu-latest ] #nosemgrep zklW + name: Update Status Check + needs: [ semgrep ] + if: always() + env: + githubCommit: ${{ github.event.pull_request.head.sha }} + steps: + - name: Set github commit id + run: | + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "schedule" ]; then + echo "githubCommit=${{ github.sha }}" >> $GITHUB_ENV + fi + exit 0 + - name: Failed + id: failed + if: (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && github.ref != 'refs/heads/master' + run: | + echo 'Failing the workflow for github security status check.' + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "failure" , "context" : "github/security-status-check" , "description" : "github/security-status-check", "target_url" : "https://github.com/${{ github.repository }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ env.githubCommit }} + exit 1 + - name: Success + if: steps.failed.conclusion == 'skipped' || github.ref != 'refs/heads/master' + run: | + echo 'Status check has passed!' + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "success" , "context" : "github/security-status-check" , "description" : "github/security-status-check", "target_url" : "https://github.com/${{ github.repository }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ env.githubCommit }} + exit 0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..580aacfbe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +## Hypertrace-Ingester 0.6.40-0.2.0 + +Cut from commit: [7540fea](https://github.com/hypertrace/hypertrace-ingester/commit/7540fea697e541b89336cd2a9c1ae90a77322be2) + +###Changelog + +- Synthetic monitoring changes [c910426](https://github.com/hypertrace/hypertrace-ingester/commit/c910426f90a714c135a2d00f2a77b6167a6321dd). + +## Hypertrace-Ingester 0.6.40-0.3.0 + +Cut from commit: [161035d](https://github.com/razorpay/hypertrace-ingester/commit/161035dd05818631a439739a14a2d01e446d81be) + +###Changelog + +- Adds resource attribute enrichment via a new trace-enricher [pull/58](https://github.com/razorpay/hypertrace-ingester/pull/58). \ No newline at end of file diff --git a/buf.yaml b/buf.yaml index 0a27111fd..aee5a6109 100644 --- a/buf.yaml +++ b/buf.yaml @@ -2,10 +2,11 @@ build: roots: - hypertrace-trace-enricher/enriched-span-constants/src/main/proto - span-normalizer/raw-span-constants/src/main/proto + lint: use: - DEFAULT breaking: use: - PACKAGE - - WIRE_JSON \ No newline at end of file + - WIRE_JSON diff --git a/build.gradle.kts b/build.gradle.kts index 76b9368db..ce7333dc3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,4 +27,3 @@ subprojects { } } } - diff --git a/hypertrace-ingester/build.gradle.kts b/hypertrace-ingester/build.gradle.kts index 6057dc487..f7b46582c 100644 --- a/hypertrace-ingester/build.gradle.kts +++ b/hypertrace-ingester/build.gradle.kts @@ -17,11 +17,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } dependencies { diff --git a/hypertrace-metrics-exporter/hypertrace-metrics-exporter/build.gradle.kts b/hypertrace-metrics-exporter/hypertrace-metrics-exporter/build.gradle.kts index cee80f8a8..d99f1ecf1 100644 --- a/hypertrace-metrics-exporter/hypertrace-metrics-exporter/build.gradle.kts +++ b/hypertrace-metrics-exporter/hypertrace-metrics-exporter/build.gradle.kts @@ -13,11 +13,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } tasks.test { diff --git a/hypertrace-metrics-generator/hypertrace-metrics-generator/build.gradle.kts b/hypertrace-metrics-generator/hypertrace-metrics-generator/build.gradle.kts index 711e1371a..f628d219d 100644 --- a/hypertrace-metrics-generator/hypertrace-metrics-generator/build.gradle.kts +++ b/hypertrace-metrics-generator/hypertrace-metrics-generator/build.gradle.kts @@ -13,11 +13,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } tasks.test { diff --git a/hypertrace-metrics-processor/hypertrace-metrics-processor/build.gradle.kts b/hypertrace-metrics-processor/hypertrace-metrics-processor/build.gradle.kts index eade074f6..638323e82 100644 --- a/hypertrace-metrics-processor/hypertrace-metrics-processor/build.gradle.kts +++ b/hypertrace-metrics-processor/hypertrace-metrics-processor/build.gradle.kts @@ -13,11 +13,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } tasks.test { diff --git a/hypertrace-trace-enricher/enriched-span-constants/src/main/java/org/hypertrace/traceenricher/enrichedspan/constants/EnrichedSpanConstants.java b/hypertrace-trace-enricher/enriched-span-constants/src/main/java/org/hypertrace/traceenricher/enrichedspan/constants/EnrichedSpanConstants.java index 696b3875f..904aeb218 100644 --- a/hypertrace-trace-enricher/enriched-span-constants/src/main/java/org/hypertrace/traceenricher/enrichedspan/constants/EnrichedSpanConstants.java +++ b/hypertrace-trace-enricher/enriched-span-constants/src/main/java/org/hypertrace/traceenricher/enrichedspan/constants/EnrichedSpanConstants.java @@ -17,6 +17,7 @@ public class EnrichedSpanConstants { public static final String UNIQUE_API_NODES_COUNT = "unique.apis.count"; public static final String GRPC_REQUEST_URL = "grpc.request.url"; public static final String GRPC_REQUEST_ENDPOINT = "grpc.request.endpoint"; + public static final String INTERNAL_SVC_LATENCY = "enriched.serviceInternalProcessingTime"; /** * Returns the constant value for the given Enum. diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricher.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricher.java new file mode 100644 index 000000000..c2757275f --- /dev/null +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricher.java @@ -0,0 +1,80 @@ +package org.hypertrace.traceenricher.enrichment.enrichers; + +import static org.hypertrace.traceenricher.util.EnricherUtil.getResourceAttribute; + +import com.typesafe.config.Config; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import org.hypertrace.core.datamodel.AttributeValue; +import org.hypertrace.core.datamodel.Event; +import org.hypertrace.core.datamodel.StructuredTrace; +import org.hypertrace.traceenricher.enrichment.AbstractTraceEnricher; +import org.hypertrace.traceenricher.enrichment.clients.ClientRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Enricher to add resource attributes to the spans. As of now resource attributes are attached from + * process tags. + */ +public class ResourceAttributeEnricher extends AbstractTraceEnricher { + + private static final Logger LOGGER = LoggerFactory.getLogger(ResourceAttributeEnricher.class); + private static final String RESOURCE_ATTRIBUTES_CONFIG_KEY = "attributes"; + private static final String NODE_SELECTOR_KEY = "node.selector"; + private static final String ATTRIBUTES_TO_MATCH_CONFIG_KEY = "attributesToMatch"; + private List resourceAttributesToAdd = new ArrayList<>(); + private Map resourceAttributeKeysToMatch = new HashMap<>(); + + @Override + public void init(Config enricherConfig, ClientRegistry clientRegistry) { + resourceAttributesToAdd = enricherConfig.getStringList(RESOURCE_ATTRIBUTES_CONFIG_KEY); + resourceAttributeKeysToMatch = + enricherConfig.getConfig(ATTRIBUTES_TO_MATCH_CONFIG_KEY).entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().unwrapped().toString())); + } + + @Override + public void enrichEvent(StructuredTrace trace, Event event) { + try { + if (!isValidEvent(event)) { + return; + } + Map attributeMap = event.getAttributes().getAttributeMap(); + for (String resourceAttributeKey : resourceAttributesToAdd) { + String resourceAttributeKeyToMatch = resourceAttributeKey; + if (resourceAttributeKeysToMatch.containsKey(resourceAttributeKey)) { + resourceAttributeKeyToMatch = resourceAttributeKeysToMatch.get(resourceAttributeKey); + } + Optional resourceAttributeMaybe = + getResourceAttribute(trace, event, resourceAttributeKeyToMatch); + + resourceAttributeMaybe.ifPresent( + attributeValue -> + attributeMap.computeIfAbsent( + resourceAttributeKey, + key -> { + if (resourceAttributeKey.equals(NODE_SELECTOR_KEY)) { + attributeValue.setValue( + attributeValue + .getValue() + .substring(attributeValue.getValue().lastIndexOf('/') + 1)); + } + return attributeValue; + })); + } + } catch (Exception e) { + LOGGER.error( + "Exception while enriching event with resource attributes having event id: {}", + event.getEventId(), + e); + } + } + + private boolean isValidEvent(Event event) { + return (event.getResourceIndex() >= 0) + && (event.getAttributes() != null) + && (event.getAttributes().getAttributeMap() != null); + } +} diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricher.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricher.java new file mode 100644 index 000000000..e7fc4d622 --- /dev/null +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricher.java @@ -0,0 +1,104 @@ +package org.hypertrace.traceenricher.enrichment.enrichers; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.hypertrace.core.datamodel.ApiNodeEventEdge; +import org.hypertrace.core.datamodel.AttributeValue; +import org.hypertrace.core.datamodel.Event; +import org.hypertrace.core.datamodel.StructuredTrace; +import org.hypertrace.core.datamodel.shared.ApiNode; +import org.hypertrace.core.datamodel.shared.trace.AttributeValueCreator; +import org.hypertrace.traceenricher.enrichedspan.constants.EnrichedSpanConstants; +import org.hypertrace.traceenricher.enrichment.AbstractTraceEnricher; +import org.hypertrace.traceenricher.trace.util.ApiTraceGraph; +import org.hypertrace.traceenricher.trace.util.ApiTraceGraphBuilder; +import org.slf4j.LoggerFactory; + +public class ServiceInternalProcessingTimeEnricher extends AbstractTraceEnricher { + + private static final org.slf4j.Logger LOG = + LoggerFactory.getLogger(ServiceInternalProcessingTimeEnricher.class); + + public void enrichTrace(StructuredTrace trace) { + try { + ApiTraceGraph apiTraceGraph = ApiTraceGraphBuilder.buildGraph(trace); + List> apiNodeList = apiTraceGraph.getApiNodeList(); + for (ApiNode apiNode : apiNodeList) { + List exitApiBoundaryEvents = apiNode.getExitApiBoundaryEvents(); + List edges = apiTraceGraph.getOutboundEdgesForApiNode(apiNode); + int totalEdgeDurations = 0; + // Note: this logic of summing the duration of each child span does not work if children + // spans + // were + // concurrent to one-another. In that case, the parent span waits only for + // max(duration_child_1, + // duration_child2,...,duration_child_n) and not duration_child1 + duration_child_2 + + // duration_child_3 + // Works for: + // |------------------PARENT-------------------| + // |---C1---| + // |---C2---| + // |---C3---| + // Doesn't work for: + // |------------------PARENT-------------------| + // |---C1---| + // |---C2---| + // |---C3---| + for (var edge : edges) { + totalEdgeDurations += getApiNodeEventEdgeDuration(edge); + } + // now sum up http or https backends + double httpExitCallsSum = + exitApiBoundaryEvents.stream() + .filter( + event -> { + Map enrichedAttributes = + event.getEnrichedAttributes().getAttributeMap(); + return enrichedAttributes.containsKey("BACKEND_PROTOCOL") + && enrichedAttributes.get("BACKEND_PROTOCOL").getValue().contains("HTTP"); + }) + .mapToDouble(event -> event.getMetrics().getMetricMap().get("Duration").getValue()) + .sum(); + Optional entryApiBoundaryEventMaybe = apiNode.getEntryApiBoundaryEvent(); + if (entryApiBoundaryEventMaybe.isPresent()) { + var entryApiBoundaryEvent = entryApiBoundaryEventMaybe.get(); + var entryApiBoundaryEventDuration = getEventDuration(entryApiBoundaryEvent); + try { + entryApiBoundaryEvent + .getAttributes() + .getAttributeMap() + .put( + EnrichedSpanConstants.INTERNAL_SVC_LATENCY, + AttributeValueCreator.create( + String.valueOf( + entryApiBoundaryEventDuration + - totalEdgeDurations + - httpExitCallsSum))); + } catch (NullPointerException e) { + LOG.error( + "NPE while calculating service internal time. entryApiBoundaryEventDuration {}, totalEdgeDurations {}", + entryApiBoundaryEventDuration, + totalEdgeDurations, + e); + throw e; + } + } + } + } catch (Exception e) { + LOG.error("Exception while calculating service internal time"); + } + } + + private static Double getEventDuration(Event event) { + assert event.getMetrics().getMetricMap() != null; + assert event.getMetrics().getMetricMap().containsKey("Duration"); + return event.getMetrics().getMetricMap().get("Duration").getValue(); + } + + private static Double getApiNodeEventEdgeDuration(ApiNodeEventEdge edge) { + assert edge.getMetrics().getMetricMap() != null; + assert edge.getMetrics().getMetricMap().containsKey("Duration"); + return edge.getMetrics().getMetricMap().get("Duration").getValue(); + } +} diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/util/EnricherUtil.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/util/EnricherUtil.java index 2a4b32be6..e8568aee4 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/util/EnricherUtil.java +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/main/java/org/hypertrace/traceenricher/util/EnricherUtil.java @@ -1,11 +1,11 @@ package org.hypertrace.traceenricher.util; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import org.apache.commons.lang3.StringUtils; +import org.hypertrace.core.datamodel.Attributes; import org.hypertrace.core.datamodel.Event; +import org.hypertrace.core.datamodel.Resource; +import org.hypertrace.core.datamodel.StructuredTrace; import org.hypertrace.core.datamodel.shared.trace.AttributeValueCreator; import org.hypertrace.core.span.constants.RawSpanConstants; import org.hypertrace.core.span.constants.v1.SpanNamePrefix; @@ -39,6 +39,24 @@ public static Map getAttributesForFirstExistingKey( return Collections.unmodifiableMap(attributes); } + public static Optional getAttribute( + Attributes attributes, String key) { + return Optional.ofNullable(attributes) + .map(Attributes::getAttributeMap) + .map(attributeMap -> attributeMap.get(key)); + } + + public static Optional getResourceAttribute( + StructuredTrace trace, Event span, String key) { + if (span.getResourceIndex() < 0 || span.getResourceIndex() >= trace.getResourceList().size()) { + return Optional.empty(); + } + + return Optional.of(trace.getResourceList().get(span.getResourceIndex())) + .map(Resource::getAttributes) + .flatMap(attributes -> getAttribute(attributes, key)); + } + public static void setAttributeForFirstExistingKey( Event event, Builder entityBuilder, List attributeKeys) { for (String attributeKey : attributeKeys) { diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricherTest.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricherTest.java new file mode 100644 index 000000000..899c7e5b1 --- /dev/null +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ResourceAttributeEnricherTest.java @@ -0,0 +1,234 @@ +package org.hypertrace.traceenricher.enrichment.enrichers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.datamodel.*; +import org.hypertrace.traceenricher.enrichment.clients.ClientRegistry; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class ResourceAttributeEnricherTest extends AbstractAttributeEnricherTest { + + private final ResourceAttributeEnricher resourceAttributeEnricher = + new ResourceAttributeEnricher(); + + private List resourceAttributesToAddList; + + @BeforeAll + public void setup() { + String configFilePath = + Thread.currentThread().getContextClassLoader().getResource("enricher.conf").getPath(); + if (configFilePath == null) { + throw new RuntimeException("Cannot find enricher config file enricher.conf in the classpath"); + } + + Config fileConfig = ConfigFactory.parseFile(new File(configFilePath)); + Config configs = ConfigFactory.load(fileConfig); + if (!configs.hasPath("enricher.ResourceAttributeEnricher")) { + throw new RuntimeException( + "Cannot find enricher config for ResourceAttributeEnricher in " + configs); + } + Config enricherConfig = configs.getConfig("enricher.ResourceAttributeEnricher"); + resourceAttributesToAddList = enricherConfig.getStringList("attributes"); + resourceAttributeEnricher.init(enricherConfig, mock(ClientRegistry.class)); + } + + @Test + public void noResourceInTrace() { + // This trace has no resource attributes. + StructuredTrace trace = getBigTrace(); + for (Event event : trace.getEventList()) { + int attributeMapSize = 0; + if (event.getAttributes() != null && event.getAttributes().getAttributeMap() != null) { + attributeMapSize = event.getAttributes().getAttributeMap().size(); + } + resourceAttributeEnricher.enrichEvent(trace, event); + if (event.getAttributes() != null && event.getAttributes().getAttributeMap() != null) { + assertEquals(attributeMapSize, event.getAttributes().getAttributeMap().size()); + } + } + } + + @Test + public void traceWithResource() { + StructuredTrace structuredTrace = mock(StructuredTrace.class); + List resourceList = new ArrayList<>(); + + resourceList.add(getResource1()); + resourceList.add(getResource2()); + resourceList.add(getResource3()); + resourceList.add(getResource4()); + when(structuredTrace.getResourceList()).thenReturn(resourceList); + + Attributes attributes = Attributes.newBuilder().setAttributeMap(new HashMap<>()).build(); + Event event = + Event.newBuilder() + .setAttributes(attributes) + .setEventId(createByteBuffer("event1")) + .setCustomerId(TENANT_ID) + .build(); + event.setResourceIndex(0); + resourceAttributeEnricher.enrichEvent(structuredTrace, event); + assertEquals( + resourceAttributesToAddList.size() - 2, event.getAttributes().getAttributeMap().size()); + assertEquals( + "test-56f5d554c-5swkj", event.getAttributes().getAttributeMap().get("pod.name").getValue()); + assertEquals( + "01188498a468b5fef1eb4accd63533297c195a73", + event.getAttributes().getAttributeMap().get("service.version").getValue()); + assertEquals("10.21.18.171", event.getAttributes().getAttributeMap().get("ip").getValue()); + assertEquals( + "worker-hypertrace", + event.getAttributes().getAttributeMap().get("node.selector").getValue()); + + Event event2 = + Event.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(new HashMap<>()).build()) + .setEventId(createByteBuffer("event2")) + .setCustomerId(TENANT_ID) + .build(); + event2.setResourceIndex(1); + addAttribute(event2, "service.version", "123"); + addAttribute(event2, "cluster.name", "default"); + resourceAttributeEnricher.enrichEvent(structuredTrace, event2); + assertEquals( + resourceAttributesToAddList.size(), event2.getAttributes().getAttributeMap().size()); + assertEquals("123", event2.getAttributes().getAttributeMap().get("service.version").getValue()); + assertEquals( + "default", event2.getAttributes().getAttributeMap().get("cluster.name").getValue()); + assertEquals( + "worker-generic", event2.getAttributes().getAttributeMap().get("node.name").getValue()); + assertEquals( + "worker-generic", event2.getAttributes().getAttributeMap().get("node.selector").getValue()); + + Event event3 = + Event.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(new HashMap<>()).build()) + .setEventId(createByteBuffer("event3")) + .setCustomerId(TENANT_ID) + .build(); + event3.setResourceIndex(2); + resourceAttributeEnricher.enrichEvent(structuredTrace, event3); + assertEquals("", event3.getAttributes().getAttributeMap().get("node.selector").getValue()); + + Event event4 = + Event.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(new HashMap<>()).build()) + .setEventId(createByteBuffer("event4")) + .setCustomerId(TENANT_ID) + .build(); + event4.setResourceIndex(3); + resourceAttributeEnricher.enrichEvent(structuredTrace, event4); + assertEquals( + "worker-generic", event4.getAttributes().getAttributeMap().get("node.selector").getValue()); + assertEquals("pod1", event4.getAttributes().getAttributeMap().get("pod.name").getValue()); + } + + private Resource getResource4() { + Map resourceAttributeMap = + new HashMap<>() { + { + put("node.selector", AttributeValue.newBuilder().setValue("worker-generic").build()); + put("host.name", AttributeValue.newBuilder().setValue("pod1").build()); + } + }; + return Resource.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(resourceAttributeMap).build()) + .build(); + } + + private Resource getResource3() { + Map resourceAttributeMap = + new HashMap<>() { + { + put( + "node.selector", + AttributeValue.newBuilder() + .setValue("node-role.kubernetes.io/worker-generic/") + .build()); + } + }; + return Resource.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(resourceAttributeMap).build()) + .build(); + } + + private Resource getResource2() { + Map resourceAttributeMap = + new HashMap<>() { + { + put( + "service.version", + AttributeValue.newBuilder() + .setValue("018a468b5fef1eb4accd63533297c195a73") + .build()); + put("environment", AttributeValue.newBuilder().setValue("stage").build()); + put( + "opencensus.exporterversion", + AttributeValue.newBuilder().setValue("Jaeger-Go-2.23.1").build()); + put("host.name", AttributeValue.newBuilder().setValue("test1-56f5d554c-5swkj").build()); + put("ip", AttributeValue.newBuilder().setValue("10.21.18.1712").build()); + put("client-uuid", AttributeValue.newBuilder().setValue("53a112a715bdf86").build()); + put("node.name", AttributeValue.newBuilder().setValue("worker-generic").build()); + put( + "cluster.name", + AttributeValue.newBuilder().setValue("worker-generic-cluster").build()); + put( + "node.selector", + AttributeValue.newBuilder() + .setValue("node-role.kubernetes.io/worker-generic") + .build()); + } + }; + return Resource.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(resourceAttributeMap).build()) + .build(); + } + + private Resource getResource1() { + // In ideal scenarios below resource tags are present in spans. + Map resourceAttributeMap = + new HashMap<>() { + { + put( + "service.version", + AttributeValue.newBuilder() + .setValue("01188498a468b5fef1eb4accd63533297c195a73") + .build()); + put("environment", AttributeValue.newBuilder().setValue("stage").build()); + put( + "opencensus.exporterversion", + AttributeValue.newBuilder().setValue("Jaeger-Go-2.23.1").build()); + put("host.name", AttributeValue.newBuilder().setValue("test-56f5d554c-5swkj").build()); + put("ip", AttributeValue.newBuilder().setValue("10.21.18.171").build()); + put("client-uuid", AttributeValue.newBuilder().setValue("53a112a715bda986").build()); + put( + "node.selector", + AttributeValue.newBuilder() + .setValue("node-role.kubernetes.io/worker-hypertrace") + .build()); + } + }; + return Resource.newBuilder() + .setAttributes(Attributes.newBuilder().setAttributeMap(resourceAttributeMap).build()) + .build(); + } + + private void addAttribute(Event event, String key, String val) { + event + .getAttributes() + .getAttributeMap() + .put(key, AttributeValue.newBuilder().setValue(val).build()); + } +} diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricherTest.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricherTest.java new file mode 100644 index 000000000..5eeb13f2c --- /dev/null +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/enrichment/enrichers/ServiceInternalProcessingTimeEnricherTest.java @@ -0,0 +1,156 @@ +package org.hypertrace.traceenricher.enrichment.enrichers; + +import static java.util.stream.Collectors.toList; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.avro.file.DataFileReader; +import org.apache.avro.specific.SpecificDatumReader; +import org.hypertrace.core.datamodel.Event; +import org.hypertrace.core.datamodel.StructuredTrace; +import org.hypertrace.traceenricher.enrichedspan.constants.EnrichedSpanConstants; +import org.hypertrace.traceenricher.enrichment.Enricher; +import org.hypertrace.traceenricher.trace.util.ApiTraceGraph; +import org.hypertrace.traceenricher.trace.util.ApiTraceGraphBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@Disabled +public class ServiceInternalProcessingTimeEnricherTest extends AbstractAttributeEnricherTest { + + private final Enricher testCandidate = new ServiceInternalProcessingTimeEnricher(); + private StructuredTrace trace; + + @BeforeEach + public void setup() throws IOException { + URL resource = + Thread.currentThread().getContextClassLoader().getResource("StructuredTrace-Hotrod.avro"); + SpecificDatumReader datumReader = + new SpecificDatumReader<>(StructuredTrace.getClassSchema()); + DataFileReader dfrStructuredTrace = + new DataFileReader<>(new File(resource.getPath()), datumReader); + trace = dfrStructuredTrace.next(); + dfrStructuredTrace.close(); + } + + @Test + public void validateServiceInternalTimeAttributeInEntrySpans() { + // this trace has 12 api nodes + // api edges + // 0 -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + // backend exit + // 1 -> to redis 13 exit calls + // 2 -> to mysql 1 exit call + // for events parts of api_node 0, there should 12 exit calls + // for events parts of api_node 1, there should be 13 exit calls + // for events parts of api_node 2, there should be 1 exit calls + // verify exit call count per service per api_trace + // this trace has 4 services + // frontend service has 1 api_entry span and that api_node has 12 exit calls [drive: 1, + // customer: 1, route: 10] + // setup + ApiTraceGraph apiTraceGraph = ApiTraceGraphBuilder.buildGraph(trace); + var apiNodes = apiTraceGraph.getApiNodeList(); + // Assert preconditions + Assertions.assertEquals(13, apiNodes.size()); + apiNodes.forEach( + apiNode -> Assertions.assertTrue(apiNode.getEntryApiBoundaryEvent().isPresent())); + List serviceNames = + apiNodes.stream() + .map( + apiNode -> { + Assertions.assertTrue(apiNode.getEntryApiBoundaryEvent().isPresent()); + return apiNode.getEntryApiBoundaryEvent().get().getServiceName(); + }) + .collect(toList()); + Assertions.assertTrue(serviceNames.contains("frontend")); + Assertions.assertTrue(serviceNames.contains("driver")); + Assertions.assertTrue(serviceNames.contains("customer")); + Assertions.assertTrue(serviceNames.contains("route")); + // execute + testCandidate.enrichTrace(trace); + // assertions: All entry spans should have this tag + apiTraceGraph + .getApiNodeList() + .forEach( + a -> + Assertions.assertTrue( + a.getEntryApiBoundaryEvent() + .get() + .getAttributes() + .getAttributeMap() + .containsKey(EnrichedSpanConstants.INTERNAL_SVC_LATENCY))); + } + + @Test + public void validateServiceInternalTimeValueInSpans() { + ApiTraceGraph apiTraceGraph = ApiTraceGraphBuilder.buildGraph(trace); + var apiNodes = apiTraceGraph.getApiNodeList(); + List entryApiBoundaryEvents = + apiNodes.stream().map(a -> a.getEntryApiBoundaryEvent().get()).collect(toList()); + // assert pre-conditions + Assertions.assertEquals(13, entryApiBoundaryEvents.size()); + // execute + testCandidate.enrichTrace(trace); + // All three services below don't have any exit calls to API, only backends. We assert that the + // time of these exit spans is + // not subtracted from the entry span. + var entryEventsForRouteSvc = getEntryEventsForService(entryApiBoundaryEvents, "route"); + for (Event event : entryEventsForRouteSvc) { + Assertions.assertEquals( + getEventDuration(event), + event + .getAttributes() + .getAttributeMap() + .get(EnrichedSpanConstants.INTERNAL_SVC_LATENCY) + .getValue()); + } + var entryEventsForCustomerSvc = getEntryEventsForService(entryApiBoundaryEvents, "customer"); + for (Event event : entryEventsForCustomerSvc) { + Assertions.assertEquals( + getEventDuration(event), + event + .getAttributes() + .getAttributeMap() + .get(EnrichedSpanConstants.INTERNAL_SVC_LATENCY) + .getValue()); + } + var entryEventsDriverSvc = getEntryEventsForService(entryApiBoundaryEvents, "driver"); + for (Event event : entryEventsDriverSvc) { + Assertions.assertEquals( + getEventDuration(event), + event + .getAttributes() + .getAttributeMap() + .get(EnrichedSpanConstants.INTERNAL_SVC_LATENCY) + .getValue()); + } + var entryEventForFrontendSvc = + getEntryEventsForService(entryApiBoundaryEvents, "frontend").get(0); + // total outbound edge duration = 1016ms + // entry event duration = 678ms + Assertions.assertEquals( + "-335.0", + entryEventForFrontendSvc + .getAttributes() + .getAttributeMap() + .get(EnrichedSpanConstants.INTERNAL_SVC_LATENCY) + .getValue()); + } + + private static List getEntryEventsForService( + List entryApiBoundaryEvents, String service) { + return entryApiBoundaryEvents.stream() + .filter(a -> a.getServiceName().equals(service)) + .collect(Collectors.toList()); + } + + private static String getEventDuration(Event event) { + return String.valueOf(event.getMetrics().getMetricMap().get("Duration").getValue()); + } +} diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/util/EnricherUtilTest.java b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/util/EnricherUtilTest.java index 701982a3e..d4f105926 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/util/EnricherUtilTest.java +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/java/org/hypertrace/traceenricher/util/EnricherUtilTest.java @@ -7,6 +7,8 @@ import java.util.Arrays; import java.util.Map; +import java.util.Optional; +import org.hypertrace.core.datamodel.AttributeValue; import org.hypertrace.core.datamodel.Attributes; import org.hypertrace.core.datamodel.Event; import org.hypertrace.entity.data.service.v1.Entity; @@ -51,4 +53,25 @@ public void testSetAttributeForFirstExistingKey() { EnricherUtil.setAttributeForFirstExistingKey(e, entityBuilder, Arrays.asList("a", "b", "c")); Assertions.assertTrue(entityBuilder.getAttributesMap().containsKey("a")); } + + @Test + public void testGetAttribute() { + Attributes attributes = + Attributes.newBuilder() + .setAttributeMap( + Map.of( + "a", + TestUtil.buildAttributeValue("a-value"), + "b", + TestUtil.buildAttributeValue("b-value"))) + .build(); + Optional val = EnricherUtil.getAttribute(attributes, "a"); + Assertions.assertEquals("a-value", val.get().getValue()); + val = EnricherUtil.getAttribute(attributes, "c"); + Assertions.assertTrue(val.isEmpty()); + + attributes = null; + val = EnricherUtil.getAttribute(attributes, "a"); + Assertions.assertTrue(val.isEmpty()); + } } diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/enricher.conf b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/enricher.conf index ea6b6bd98..2b92ddd01 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/enricher.conf +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/enricher.conf @@ -1,5 +1,5 @@ enricher { - names = ["SpanTypeAttributeEnricher", "ApiStatusEnricher", "EndpointEnricher", "TransactionNameEnricher", "ApiBoundaryTypeAttributeEnricher", "ErrorsAndExceptionsEnricher", "BackendEntityEnricher", "HttpAttributeEnricher", "DefaultServiceEntityEnricher", "UserAgentSpanEnricher"] + names = ["SpanTypeAttributeEnricher", "ApiStatusEnricher", "EndpointEnricher", "TransactionNameEnricher", "ApiBoundaryTypeAttributeEnricher", "ErrorsAndExceptionsEnricher", "BackendEntityEnricher", "HttpAttributeEnricher", "DefaultServiceEntityEnricher", "UserAgentSpanEnricher", "ServiceInternalProcessingTimeEnricher"] DefaultServiceEntityEnricher { class = "org.hypertrace.traceenricher.enrichment.enrichers.DefaultServiceEntityEnricher" @@ -61,4 +61,18 @@ enricher { class = "org.hypertrace.traceenricher.enrichment.enrichers.GrpcAttributeEnricher" dependencies = ["SpanTypeAttributeEnricher", "ApiBoundaryTypeAttributeEnricher"] } + + ServiceInternalProcessingTimeEnricher { + class = "org.hypertrace.traceenricher.enrichment.enrichers.ServiceInternalProcessingTimeEnricher" + dependencies = ["SpanTypeAttributeEnricher", "ApiBoundaryTypeAttributeEnricher"] + } + + ResourceAttributeEnricher { + class = "org.hypertrace.traceenricher.enrichment.enrichers.ResourceAttributeEnricher" + attributes = ["pod.name","node.name","cluster.name","ip","service.version","node.selector"] + attributesToMatch { + pod.name = "host.name" + } + } + } \ No newline at end of file diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/missing-downstream-entry-spans/after-enrichment.json b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/missing-downstream-entry-spans/after-enrichment.json index 37028d617..78753e446 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/missing-downstream-entry-spans/after-enrichment.json +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher-impl/src/test/resources/missing-downstream-entry-spans/after-enrichment.json @@ -55,6 +55,14 @@ "binary_value": null, "value_list": null, "value_map": null + }, + "enriched.serviceInternalProcessingTime":{ + "value": { + "string": "4041.0" + }, + "binary_value":null, + "value_list":null, + "value_map":null } } } diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher/build.gradle.kts b/hypertrace-trace-enricher/hypertrace-trace-enricher/build.gradle.kts index 943f53156..77f31e91b 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher/build.gradle.kts +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher/build.gradle.kts @@ -12,11 +12,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } // Config for gw run to be able to run this locally. Just execute gw run here on Intellij or on the console. diff --git a/hypertrace-trace-enricher/hypertrace-trace-enricher/src/main/resources/configs/common/application.conf b/hypertrace-trace-enricher/hypertrace-trace-enricher/src/main/resources/configs/common/application.conf index b9c237b0b..cd42572c7 100644 --- a/hypertrace-trace-enricher/hypertrace-trace-enricher/src/main/resources/configs/common/application.conf +++ b/hypertrace-trace-enricher/hypertrace-trace-enricher/src/main/resources/configs/common/application.conf @@ -24,7 +24,7 @@ kafka.streams.config = { } enricher { - names = ["SpanTypeAttributeEnricher", "ApiStatusEnricher", "EndpointEnricher", "TransactionNameEnricher", "ApiBoundaryTypeAttributeEnricher", "ErrorsAndExceptionsEnricher", "BackendEntityEnricher", "HttpAttributeEnricher", "DefaultServiceEntityEnricher", "UserAgentSpanEnricher", "SpaceEnricher", "EntitySpanEnricher", "ExitCallsEnricher", "TraceStatsEnricher", "GrpcAttributeEnricher"] + names = ["SpanTypeAttributeEnricher", "ApiStatusEnricher", "EndpointEnricher", "TransactionNameEnricher", "ApiBoundaryTypeAttributeEnricher", "ErrorsAndExceptionsEnricher", "BackendEntityEnricher", "HttpAttributeEnricher", "DefaultServiceEntityEnricher", "UserAgentSpanEnricher", "SpaceEnricher", "EntitySpanEnricher", "ExitCallsEnricher", "TraceStatsEnricher", "GrpcAttributeEnricher", "ServiceInternalProcessingTimeEnricher", "ResourceAttributeEnricher"] clients = { entity.service.config = { @@ -115,6 +115,20 @@ enricher { class = "org.hypertrace.traceenricher.enrichment.enrichers.GrpcAttributeEnricher" dependencies = ["SpanTypeAttributeEnricher", "ApiBoundaryTypeAttributeEnricher"] } + + ServiceInternalProcessingTimeEnricher { + class = "org.hypertrace.traceenricher.enrichment.enrichers.ServiceInternalProcessingTimeEnricher" + dependencies = ["SpanTypeAttributeEnricher", "ApiBoundaryTypeAttributeEnricher"] + } + + ResourceAttributeEnricher { + class = "org.hypertrace.traceenricher.enrichment.enrichers.ResourceAttributeEnricher" + attributes = ["pod.name","node.name","cluster.name","ip","service.version","node.selector"] + attributesToMatch { + pod.name = "host.name" + } + } + } logger { diff --git a/hypertrace-view-generator/hypertrace-view-generator/build.gradle.kts b/hypertrace-view-generator/hypertrace-view-generator/build.gradle.kts index c1a5def90..757a5ffec 100644 --- a/hypertrace-view-generator/hypertrace-view-generator/build.gradle.kts +++ b/hypertrace-view-generator/hypertrace-view-generator/build.gradle.kts @@ -13,11 +13,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { - serviceName.set("all-views") + serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } tasks.test { diff --git a/raw-spans-grouper/raw-spans-grouper/build.gradle.kts b/raw-spans-grouper/raw-spans-grouper/build.gradle.kts index 545c15579..3cfa0d820 100644 --- a/raw-spans-grouper/raw-spans-grouper/build.gradle.kts +++ b/raw-spans-grouper/raw-spans-grouper/build.gradle.kts @@ -12,11 +12,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } // Config for gw run to be able to run this locally. Just execute gw run here on Intellij or on the console. diff --git a/span-normalizer/span-normalizer/build.gradle.kts b/span-normalizer/span-normalizer/build.gradle.kts index f94a7f667..b9ec9f13a 100644 --- a/span-normalizer/span-normalizer/build.gradle.kts +++ b/span-normalizer/span-normalizer/build.gradle.kts @@ -13,10 +13,14 @@ application { hypertraceDocker { defaultImage { + imageName.set("hypertrace-ingester") javaApplication { + serviceName.set("${project.name}") adminPort.set(8099) } + namespace.set("razorpay") } + tag("${project.name}" + "_" + System.getenv("IMAGE_TAG")) } // Config for gw run to be able to run this locally. Just execute gw run here on Intellij or on the console. diff --git a/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/span/normalizer/constants/SpanNormalizerConstants.java b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/span/normalizer/constants/SpanNormalizerConstants.java index 058ebc408..521944afb 100644 --- a/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/span/normalizer/constants/SpanNormalizerConstants.java +++ b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/span/normalizer/constants/SpanNormalizerConstants.java @@ -5,4 +5,9 @@ public class SpanNormalizerConstants { public static final String OUTPUT_TOPIC_CONFIG_KEY = "output.topic"; public static final String OUTPUT_TOPIC_RAW_LOGS_CONFIG_KEY = "raw.logs.output.topic"; public static final String SPAN_NORMALIZER_JOB_CONFIG = "span-normalizer-job-config"; + public static final String BYPASS_OUTPUT_TOPIC_CONFIG_KEY = "bypass.output.topic"; + public static final String PII_KEYS_CONFIG_KEY = "pii.keys"; + public static final String PII_REGEX_CONFIG_KEY = "pii.regex"; + public static final String PII_FIELD_REDACTED_VAL = "[redacted]"; + public static final String CONTAINS_PII_TAGS_KEY = "containsPIITags"; } diff --git a/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizer.java b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizer.java index e3b490544..1048ab2b6 100644 --- a/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizer.java +++ b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizer.java @@ -1,13 +1,16 @@ package org.hypertrace.core.spannormalizer.jaeger; import static org.hypertrace.core.datamodel.shared.AvroBuilderCache.fastNewBuilder; +import static org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry.registerCounter; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ProtocolStringList; import com.google.protobuf.util.Timestamps; import com.typesafe.config.Config; import io.jaegertracing.api_v2.JaegerSpanInternalModel; import io.jaegertracing.api_v2.JaegerSpanInternalModel.KeyValue; import io.jaegertracing.api_v2.JaegerSpanInternalModel.Span; +import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Timer; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -21,6 +24,8 @@ import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -45,23 +50,32 @@ import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; import org.hypertrace.core.span.constants.RawSpanConstants; import org.hypertrace.core.span.constants.v1.JaegerAttribute; +import org.hypertrace.core.spannormalizer.constants.SpanNormalizerConstants; +import org.hypertrace.core.spannormalizer.jaeger.tenant.PIIMatchType; import org.hypertrace.core.spannormalizer.util.JaegerHTTagsConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class JaegerSpanNormalizer { + private static final Logger LOG = LoggerFactory.getLogger(JaegerSpanNormalizer.class); /** Service name can be sent against this key as well */ public static final String OLD_JAEGER_SERVICENAME_KEY = "jaeger.servicename"; private static final String SPAN_NORMALIZATION_TIME_METRIC = "span.normalization.time"; + private static final String SPAN_REDACTED_ATTRIBUTES_COUNTER = "span.redacted.attributes"; + private final Map spanAttributesRedactedCounters = new ConcurrentHashMap<>(); private static JaegerSpanNormalizer INSTANCE; private final ConcurrentMap tenantToSpanNormalizationTimer = new ConcurrentHashMap<>(); + private final JaegerResourceNormalizer resourceNormalizer = new JaegerResourceNormalizer(); private final TenantIdHandler tenantIdHandler; + private AttributeValue redactedAttributeValue = null; + private final Set tagKeysToRedact = new HashSet<>(); + private final Set tagRegexPatternToRedact = new HashSet<>(); public static JaegerSpanNormalizer get(Config config) { if (INSTANCE == null) { @@ -75,6 +89,30 @@ public static JaegerSpanNormalizer get(Config config) { } public JaegerSpanNormalizer(Config config) { + try { + if (config.hasPath(SpanNormalizerConstants.PII_KEYS_CONFIG_KEY)) { + config.getStringList(SpanNormalizerConstants.PII_KEYS_CONFIG_KEY).stream() + .map(String::toUpperCase) + .forEach(tagKeysToRedact::add); + redactedAttributeValue = + AttributeValue.newBuilder() + .setValue(SpanNormalizerConstants.PII_FIELD_REDACTED_VAL) + .build(); + } + if (config.hasPath(SpanNormalizerConstants.PII_REGEX_CONFIG_KEY)) { + config.getStringList(SpanNormalizerConstants.PII_REGEX_CONFIG_KEY).stream() + .map(Pattern::compile) + .forEach(tagRegexPatternToRedact::add); + if (redactedAttributeValue == null) { + redactedAttributeValue = + AttributeValue.newBuilder() + .setValue(SpanNormalizerConstants.PII_FIELD_REDACTED_VAL) + .build(); + } + } + } catch (Exception e) { + LOG.error("An exception occurred while loading redaction configs: ", e); + } this.tenantIdHandler = new TenantIdHandler(config); } @@ -82,6 +120,10 @@ public Timer getSpanNormalizationTimer(String tenantId) { return tenantToSpanNormalizationTimer.get(tenantId); } + public Map getSpanAttributesRedactedCounters() { + return spanAttributesRedactedCounters; + } + @Nullable public RawSpan convert(String tenantId, Span jaegerSpan) throws Exception { Map tags = @@ -118,15 +160,121 @@ private Callable getRawSpanNormalizerCallable( .normalize(jaegerSpan, tenantIdHandler.getTenantIdProvider().getTenantIdTagKey()) .ifPresent(rawSpanBuilder::setResource); + // redact PII tags, tag comparisons are case insensitive (Resource tags are skipped) + if (redactedAttributeValue != null) { + sanitiseSpan(rawSpanBuilder); + } + // build raw span RawSpan rawSpan = rawSpanBuilder.build(); if (LOG.isDebugEnabled()) { logSpanConversion(jaegerSpan, rawSpan); } + return rawSpan; }; } + private void sanitiseSpan(Builder rawSpanBuilder) { + + try { + var attributeMap = rawSpanBuilder.getEvent().getAttributes().getAttributeMap(); + var spanServiceName = rawSpanBuilder.getEvent().getServiceName(); + Set tagKeys = attributeMap.keySet(); + + AtomicReference containsPIIFields = new AtomicReference<>(); + containsPIIFields.set(false); + + try { + tagKeys.stream() + .filter( + tagKey -> + tagKeysToRedact.contains(tagKey.toUpperCase()) + || attributeMap + .get(tagKey) + .getValue() + .equals(SpanNormalizerConstants.PII_FIELD_REDACTED_VAL)) + .peek( + tagKey -> { + containsPIIFields.set(true); + spanAttributesRedactedCounters + .computeIfAbsent( + PIIMatchType.KEY.toString(), + k -> + registerCounter( + SPAN_REDACTED_ATTRIBUTES_COUNTER, + Map.of("matchType", PIIMatchType.KEY.toString()))) + .increment(); + }) + .forEach( + tagKey -> { + logSpanRedaction(tagKey, spanServiceName, PIIMatchType.KEY); + attributeMap.put(tagKey, redactedAttributeValue); + }); + } catch (Exception e) { + LOG.error("An exception occurred while sanitising spans with key match: ", e); + } + + try { + for (Pattern pattern : tagRegexPatternToRedact) { + for (String tagKey : tagKeys) { + if (pattern.matcher(attributeMap.get(tagKey).getValue()).matches()) { + containsPIIFields.set(true); + spanAttributesRedactedCounters + .computeIfAbsent( + PIIMatchType.REGEX.toString(), + k -> + registerCounter( + SPAN_REDACTED_ATTRIBUTES_COUNTER, + Map.of("matchType", PIIMatchType.REGEX.toString()))) + .increment(); + logSpanRedaction(tagKey, spanServiceName, PIIMatchType.REGEX); + attributeMap.put(tagKey, redactedAttributeValue); + } + } + } + } catch (Exception e) { + LOG.error("An exception occurred while sanitising spans with regex match: ", e); + } + + // if the trace contains PII field, add a field to indicate this. We can later slice-and-dice + // based on this tag + if (containsPIIFields.get()) { + rawSpanBuilder + .getEvent() + .getAttributes() + .getAttributeMap() + .put( + SpanNormalizerConstants.CONTAINS_PII_TAGS_KEY, + AttributeValue.newBuilder().setValue("true").build()); + } + } catch (Exception e) { + LOG.error("An exception occurred while sanitising spans: ", e); + } + } + + private void logSpanRedaction(String tagKey, String spanServiceName, PIIMatchType matchType) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug( + new ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString( + Map.of( + "bookmark", + "REDACTED_KEY", + "key", + tagKey, + "matchtype", + matchType.toString(), + "serviceName", + spanServiceName))); + } + } catch (Exception e) { + LOG.error("An exception occurred while logging span redaction: ", e); + } + } + /** * Builds the event object from the jaeger span. Note: tagsMap should contain keys that have * already been converted to lowercase by the caller. diff --git a/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/tenant/PIIMatchType.java b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/tenant/PIIMatchType.java new file mode 100644 index 000000000..9ef532faf --- /dev/null +++ b/span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/tenant/PIIMatchType.java @@ -0,0 +1,6 @@ +package org.hypertrace.core.spannormalizer.jaeger.tenant; + +public enum PIIMatchType { + KEY, + REGEX +} diff --git a/span-normalizer/span-normalizer/src/main/resources/configs/common/application.conf b/span-normalizer/span-normalizer/src/main/resources/configs/common/application.conf index 0a24d0e29..1d08b5342 100644 --- a/span-normalizer/span-normalizer/src/main/resources/configs/common/application.conf +++ b/span-normalizer/span-normalizer/src/main/resources/configs/common/application.conf @@ -33,3 +33,8 @@ logger.file.dir = "/var/logs/span-normalizer" metrics.reporter.prefix = org.hypertrace.core.spannormalizer.jobSpanNormalizer metrics.reporter.names = ["prometheus"] metrics.reportInterval = 60 + +pii = { + keys = ["authorization"] + regex = [] +} \ No newline at end of file diff --git a/span-normalizer/span-normalizer/src/test/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizerTest.java b/span-normalizer/span-normalizer/src/test/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizerTest.java index 4020d1af3..c05372151 100644 --- a/span-normalizer/span-normalizer/src/test/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizerTest.java +++ b/span-normalizer/span-normalizer/src/test/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizerTest.java @@ -7,6 +7,7 @@ import io.jaegertracing.api_v2.JaegerSpanInternalModel.KeyValue; import io.jaegertracing.api_v2.JaegerSpanInternalModel.Process; import io.jaegertracing.api_v2.JaegerSpanInternalModel.Span; +import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Timer; import java.io.IOException; import java.lang.reflect.Field; @@ -21,6 +22,9 @@ import org.hypertrace.core.span.constants.RawSpanConstants; import org.hypertrace.core.span.constants.v1.JaegerAttribute; import org.hypertrace.core.spannormalizer.SpanNormalizer; +import org.hypertrace.core.spannormalizer.constants.SpanNormalizerConstants; +import org.hypertrace.core.spannormalizer.jaeger.tenant.PIIMatchType; +import org.hypertrace.core.spannormalizer.utils.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -28,6 +32,7 @@ import org.junitpioneer.jupiter.SetEnvironmentVariable; public class JaegerSpanNormalizerTest { + private final Random random = new Random(); @BeforeAll @@ -76,7 +81,11 @@ private Map getCommonConfig() { "bootstrap.servers", "localhost:9092"), "schema.registry.config", - Map.of("schema.registry.url", "http://localhost:8081")); + Map.of("schema.registry.url", "http://localhost:8081"), + "pii.keys", + List.of("http.method", "http.url", "amount", "Authorization"), + "pii.regex", + List.of("^(?:(?:\\+|0{0,2})91(\\s*[\\-]\\s*)?|[0]?)?[789]\\d{9}$")); } @Test @@ -228,4 +237,130 @@ public void testConvertToJsonString() throws IOException { "{\"value\":{\"string\":\"test-val\"},\"binary_value\":null,\"value_list\":null,\"value_map\":null}", JaegerSpanNormalizer.convertToJsonString(attributeValue, AttributeValue.getClassSchema())); } + + @Test + public void testPiiFieldRedaction() throws Exception { + String tenantId = "tenant-" + random.nextLong(); + Map configs = new HashMap<>(getCommonConfig()); + configs.putAll(Map.of("processor", Map.of("defaultTenantId", tenantId))); + JaegerSpanNormalizer normalizer = JaegerSpanNormalizer.get(ConfigFactory.parseMap(configs)); + Process process = Process.newBuilder().build(); + Span span = + Span.newBuilder() + .setProcess(process) + .addTags(0, KeyValue.newBuilder().setKey("http.method").setVStr("GET").build()) + .addTags(1, KeyValue.newBuilder().setKey("http.url").setVStr("hypertrace.org")) + .addTags(2, KeyValue.newBuilder().setKey("kind").setVStr("client")) + .addTags(3, KeyValue.newBuilder().setKey("authorization").setVStr("authToken").build()) + .addTags(4, KeyValue.newBuilder().setKey("amount").setVInt64(2300).build()) + .addTags(5, KeyValue.newBuilder().setKey("phoneNum").setVStr("+919123456780").build()) + .addTags(6, KeyValue.newBuilder().setKey("phoneNum1").setVStr("7123456980").build()) + .addTags(7, KeyValue.newBuilder().setKey("phoneNum2").setVStr("+1234567890").build()) + .addTags(8, KeyValue.newBuilder().setKey("phoneNum3").setVStr("123456789").build()) + .addTags(9, KeyValue.newBuilder().setKey("otp").setVStr("[redacted]").build()) + .build(); + + RawSpan rawSpan = normalizer.convert(tenantId, span); + + var attributes = rawSpan.getEvent().getAttributes().getAttributeMap(); + Map counterMap = normalizer.getSpanAttributesRedactedCounters(); + + Assertions.assertEquals("client", attributes.get("kind").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("http.url").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("http.method").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("amount").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("authorization").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("phonenum").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("phonenum1").getValue()); + Assertions.assertEquals("+1234567890", attributes.get("phonenum2").getValue()); + Assertions.assertEquals("123456789", attributes.get("phonenum3").getValue()); + Assertions.assertEquals("123456789", attributes.get("phonenum3").getValue()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("otp").getValue()); + Assertions.assertEquals(5.0, counterMap.get(PIIMatchType.KEY.toString()).count()); + Assertions.assertEquals(2.0, counterMap.get(PIIMatchType.REGEX.toString()).count()); + Assertions.assertTrue(attributes.containsKey(SpanNormalizerConstants.CONTAINS_PII_TAGS_KEY)); + Assertions.assertEquals( + "true", attributes.get(SpanNormalizerConstants.CONTAINS_PII_TAGS_KEY).getValue()); + + span = + Span.newBuilder() + .setProcess(process) + .addTags(0, KeyValue.newBuilder().setKey("otp").setVStr("[redacted]").build()) + .build(); + + rawSpan = normalizer.convert(tenantId, span); + attributes = rawSpan.getEvent().getAttributes().getAttributeMap(); + counterMap = normalizer.getSpanAttributesRedactedCounters(); + + Assertions.assertEquals(6.0, counterMap.get(PIIMatchType.KEY.toString()).count()); + Assertions.assertEquals(2.0, counterMap.get(PIIMatchType.REGEX.toString()).count()); + Assertions.assertEquals( + SpanNormalizerConstants.PII_FIELD_REDACTED_VAL, attributes.get("otp").getValue()); + Assertions.assertEquals( + "true", attributes.get(SpanNormalizerConstants.CONTAINS_PII_TAGS_KEY).getValue()); + } + + @Test + public void testPiiFieldRedactionWithNoConfig() throws Exception { + String tenantId = "tenant-" + random.nextLong(); + + Map config = + Map.of( + "span.type", + "jaeger", + "input.topic", + "jaeger-spans", + "output.topic", + "raw-spans-from-jaeger-spans", + "kafka.streams.config", + Map.of( + "application.id", + "jaeger-spans-to-raw-spans-job", + "bootstrap.servers", + "localhost:9092"), + "schema.registry.config", + Map.of("schema.registry.url", "http://localhost:8081")); + + Map configs = new HashMap<>(config); + configs.putAll(Map.of("processor", Map.of("defaultTenantId", tenantId))); + JaegerSpanNormalizer normalizer = JaegerSpanNormalizer.get(ConfigFactory.parseMap(configs)); + Process process = Process.newBuilder().build(); + Span span = + Span.newBuilder() + .setProcess(process) + .addTags(0, TestUtils.createKeyValue("http.method", "GET")) + .addTags(1, TestUtils.createKeyValue("http.url", "hypertrace.org")) + .addTags(2, TestUtils.createKeyValue("kind", "client")) + .addTags(3, TestUtils.createKeyValue("authorization", "authToken")) + .addTags(4, TestUtils.createKeyValue("amount", 2300)) + .addTags(5, KeyValue.newBuilder().setKey("phoneNum").setVStr("+919123456780").build()) + .addTags(6, KeyValue.newBuilder().setKey("phoneNum1").setVStr("7123456980").build()) + .addTags(7, KeyValue.newBuilder().setKey("phoneNum2").setVStr("+1234567890").build()) + .addTags(8, KeyValue.newBuilder().setKey("phoneNum3").setVStr("123456789").build()) + .build(); + + RawSpan rawSpan = normalizer.convert(tenantId, span); + + var attributes = rawSpan.getEvent().getAttributes().getAttributeMap(); + Map counterMap = normalizer.getSpanAttributesRedactedCounters(); + + Assertions.assertEquals("client", attributes.get("kind").getValue()); + Assertions.assertEquals("hypertrace.org", attributes.get("http.url").getValue()); + Assertions.assertEquals("GET", attributes.get("http.method").getValue()); + Assertions.assertEquals(2300, Integer.valueOf(attributes.get("amount").getValue())); + Assertions.assertEquals("authToken", attributes.get("authorization").getValue()); + Assertions.assertEquals("+919123456780", attributes.get("phonenum").getValue()); + Assertions.assertEquals("7123456980", attributes.get("phonenum1").getValue()); + Assertions.assertEquals("+1234567890", attributes.get("phonenum2").getValue()); + Assertions.assertEquals("123456789", attributes.get("phonenum3").getValue()); + Assertions.assertTrue(counterMap.isEmpty()); + Assertions.assertFalse(attributes.containsKey(SpanNormalizerConstants.CONTAINS_PII_TAGS_KEY)); + } }