{"id":15648694,"url":"https://github.com/mdelapenya/junit2otlp","last_synced_at":"2025-04-30T14:41:29.183Z","repository":{"id":38342750,"uuid":"427069609","full_name":"mdelapenya/junit2otlp","owner":"mdelapenya","description":"A jUnit parser that sends metrics and traces using OpenTelemetry","archived":false,"fork":false,"pushed_at":"2025-04-12T07:05:13.000Z","size":412,"stargazers_count":15,"open_issues_count":8,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-12T07:35:40.620Z","etag":null,"topics":["cicd","github-actions","gitlab-runner","golang","jenkins","junit","opentelemetry"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mdelapenya.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-11-11T16:30:17.000Z","updated_at":"2025-04-12T07:05:15.000Z","dependencies_parsed_at":"2024-06-19T13:34:23.944Z","dependency_job_id":"d09b9a80-41ab-4050-ba3d-5c64d7211d49","html_url":"https://github.com/mdelapenya/junit2otlp","commit_stats":{"total_commits":181,"total_committers":3,"mean_commits":"60.333333333333336","dds":"0.13812154696132595","last_synced_commit":"c715e3f46d8ec6b308bf8e8a266d9b3a44dc90a2"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdelapenya%2Fjunit2otlp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdelapenya%2Fjunit2otlp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdelapenya%2Fjunit2otlp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdelapenya%2Fjunit2otlp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mdelapenya","download_url":"https://codeload.github.com/mdelapenya/junit2otlp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251722819,"owners_count":21633020,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cicd","github-actions","gitlab-runner","golang","jenkins","junit","opentelemetry"],"created_at":"2024-10-03T12:25:52.553Z","updated_at":"2025-04-30T14:41:29.155Z","avatar_url":"https://github.com/mdelapenya.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# junit2otlp\n\n[![Tests](https://github.com/mdelapenya/junit2otlp/actions/workflows/tests.yml/badge.svg)](https://github.com/mdelapenya/junit2otlp/actions/workflows/tests.yml)\n\nThis simple CLI, written in Go, is sending jUnit metrics to a back-end using [Open Telemetry](https://opentelemetry.io).\n\n\u003e Inspired by https://github.com/axw/test2otlp, which sends traces and spans for `go test` JSON events as they occur.\n\n## Background\nAs jUnit represents a de-facto standard for test results in every programming language, this tool consumes the XML files produced by the test runner (or a tool converting to xUnit format), sending metrics to one or more open-source or commercial back-ends with Open Telemetry.\n\n## Supported CI runners\nThis tool will work in the context of a CI runner, such as a Github action, a Jenkins job, a Gitlab runner, or even a local execution. This is important because it will use the context of the CI execution to infer the attributes to be added to the OpenTelemetry traces and spans.\n\nIn particular the order of evaluation to detect the right execution context is the following:\n\n```\n Local execution \u003e Github action \u003e Jenkins multibranch pipeline \u003e Gitlab runner \u003e NIL\n```\n\n### Local execution\nIt reads the environment variables that are avaible in the context of a local execution, representing the fallback if no context is discovered:\n\n```golang\n// FromLocal returns an SCM context for local, using TARGET_BRANCH and BRANCH as the variables controlling\n// if the SCM context represents a change request. BRANCH is mandatory, otherwise an empty context will be retrieved.\n// If TARGET_BRANCH is not empty, it will represent a change request\nfunc FromLocal() *ScmContext {\n\tbaseRef := os.Getenv(\"TARGET_BRANCH\")\n\theadRef := os.Getenv(\"BRANCH\")\n\tif headRef == \"\" {\n\t\treturn nil\n\t}\n\n\tisPR := (baseRef != \"\")\n\n\treturn \u0026ScmContext{\n\t\tChangeRequest: isPR,\n\t\tCommit:        \"\",\n\t\tBranch:        headRef,\n\t\tProvider:      \"\",\n\t\tTargetBranch:  baseRef,\n\t}\n}\n```\n\n### Github Actions\nIt reads the environment variables that are avaible in the context of a Github Action execution:\n\n```golang\n// FromGithub returns an SCM context for Github, reading the right environment variables, as described\n// in their docs\nfunc FromGithub() *ScmContext {\n\tif os.Getenv(\"GITHUB_SHA\") == \"\" {\n\t\treturn nil\n\t}\n\n\tsha := os.Getenv(\"GITHUB_SHA\")\n\tbranchName := os.Getenv(\"GITHUB_REF_NAME\")\n\tbaseRef := os.Getenv(\"GITHUB_BASE_REF\") // only present for pull requests on Github Actions\n\theadRef := os.Getenv(\"GITHUB_HEAD_REF\") // only present for pull requests on Github Actions\n\n\tisChangeRequest := (baseRef != \"\" \u0026\u0026 headRef != \"\")\n\n\treturn \u0026ScmContext{\n\t\tChangeRequest: isChangeRequest,\n\t\tCommit:        sha,\n\t\tBranch:        branchName,\n\t\tProvider:      \"Github\",\n\t\tTargetBranch:  baseRef,\n\t}\n}\n```\n\n### Jenkins multibranch pipelines\nIt reads the environment variables that are avaible in the context of a Jenkins multibranch pipeline execution:\n\n```golang\n// FromJenkins returns an SCM context for Jenkins, reading the right environment variables, as described\n// in their docs\nfunc FromJenkins() *ScmContext {\n\tif os.Getenv(\"JENKINS_URL\") == \"\" {\n\t\treturn nil\n\t}\n\n\tisPR := os.Getenv(\"CHANGE_ID\") != \"\"  // only present on multibranch pipelines on Jenkins\n\theadRef := os.Getenv(\"BRANCH_NAME\")   // only present on multibranch pipelines on Jenkins\n\tsha := os.Getenv(\"GIT_COMMIT\")        // only present on multibranch pipelines on Jenkins\n\tbaseRef := os.Getenv(\"CHANGE_TARGET\") // only present on multibranch pipelines on Jenkins\n\n\tif isPR {\n\t\treturn \u0026ScmContext{\n\t\t\tChangeRequest: isPR,\n\t\t\tCommit:        sha,\n\t\t\tBranch:        headRef,\n\t\t\tProvider:      \"Jenkins\",\n\t\t\tTargetBranch:  baseRef,\n\t\t}\n\t} else {\n\t\treturn \u0026ScmContext{\n\t\t\tChangeRequest: isPR,\n\t\t\tCommit:        sha,\n\t\t\tBranch:        headRef,\n\t\t\tProvider:      \"Jenkins\",\n\t\t\tTargetBranch:  headRef,\n\t\t}\n\t}\n}\n```\n\n### Gitlab Runners\nIt reads the environment variables that are avaible in the context of a Gitlab runner execution:\n\n```golang\n// FromGitlab returns an SCM context for Gitlab, reading the right environment variables, as described\n// in their docs\nfunc FromGitlab() *ScmContext {\n\tif os.Getenv(\"CI_COMMIT_REF_NAME\") == \"\" {\n\t\treturn nil\n\t}\n\n\tsha := os.Getenv(\"CI_MERGE_REQUEST_SOURCE_BRANCH_SHA\")      // only present on merge requests on Gitlab CI\n\tcommitBranch := os.Getenv(\"CI_COMMIT_BRANCH\")               // only present on branches on Gitlab CI\n\theadRef := os.Getenv(\"CI_COMMIT_REF_NAME\")                  // only present on branches on Gitlab CI\n\tbaseRef := os.Getenv(\"CI_MERGE_REQUEST_TARGET_BRANCH_NAME\") // only present on merge requests on Gitlab CI\n\n\tisChangeRequest := (commitBranch == \"\")\n\n\treturn \u0026ScmContext{\n\t\tChangeRequest: isChangeRequest,\n\t\tCommit:        sha,\n\t\tBranch:        headRef,\n\t\tProvider:      \"Gitlab\",\n\t\tTargetBranch:  baseRef,\n\t}\n}\n```\n\n## OpenTelemetry configuration\nThis tool is able to override the following attributes:\n\n| Attribute | Flag | Default value | Description |\n| --------- | ---- | ------------- | ----------- |\n| Max Batch Size | --batch-size | `10` | Maximum export batch size allowed when creating a BatchSpanProcessor. |\n| Repository Path | --repository-path | `.` | Path to the SCM repository to be read. |\n| Service Name | --service-name | `junit2otlp` | Overrides OpenTelemetry's service name. If the `OTEL_SERVICE_NAME` environment variable is set, it will take precedence over any other value. |\n| Service Version | --service-version | Empty | Overrides OpenTelemetry's service version. If the `OTEL_SERVICE_VERSION` environment variable is set, it will take precedence over any other value. |\n| Trace Name | --trace-name | `junit2otlp` | Overrides OpenTelemetry's trace name. |\n| Properties Allowed | --properties-allowed | All | Comma separated list of properties to be allowed in the jUnit report. |\n| Additional Attributes | --additional-attributes | Empty | Comma separated list of attributes to be added to the jUnit report. |\n\nFor using this tool in a distributed tracing scenario, where there is a parent trace in which the test reports traces should be attached, it's important to set the `TRACEPARENT` environment variable, so that the traces and spans generated by this tool are located under the right parent trace. Please read more on this [here](https://github.com/open-telemetry/opentelemetry-specification/issues/740).\n\nFor further reference on environment variables in the OpenTelemetry SDK, please read the [official specification](https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/)\n\n## OpenTelemetry Attributes\nThis tool is going to parse the XML report produced by jUnit, or any other tool converting to that format, adding different attributes, separated by different categories:\n\n- Test metrics attributes\n- Ownership attributes\n\n### Metrics and Traces\nThe following attributes are added as metrics and/or traces.\n\n#### Test execution attributes\nFor each test execution, represented by a test report file, the tool will add the following attributes to the metric document, including them in the trace representing the test execution.\n\n| Attribute | Description |\n| --------- | ----------- |\n| `tests.suite.failed` | Number of failed tests in the test execution |\n| `tests.suite.error` | Number of errored tests in the test execution |\n| `tests.suite.passed` | Number of passed tests in the test execution |\n| `tests.suite.skipped` | Number of skipped tests in the test execution |\n| `tests.suite.duration` | Duration of the test execution |\n| `tests.suite.suitename` | Name of the test execution |\n| `tests.suite.systemerr` | Log produced by Systemerr |\n| `tests.suite.systemout` | Log produced by Systemout |\n| `tests.suite.total` | Total number of tests in the test execution |\n\n#### Test case attributes\nFor each test case in the test execution, the tool will add the following attributes to the span document representing the test case:\n\n| Attribute | Description |\n| --------- | ----------- |\n| `tests.case.classname` | Classname or file for the test case |\n| `tests.case.duration` | Duration of the test case |\n| `tests.case.error` | Error message of the test case |\n| `tests.case.message` | Message of the test case |\n| `tests.case.status` | Status of the test case |\n| `tests.case.systemerr` | Log produced by Systemerr |\n| `tests.case.systemout` | Log produced by Systemout |\n\n### Ownership attributes\nThese attributes are added to the traces and spans sent by the tool, identifying the owner (or owners) of the test suite, trying to correlate a test failure with an author or authors. To identify the owner, the tool will inspect the SCM repository for the project.\n\n#### SCM attributes\nBecause the XML test report is evaluated for a project **in a SCM repository**, the tool will add the following attributes to each trace and span:\n\n| Attribute | Description |\n| --------- | ----------- |\n| `scm.authors` | Array of unique Email addresses for the authors of the commits |\n| `scm.baseRef` | Name of the target branch (Only for change requests) |\n| `scm.branch` | Name of the branch where the test execution is processed |\n| `scm.committers` | Array of unique Email addresses for the committers of the commits |\n| `scm.provider` | Optional. If present, will include the name of the SCM provider, such as Github, Gitlab, Bitbucket, etc. |\n| `scm.repository` | Array of unique URLs representing the repository (i.e. https://github.com/mdelapenya/junit2otlp) |\n| `scm.type` | Type of the SCM (i.e. git, svn, mercurial)  At this moment the tool only supports Git repositories. |\n\n#### Change request attributes\nThe tool will add the following attributes to each trace and span if and only if the XML test report is evaluated in the context of a change requests **for a Git repository**:\n\n| Attribute | Description |\n| --------- | ----------- |\n| `scm.git.additions` | Number of added lines in the changeset |\n| `scm.git.deletions` | Number of deleted lines in the changeset |\n| `scm.git.clone.depth` | Depth of the git clone |\n| `scm.git.clone.shallow` | Whethere the git clone was shallow or not |\n| `scm.git.files.modified` | Number of modified files in the changeset |\n\nA changeset is calculated based on the HEAD commit and the first ancestor between HEAD and the branch where the changeset is submitted against.\n\n## Docker image\nIt's possible to run the binary as a Docker image. To build and use the image\n\n1. First build the Docker image using this Make goal:\n```shell\nmake build-docker-image\n```\n\n2. Then start the Elastic Stack back-end:\n```shell\nmake demo-start-elastic\n```\n\n3. Finally, once the services are started, run:\n```\ncat TEST-sample3.xml | docker run --rm -i --network elastic_junit2otlp --volume \"$(pwd):/opt/projectname\" --env OTEL_EXPORTER_OTLP_ENDPOINT=http://apm-server:8200 mdelapenya/junit2otlp:latest --service-name DOCKERFOO --trace-name TRACEBAR --repository-path \"/opt/projectname\"\n```\n  - We are making the Docker container receive the pipe with the `-i` flag.\n  - We are attaching the container to the same Docker network where the services are running.\n  - We are passing an environment variable with the URL of the OpenTelemetry exporter endpoint, in this case an APM Server instance.\n  - We are passing command line flags to the container, setting the service name (_DOCKERFOO_) and the trace name (_TRACEBAR_).\n\n## Demos\nTo demonstrate how traces and metrics are sent to different back-ends, we are provising the following demos:\n\n- Elastic\n- Jaeger\n- Prometheus\n- Zipkin\n\n### Elastic\nIt will use the Elastic Stack as back-end, sending the traces, spans and metrics through the APM Server, storing them in Elasticsearch and finally using Kibana as visualisation layer.\n\n```shell\nmake demo-start-elastic\ngo build \u0026\u0026 chmod +x ./junit2otlp\n\nexport OTEL_EXPORTER_OTLP_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_METRIC_INSECURE=\"true\"\nexport OTEL_SERVICE_NAME=\"elastic-srv\"\nexport OTEL_EXPORTER_OTLP_ENDPOINT=\"http://localhost:8200\"\nexport OTEL_EXPORTER_OTLP_SPAN_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_HEADERS=\"\"\nexport TARGET_BRANCH=main\nexport BRANCH=main\ncat TEST-sample.xml | ./junit2otlp\ncat TEST-sample2.xml | ./junit2otlp\ncat TEST-sample3.xml | ./junit2otlp\nopen http://localhost:5601/app/apm/services?rangeFrom=now-15m\u0026rangeTo=now\u0026comparisonEnabled=true\u0026comparisonType=day\n```\n\n### Jaeger\nIt will use Jaeger as back-end, sending the traces, spans and metrics through the OpenTelemetry collector, storing them in memory.\n\n```shell\nmake demo-start-jaeger\nexport OTEL_EXPORTER_OTLP_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_METRIC_INSECURE=\"true\"\nexport OTEL_SERVICE_NAME=\"jaeger-srv\"\nexport OTEL_EXPORTER_OTLP_ENDPOINT=\"http://localhost:14317\"\nexport OTEL_EXPORTER_OTLP_SPAN_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_HEADERS=\"\"\nexport TARGET_BRANCH=main\nexport BRANCH=main\ngo build \u0026\u0026 chmod +x ./junit2otlp\ncat TEST-sample.xml | ./junit2otlp\ncat TEST-sample2.xml | ./junit2otlp\ncat TEST-sample3.xml | ./junit2otlp\nopen http://localhost:16686\n```\n\n### Prometheus\nIt will use Prometheus as back-end, sending the traces, spans and metrics through the OpenTelemetry collector, storing them in memory.\n\n```shell\nmake demo-start-prometheus\ngo build \u0026\u0026 chmod +x ./junit2otlp\n\nexport OTEL_EXPORTER_OTLP_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_METRIC_INSECURE=\"true\"\nexport OTEL_SERVICE_NAME=\"prometheus-srv\"\nexport OTEL_EXPORTER_OTLP_ENDPOINT=\"http://localhost:14317\"\nexport OTEL_EXPORTER_OTLP_SPAN_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_HEADERS=\"\"\nexport TARGET_BRANCH=main\nexport BRANCH=main\ncat TEST-sample.xml | ./junit2otlp\ncat TEST-sample2.xml | ./junit2otlp\ncat TEST-sample3.xml | ./junit2otlp\nopen http://localhost:9090\n```\n\n### Zipkin\nIt will use Prometheus as back-end, sending the traces, spans and metrics through the OpenTelemetry collector, storing them in memory.\n\n```shell\nmake demo-start-zipkin\ngo build \u0026\u0026 chmod +x ./junit2otlp\n\nexport OTEL_EXPORTER_OTLP_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_METRIC_INSECURE=\"true\"\nexport OTEL_SERVICE_NAME=\"zipkin-srv\"\nexport OTEL_EXPORTER_OTLP_ENDPOINT=\"http://localhost:14317\"\nexport OTEL_EXPORTER_OTLP_SPAN_INSECURE=\"true\"\nexport OTEL_EXPORTER_OTLP_HEADERS=\"\"\nexport TARGET_BRANCH=main\nexport BRANCH=main\ncat TEST-sample.xml | ./junit2otlp\ncat TEST-sample2.xml | ./junit2otlp\ncat TEST-sample3.xml | ./junit2otlp\nopen http://localhost:9411\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdelapenya%2Fjunit2otlp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmdelapenya%2Fjunit2otlp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdelapenya%2Fjunit2otlp/lists"}