diff --git a/README.md b/README.md index 5db6f39..da3ceb9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [Go](https://golang.org/dl/) - [Gh](https://cli.github.com/) +- [Docker](https://docs.docker.com/get-docker/) ### Install tools @@ -55,8 +56,15 @@ Copy the `ngrok` URL and go to your GitHub repository, in this case we use `elas + +### Start Jaeger + +```shell +docker compose up +``` + ### Run the collector ```shell make run -``` \ No newline at end of file +``` diff --git a/builder-config.yml b/builder-config.yml index 64a5f03..ad70125 100644 --- a/builder-config.yml +++ b/builder-config.yml @@ -6,6 +6,7 @@ exporters: - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.102.0 - gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.102.0 - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/elasticsearchexporter v0.102.0 + - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/fileexporter v0.102.0 processors: - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.102.0 diff --git a/config.yml b/config.yml index cd8a3ba..e90f418 100644 --- a/config.yml +++ b/config.yml @@ -18,6 +18,12 @@ extensions: exporters: debug: verbosity: basic + file: + path: /tmp/logs.txt + otlp: + endpoint: "localhost:4317" + tls: + insecure: true service: extensions: [zpages, pprof] @@ -25,7 +31,7 @@ service: traces: receivers: [githubactionsjunit] processors: [batch] - exporters: [debug] + exporters: [debug, file, otlp] telemetry: logs: level: debug diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f5203c8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.9' + +services: + jaeger: + image: jaegertracing/all-in-one:${JAEGER_VERSION:-latest} + ports: + - "16686:16686" + - "4317:4317" + environment: + - LOG_LEVEL=debug diff --git a/go.mod b/go.mod index 6bb426d..7f75123 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,62 @@ module github.com/reakaleek/opentelemetry-github-actions-junit-receiver go 1.21.3 require ( - github.com/google/go-github/v62 v62.0.0 // indirect - github.com/joshdk/go-junit v1.0.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib v0.102.0 // indirect + github.com/google/go-github/v62 v62.0.0 + github.com/google/uuid v1.6.0 + github.com/joshdk/go-junit v1.0.0 + github.com/julienschmidt/httprouter v1.3.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/component v0.102.1 + go.opentelemetry.io/collector/config/confighttp v0.102.0 + go.opentelemetry.io/collector/config/configopaque v1.9.0 + go.opentelemetry.io/collector/consumer v0.102.1 + go.opentelemetry.io/collector/pdata v1.9.0 + go.opentelemetry.io/collector/receiver v0.102.0 + go.opentelemetry.io/otel v1.27.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rs/cors v1.11.0 // indirect go.opentelemetry.io/collector v0.102.1 // indirect + go.opentelemetry.io/collector/config/configauth v0.102.0 // indirect + go.opentelemetry.io/collector/config/configcompression v1.9.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.102.1 // indirect + go.opentelemetry.io/collector/config/configtls v0.102.0 // indirect + go.opentelemetry.io/collector/config/internal v0.102.0 // indirect + go.opentelemetry.io/collector/confmap v0.102.1 // indirect + go.opentelemetry.io/collector/extension v0.102.0 // indirect + go.opentelemetry.io/collector/extension/auth v0.102.0 // indirect + go.opentelemetry.io/collector/featuregate v1.9.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 18f9712..d3a931c 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,181 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= -github.com/open-telemetry/opentelemetry-collector-contrib v0.102.0 h1:wbHlYIO4SboyDwnXF7n5seltSJsTVuTTCgsVHo0CISI= -github.com/open-telemetry/opentelemetry-collector-contrib v0.102.0/go.mod h1:L4HPKqFkX0isc1E52fmncOfGtx6CJu+UVgDzWwN6eC8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= +github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/collector v0.102.1 h1:M/ciCcReQsSDYG9bJ2Qwqk7pQILDJ2bM/l0MdeCAvJE= go.opentelemetry.io/collector v0.102.1/go.mod h1:yF1lDRgL/Eksb4/LUnkMjvLvHHpi6wqBVlzp+dACnPM= +go.opentelemetry.io/collector/component v0.102.1 h1:66z+LN5dVCXhvuVKD1b56/3cYLK+mtYSLIwlskYA9IQ= +go.opentelemetry.io/collector/component v0.102.1/go.mod h1:XfkiSeImKYaewT2DavA80l0VZ3JjvGndZ8ayPXfp8d0= +go.opentelemetry.io/collector/config/configauth v0.102.0 h1:SMtTwysDzpGPgTrjGtxilP0jLbP2vcUvVf2reF5kIf4= +go.opentelemetry.io/collector/config/configauth v0.102.0/go.mod h1:DXDbCaehy9XIBkZ3dGAMSQmEby6CFhXjfLJqZbkeRcw= +go.opentelemetry.io/collector/config/configcompression v1.9.0 h1:B2q6XMO6xiF2s+14XjqAQHGY5UefR+PtkZ0WAlmSqpU= +go.opentelemetry.io/collector/config/configcompression v1.9.0/go.mod h1:6+m0GKCv7JKzaumn7u80A2dLNCuYf5wdR87HWreoBO0= +go.opentelemetry.io/collector/config/confighttp v0.102.0 h1:rWdOoyChzyUnEPKJFLhvE69ztS7xwq4QZxCk+b78gsc= +go.opentelemetry.io/collector/config/confighttp v0.102.0/go.mod h1:gnpwVekcqmJB7Ljubs/aw8z8cwvB+crxxU/wfkTDtr4= +go.opentelemetry.io/collector/config/configopaque v1.9.0 h1:jocenLdK/rVG9UoGlnpiBxXLXgH5NhIXCrVSTyKVYuA= +go.opentelemetry.io/collector/config/configopaque v1.9.0/go.mod h1:8v1yaH4iYjcigbbyEaP/tzVXeFm4AaAsKBF9SBeqaG4= +go.opentelemetry.io/collector/config/configtelemetry v0.102.1 h1:f/CYcrOkaHd+COIJ2lWnEgBCHfhEycpbow4ZhrGwAlA= +go.opentelemetry.io/collector/config/configtelemetry v0.102.1/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= +go.opentelemetry.io/collector/config/configtls v0.102.0 h1:U9xbye6bzsnQnJMCgYURyUcF1HGBGLb4+b3uIyxtmDE= +go.opentelemetry.io/collector/config/configtls v0.102.0/go.mod h1:KHdrvo3cwosgDxclyiLWmtbovIwqvaIGeTXr3p5721A= +go.opentelemetry.io/collector/config/internal v0.102.0 h1:tkkt6WFS6TNcSDPdWyqAOVStb5A2fuyY87khRVjZ4FI= +go.opentelemetry.io/collector/config/internal v0.102.0/go.mod h1:Yil8exjr0GTK+g27IftW1ch+DqsDI5dup4pNeGTF9S4= +go.opentelemetry.io/collector/confmap v0.102.1 h1:wZuH+d/P11Suz8wbp+xQCJ0BPE9m5pybtUe74c+rU7E= +go.opentelemetry.io/collector/confmap v0.102.1/go.mod h1:KgpS7UxH5rkd69CzAzlY2I1heH8Z7eNCZlHmwQBMxNg= +go.opentelemetry.io/collector/consumer v0.102.1 h1:0CkgHhxwx4lI/m+hWjh607xyjooW5CObZ8hFQy5vvo0= +go.opentelemetry.io/collector/consumer v0.102.1/go.mod h1:HoXqmrRV13jLnP3/Gg3fYNdRkDPoO7UW58hKiLyFF60= +go.opentelemetry.io/collector/extension v0.102.0 h1:R5PHbdRT31BgKNgmlY30kD0VI78SIWfQ2uKD8XAJkAY= +go.opentelemetry.io/collector/extension v0.102.0/go.mod h1:Q159CNiohyuXt1nDOH+rw4covTXWie8dsdls0sLuz7k= +go.opentelemetry.io/collector/extension/auth v0.102.0 h1:q3A3J0c5QE2SJHyZCXmdiV+9j+zbvQ4zdiAokuV3llw= +go.opentelemetry.io/collector/extension/auth v0.102.0/go.mod h1:3wc9a+pOyngaD8wIwOsAArOJfDCzHBQMBuNtzOGaFuY= +go.opentelemetry.io/collector/featuregate v1.9.0 h1:mC4/HnR5cx/kkG1RKOQAvHxxg5Ktmd9gpFdttPEXQtA= +go.opentelemetry.io/collector/featuregate v1.9.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= +go.opentelemetry.io/collector/pdata v1.9.0 h1:qyXe3HEVYYxerIYu0rzgo1Tx2d1Zs6iF+TCckbHLFOw= +go.opentelemetry.io/collector/pdata v1.9.0/go.mod h1:vk7LrfpyVpGZrRWcpjyy0DDZzL3SZiYMQxfap25551w= +go.opentelemetry.io/collector/receiver v0.102.0 h1:8rHNjWjV90bL0dgvKVc/7D10NCbM7bXCiqpcLRz5jBI= +go.opentelemetry.io/collector/receiver v0.102.0/go.mod h1:bYDwYItMrj7Drx0Pn4wZQ8Ii67lp9Nta62gbau93FhA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= +go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/receiver.go b/receiver.go index 6963cb7..39bcc53 100644 --- a/receiver.go +++ b/receiver.go @@ -5,21 +5,21 @@ import ( "context" "errors" "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "github.com/google/go-github/v62/github" "github.com/joshdk/go-junit" "github.com/julienschmidt/httprouter" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/receiver" - "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "go.uber.org/zap" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" ) func newTracesReceiver(cfg *Config, params receiver.CreateSettings, nextConsumer consumer.Traces) (receiver.Traces, error) { @@ -30,19 +30,21 @@ func newTracesReceiver(cfg *Config, params receiver.CreateSettings, nextConsumer } params.Logger.Info("GitHub API rate limit", zap.Int("limit", rateLimit.GetCore().Limit), zap.Int("remaining", rateLimit.GetCore().Remaining), zap.Time("reset", rateLimit.GetCore().Reset.Time)) return &githubactionsjunitReceiver{ - config: cfg, - settings: params, - logger: params.Logger, - ghClient: ghClient, + nextConsumer: nextConsumer, + config: cfg, + settings: params, + logger: params.Logger, + ghClient: ghClient, }, nil } type githubactionsjunitReceiver struct { - config *Config - server *http.Server - settings receiver.CreateSettings - logger *zap.Logger - ghClient *github.Client + nextConsumer consumer.Traces + config *Config + server *http.Server + settings receiver.CreateSettings + logger *zap.Logger + ghClient *github.Client } func (rec *githubactionsjunitReceiver) Start(ctx context.Context, host component.Host) error { @@ -90,7 +92,7 @@ func (rec *githubactionsjunitReceiver) handleEvent(w http.ResponseWriter, r *htt rec.handleWorkflowRunEvent(event, w, r, nil) default: { - rec.logger.Debug("Skipping the request because it is not a workflow_job event", zap.Any("event", event)) + rec.logger.Debug("Skipping the request because it is not a workflow_run event") w.WriteHeader(http.StatusOK) } } @@ -99,7 +101,7 @@ func (rec *githubactionsjunitReceiver) handleEvent(w http.ResponseWriter, r *htt func (rec *githubactionsjunitReceiver) handleWorkflowRunEvent(workflowRunEvent *github.WorkflowRunEvent, w http.ResponseWriter, r *http.Request, _ httprouter.Params) { rec.logger.Debug("Handling workflow run event", zap.Int64("workflow_run.id", workflowRunEvent.WorkflowRun.GetWorkflowID())) if workflowRunEvent.GetAction() != "completed" { - rec.logger.Debug("Skipping the request because it is not a completed workflow_job event", zap.Any("event", workflowRunEvent)) + rec.logger.Debug("Skipping the request because it is not a completed workflow_job event", zap.String("action", workflowRunEvent.GetAction())) w.WriteHeader(http.StatusOK) return } @@ -121,30 +123,99 @@ func (rec *githubactionsjunitReceiver) handleWorkflowRunEvent(workflowRunEvent * w.WriteHeader(http.StatusOK) return } + + traces := ptrace.NewTraces() + resourceSpans := traces.ResourceSpans().AppendEmpty() + resource := resourceSpans.Resource() + scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() + rootSpan := scopeSpans.Spans().AppendEmpty() + rootSpan.SetName(workflowRunEvent.GetWorkflowRun().GetName()) + rootSpan.SetKind(ptrace.SpanKindServer) + startTimestamp := workflowRunEvent.GetWorkflowRun().GetCreatedAt().Time + rootSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(startTimestamp)) + rootSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(workflowRunEvent.GetWorkflowRun().GetUpdatedAt().Time)) + traceId, err := generateTraceID(workflowRunEvent.GetWorkflowRun().GetID(), int(workflowRunEvent.GetWorkflowRun().GetRunAttempt())) + createResourceAttributes(resource, workflowRunEvent, rec.config) + + if err != nil { + rec.logger.Error("Failed to generate trace ID", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) + } + rootSpan.SetTraceID(traceId) + for _, artifact := range junitArtifacts { - err := processArtifact(rec.logger, rec.ghClient, workflowRunEvent, artifact) + err := processArtifact(rec.logger, rec.ghClient, rec.config, workflowRunEvent, artifact, rootSpan, scopeSpans, startTimestamp, traces) if err != nil { // TODO: report error but keep processing other artifacts rec.logger.Error("Failed to process artifact", zap.Error(err)) w.WriteHeader(http.StatusInternalServerError) return } + } + err = rec.nextConsumer.ConsumeTraces(r.Context(), traces) + if err != nil { + rec.logger.Error("Failed to process traces", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) } + w.WriteHeader(http.StatusOK) } -func processArtifact(logger *zap.Logger, ghClient *github.Client, workflowRunEvent *github.WorkflowRunEvent, artifact *github.Artifact) error { +func processArtifact(logger *zap.Logger, ghClient *github.Client, config *Config, workflowRunEvent *github.WorkflowRunEvent, artifact *github.Artifact, rootSpan ptrace.Span, scopeSpans ptrace.ScopeSpans, startTimestamp time.Time, traces ptrace.Traces) error { zipFile, err := downloadArtifact(context.Background(), ghClient, workflowRunEvent, artifact) if err != nil { return err } defer zipFile.Close() + + spanId, err := generateParentSpanID(workflowRunEvent.GetWorkflowRun().GetID(), workflowRunEvent.GetWorkflowRun().GetRunAttempt()) + if err != nil { + logger.Error("Failed to generate parent span ID", zap.Error(err)) + return fmt.Errorf("failed to generate parent span ID: %w", err) + } + rootSpan.SetSpanID(spanId) + + artifactSpan := scopeSpans.Spans().AppendEmpty() + artifactSpan.SetName(artifact.GetName()) + artifactSpan.SetKind(ptrace.SpanKindInternal) + artifactSpan.SetParentSpanID(rootSpan.SpanID()) + artifactSpan.SetTraceID(rootSpan.TraceID()) + artifactSpan.SetSpanID(NewSpanID()) + artifactSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(startTimestamp)) + + var maxDuration time.Duration = 0 + for _, file := range zipFile.Reader.File { logger.Debug("Processing file", zap.String("artifact", artifact.GetName()), zap.String("file", file.Name)) suites := processJunitFile(file, logger) for _, suite := range suites { - processSuite(suite, logger) + if suite.Totals.Duration > maxDuration { + maxDuration = suite.Totals.Duration + } + } + artifactSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(startTimestamp.Add(maxDuration))) + + // Convert the Test Suites to OpenTelemetry traces + //td, err := suitesToTraces(suites, workflowRunEvent, config, logger) + convertSuiteToTraces(suites, startTimestamp, scopeSpans, artifactSpan) + if err != nil { + logger.Debug("Failed to convert event to traces", zap.Error(err)) + // Move forward with the next file } + + //// Check if the traces data contains any ResourceSpans + //if td.ResourceSpans().Len() > 0 { + // spanCount := td.SpanCount() + // logger.Debug("Unmarshaled spans", zap.Int("#spans", spanCount)) + // td.ResourceSpans() + // + // consumerErr := nextConsumer.ConsumeTraces(ctx, td) + // if consumerErr != nil { + // logger.Debug("Failed to process traces", zap.Error(consumerErr)) + // } + //} else { + // logger.Debug("No spans to unmarshal or traces not initialized") + //} } return nil } @@ -175,55 +246,6 @@ func processJunitFile(file *zip.File, logger *zap.Logger) []junit.Suite { return suites } -func processSuite(suite junit.Suite, logger *zap.Logger) { - - // Set up the attributes for the suite - suiteAttributes := []attribute.KeyValue{ - semconv.CodeNamespaceKey.String(suite.Package), - attribute.Key(TestsSuiteName).String(suite.Name), - attribute.Key(TestsSystemErr).String(suite.SystemErr), - attribute.Key(TestsSystemOut).String(suite.SystemOut), - attribute.Key(TestsDuration).Int64(suite.Totals.Duration.Milliseconds()), - } - - // Add suite properties as labels - suiteAttributes = append(suiteAttributes, propsToLabels(suite.Properties)...) - - // For each test in the suite, set up the attributes - for _, test := range suite.Tests { - testAttributes := []attribute.KeyValue{ - semconv.CodeFunctionKey.String(test.Name), - attribute.Key(TestDuration).Int64(test.Duration.Milliseconds()), - attribute.Key(TestClassName).String(test.Classname), - attribute.Key(TestMessage).String(test.Message), - attribute.Key(TestStatus).String(string(test.Status)), - attribute.Key(TestSystemErr).String(test.SystemErr), - attribute.Key(TestSystemOut).String(test.SystemOut), - } - - testAttributes = append(testAttributes, propsToLabels(test.Properties)...) - testAttributes = append(testAttributes, suiteAttributes...) - - if test.Error != nil { - testAttributes = append(testAttributes, attribute.Key(TestError).String(test.Error.Error())) - } - var stringSlice []string - for _, attr := range testAttributes { - stringSlice = append(stringSlice, fmt.Sprintf("%s: %v", attr.Key, attr.Value.AsString())) - } - logger.Debug("Processing test suite", zap.Strings("attributes", stringSlice)) - } -} - -func propsToLabels(props map[string]string) []attribute.KeyValue { - attributes := []attribute.KeyValue{} - for k, v := range props { - attributes = append(attributes, attribute.Key(k).String(v)) - } - - return attributes -} - func getArtifacts(ctx context.Context, ghEvent *github.WorkflowRunEvent, ghClient *github.Client) ([]*github.Artifact, error) { listArtifactsOpts := &github.ListOptions{ PerPage: 100, diff --git a/semcov.go b/semcov.go deleted file mode 100644 index 4356028..0000000 --- a/semcov.go +++ /dev/null @@ -1,23 +0,0 @@ -package opentelemetrygithubactionsjunitreceiver - -const ( - // suite keys - FailedTestsCount = "tests.suite.failed" - ErrorTestsCount = "tests.suite.error" - PassedTestsCount = "tests.suite.passed" - SkippedTestsCount = "tests.suite.skipped" - TestsDuration = "tests.suite.duration" - TestsSuiteName = "tests.suite.suitename" - TestsSystemErr = "tests.suite.systemerr" - TestsSystemOut = "tests.suite.systemout" - TotalTestsCount = "tests.suite.total" - - // test keys - TestClassName = "tests.case.classname" - TestDuration = "tests.case.duration" - TestError = "tests.case.error" - TestMessage = "tests.case.message" - TestStatus = "tests.case.status" - TestSystemErr = "tests.case.systemerr" - TestSystemOut = "tests.case.systemout" -) diff --git a/trace_attributes.go b/trace_attributes.go new file mode 100644 index 0000000..03d00e9 --- /dev/null +++ b/trace_attributes.go @@ -0,0 +1,101 @@ +package opentelemetrygithubactionsjunitreceiver + +import ( + "strings" + "time" + + "github.com/google/go-github/v62/github" + "github.com/joshdk/go-junit" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" +) + +func createResourceAttributesTestSuite(span ptrace.Span, suite junit.Suite) { + attrs := span.Attributes() + attrs.PutStr("service.name", "junit") + attrs.PutStr(string(semconv.CodeNamespaceKey), suite.Package) + attrs.PutStr("tests.suite.suitename", suite.Name) + attrs.PutStr("tests.suite.systemerr", suite.SystemErr) + attrs.PutStr("tests.suite.systemout", suite.SystemOut) + attrs.PutInt("tests.suite.duration", suite.Totals.Duration.Milliseconds()) +} + +func createResourceAttributesTest(span ptrace.Span, test junit.Test) { + span.Attributes().PutInt("tests.case.duration", test.Duration.Milliseconds()) + span.Attributes().PutStr(string(semconv.CodeFunctionKey), test.Name) + span.Attributes().PutStr("tests.case.classname", test.Classname) + span.Attributes().PutStr("tests.case.message", test.Message) + span.Attributes().PutStr("tests.case.status", string(test.Status)) + span.Attributes().PutStr("tests.case.systemerr", test.SystemErr) + span.Attributes().PutStr("tests.case.systemout", test.SystemOut) + if test.Error != nil { + span.Attributes().PutStr("tests.case.error", test.Error.Error()) + } +} + +func createResourceAttributes(resource pcommon.Resource, e *github.WorkflowRunEvent, config *Config) { + attrs := resource.Attributes() + + serviceName := generateServiceName(config, e.GetRepo().GetFullName()) + attrs.PutStr("service.name", serviceName) + + attrs.PutStr("ci.github.workflow.run.actor.login", e.GetWorkflowRun().GetActor().GetLogin()) + + attrs.PutStr("ci.github.workflow.run.conclusion", e.GetWorkflowRun().GetConclusion()) + attrs.PutStr("ci.github.workflow.run.created_at", e.GetWorkflowRun().GetCreatedAt().Format(time.RFC3339)) + attrs.PutStr("ci.github.workflow.run.display_title", e.GetWorkflowRun().GetDisplayTitle()) + attrs.PutStr("ci.github.workflow.run.event", e.GetWorkflowRun().GetEvent()) + attrs.PutStr("ci.github.workflow.run.head_branch", e.GetWorkflowRun().GetHeadBranch()) + attrs.PutStr("ci.github.workflow.run.head_sha", e.GetWorkflowRun().GetHeadSHA()) + attrs.PutStr("ci.github.workflow.run.html_url", e.GetWorkflowRun().GetHTMLURL()) + attrs.PutInt("ci.github.workflow.run.id", e.GetWorkflowRun().GetID()) + attrs.PutStr("ci.github.workflow.run.name", e.GetWorkflowRun().GetName()) + attrs.PutStr("ci.github.workflow.run.path", e.GetWorkflow().GetPath()) + + if e.GetWorkflowRun().GetPreviousAttemptURL() != "" { + htmlURL := transformGitHubAPIURL(e.GetWorkflowRun().GetPreviousAttemptURL()) + attrs.PutStr("ci.github.workflow.run.previous_attempt_url", htmlURL) + } + + if len(e.GetWorkflowRun().ReferencedWorkflows) > 0 { + var referencedWorkflows []string + for _, workflow := range e.GetWorkflowRun().ReferencedWorkflows { + referencedWorkflows = append(referencedWorkflows, workflow.GetPath()) + } + attrs.PutStr("ci.github.workflow.run.referenced_workflows", strings.Join(referencedWorkflows, ";")) + } + + attrs.PutInt("ci.github.workflow.run.run_attempt", int64(e.GetWorkflowRun().GetRunAttempt())) + attrs.PutStr("ci.github.workflow.run.run_started_at", e.GetWorkflowRun().RunStartedAt.Format(time.RFC3339)) + attrs.PutStr("ci.github.workflow.run.status", e.GetWorkflowRun().GetStatus()) + attrs.PutStr("ci.github.workflow.run.sender.login", e.GetSender().GetLogin()) + attrs.PutStr("ci.github.workflow.run.triggering_actor.login", e.GetWorkflowRun().GetTriggeringActor().GetLogin()) + attrs.PutStr("ci.github.workflow.run.updated_at", e.GetWorkflowRun().GetUpdatedAt().Format(time.RFC3339)) + duration := e.GetWorkflowRun().GetUpdatedAt().Sub(e.GetWorkflowRun().GetRunStartedAt().Time) + attrs.PutInt("ci.github.workflow.run.duration_millis", duration.Milliseconds()) + + attrs.PutStr("ci.system", "github") + + attrs.PutStr("scm.system", "git") + + attrs.PutStr("scm.git.head_branch", e.GetWorkflowRun().GetHeadBranch()) + attrs.PutStr("scm.git.head_commit.author.email", e.GetWorkflowRun().GetHeadCommit().GetAuthor().GetEmail()) + attrs.PutStr("scm.git.head_commit.author.name", e.GetWorkflowRun().GetHeadCommit().GetAuthor().GetName()) + attrs.PutStr("scm.git.head_commit.committer.email", e.GetWorkflowRun().GetHeadCommit().GetCommitter().GetEmail()) + attrs.PutStr("scm.git.head_commit.committer.name", e.GetWorkflowRun().GetHeadCommit().GetCommitter().GetName()) + attrs.PutStr("scm.git.head_commit.message", e.GetWorkflowRun().GetHeadCommit().GetMessage()) + attrs.PutStr("scm.git.head_commit.timestamp", e.GetWorkflowRun().GetHeadCommit().GetTimestamp().Format(time.RFC3339)) + attrs.PutStr("scm.git.head_sha", e.GetWorkflowRun().GetHeadSHA()) + + if len(e.GetWorkflowRun().PullRequests) > 0 { + var prUrls []string + for _, pr := range e.GetWorkflowRun().PullRequests { + prUrls = append(prUrls, convertPRURL(pr.GetURL())) + } + attrs.PutStr("scm.git.pull_requests.url", strings.Join(prUrls, ";")) + } + + attrs.PutStr("scm.git.repo", e.GetRepo().GetFullName()) + +} diff --git a/trace_test_handling.go b/trace_test_handling.go new file mode 100644 index 0000000..6df149e --- /dev/null +++ b/trace_test_handling.go @@ -0,0 +1,165 @@ +package opentelemetrygithubactionsjunitreceiver + +import ( + "fmt" + + "github.com/google/go-github/v62/github" + "github.com/joshdk/go-junit" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" +) + +func suitesToTraces(suites []junit.Suite, e *github.WorkflowRunEvent, config *Config, logger *zap.Logger) (ptrace.Traces, error) { + logger.Debug("Determining event") + traces := ptrace.NewTraces() + resourceSpans := traces.ResourceSpans().AppendEmpty() + // NOTE: Avoid too much attributes to help with debugging the new changes. To be commented out later. + //runResource := resourceSpans.Resource() + scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() + //suiteResource := resourceSpans.Resource() + + logger.Info("Processing WorkflowRunEvent", zap.Int64("workflow_id", e.GetWorkflowRun().GetID()), zap.String("workflow_name", e.GetWorkflowRun().GetName()), zap.String("repo", e.GetRepo().GetFullName())) + + traceID, err := generateTraceID(*e.WorkflowRun.ID, int(*e.WorkflowRun.RunAttempt)) + if err != nil { + logger.Error("Failed to generate trace ID", zap.Error(err)) + return ptrace.Traces{}, fmt.Errorf("failed to generate trace ID: %w", err) + } + + // NOTE: Avoid too much attributes to help with debugging the new changes. To be commented out later. + //createResourceAttributes(runResource, e, config) + rootSpanID, _ := createRootSpan(resourceSpans, e, traceID, logger) + + for _, suite := range suites { + //createResourceAttributesTestSuite(suiteResource, suite, config) + if err != nil { + // QUESTION: Should we return an error here? + logger.Error("Failed to generate trace ID", zap.Error(err)) + return ptrace.Traces{}, fmt.Errorf("failed to generate trace ID: %w", err) + } + + suiteSpanID := createSuiteSpan(scopeSpans, suite, e, traceID, rootSpanID, logger) + logger.Debug("createParentSpan", zap.String("span-id", suiteSpanID.String())) + + // NOTE: If I enable this, I will not work as expected and therefore transaction will be empty. + //processTests(scopeSpans, suite.Tests, e, traceID, suiteSpanID, logger) + } + + return traces, nil +} + +func createRootSpan(resourceSpans ptrace.ResourceSpans, event *github.WorkflowRunEvent, traceID pcommon.TraceID, logger *zap.Logger) (pcommon.SpanID, error) { + logger.Debug("Creating root parent span", zap.String("name", event.GetWorkflowRun().GetName())) + scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() + span := scopeSpans.Spans().AppendEmpty() + + rootSpanID, err := generateParentSpanID(event.GetWorkflowRun().GetID(), event.GetWorkflowRun().GetRunAttempt()) + if err != nil { + logger.Error("Failed to generate root span ID", zap.Error(err)) + return pcommon.SpanID{}, fmt.Errorf("failed to generate root span ID: %w", err) + } + + span.SetTraceID(traceID) + span.SetSpanID(rootSpanID) + span.SetName(event.GetWorkflowRun().GetName()) + span.SetKind(ptrace.SpanKindServer) + setSpanTimes(span, event.GetWorkflowRun().GetRunStartedAt().Time, event.GetWorkflowRun().GetUpdatedAt().Time) + + // TODO: Set status based on conclusion or base don the number of failed tests? + switch event.WorkflowRun.GetConclusion() { + case "success": + span.Status().SetCode(ptrace.StatusCodeOk) + case "failure": + span.Status().SetCode(ptrace.StatusCodeError) + default: + span.Status().SetCode(ptrace.StatusCodeUnset) + } + + span.Status().SetMessage(event.GetWorkflowRun().GetConclusion()) + + // Attempt to link to previous trace ID if applicable + if event.GetWorkflowRun().GetPreviousAttemptURL() != "" && event.GetWorkflowRun().GetRunAttempt() > 1 { + logger.Debug("Linking to previous trace ID for WorkflowRunEvent") + previousRunAttempt := event.GetWorkflowRun().GetRunAttempt() - 1 + previousTraceID, err := generateTraceID(event.GetWorkflowRun().GetID(), previousRunAttempt) + if err != nil { + logger.Error("Failed to generate previous trace ID", zap.Error(err)) + } else { + link := span.Links().AppendEmpty() + link.SetTraceID(previousTraceID) + logger.Debug("Successfully linked to previous trace ID", zap.String("previousTraceID", previousTraceID.String())) + } + } + + return rootSpanID, nil +} + +func createSuiteSpan(scopeSpans ptrace.ScopeSpans, suite junit.Suite, event *github.WorkflowRunEvent, traceID pcommon.TraceID, parentSpanID pcommon.SpanID, logger *zap.Logger) pcommon.SpanID { + logger.Debug("Creating parent span", zap.String("name", string(*event.WorkflowRun.Name))) + span := scopeSpans.Spans().AppendEmpty() + span.SetTraceID(traceID) + + span.SetParentSpanID(parentSpanID) + + jobSpanID, _ := generateJobSpanID(*event.WorkflowRun.ID, int(*event.WorkflowRun.RunAttempt), suite.Name) + span.SetSpanID(jobSpanID) + + span.SetName(suite.Name) + span.SetKind(ptrace.SpanKindInternal) + + // NOTE: JUnit does not provide when the test started but we can assume + // it started when the workflow run started and ended by adding the duration. + setSpanTimes(span, event.GetWorkflowRun().GetRunStartedAt().Time, event.GetWorkflowRun().GetRunStartedAt().Time.Add(suite.Totals.Duration)) + + anyFailure := false + for _, test := range suite.Tests { + if test.Error != nil { + anyFailure = true + } + } + // QUESTION: what status if any skipped tests? + if anyFailure { + span.Status().SetCode(ptrace.StatusCodeError) + } else { + span.Status().SetCode(ptrace.StatusCodeOk) + } + + return span.SpanID() +} + +func createSpan(scopeSpans ptrace.ScopeSpans, test junit.Test, event *github.WorkflowRunEvent, traceID pcommon.TraceID, parentSpanID pcommon.SpanID, logger *zap.Logger, stepNumber ...int) pcommon.SpanID { + logger.Debug("Processing span", zap.String("test_name", test.Name)) + span := scopeSpans.Spans().AppendEmpty() + span.SetTraceID(traceID) + span.SetParentSpanID(parentSpanID) + + var spanID pcommon.SpanID + createResourceAttributesTest(span, test) + + span.SetSpanID(spanID) + // NOTE: JUnit does not provide when the test started but we can assume + // it started when the workflow run started and ended by adding the duration. + setSpanTimes(span, event.GetWorkflowRun().GetRunStartedAt().Time, event.GetWorkflowRun().GetRunStartedAt().Time.Add(test.Duration)) + + span.SetName(test.Name) + span.SetKind(ptrace.SpanKindInternal) + + // QUESTION: what status if any skipped tests? + switch test.Status { + case junit.StatusPassed: + span.Status().SetCode(ptrace.StatusCodeOk) + case junit.StatusFailed: + span.Status().SetCode(ptrace.StatusCodeError) + default: + span.Status().SetCode(ptrace.StatusCodeUnset) + } + + return span.SpanID() +} + +func processTests(scopeSpans ptrace.ScopeSpans, tests []junit.Test, event *github.WorkflowRunEvent, traceID pcommon.TraceID, parentSpanID pcommon.SpanID, logger *zap.Logger) { + for _, test := range tests { + createSpan(scopeSpans, test, event, traceID, parentSpanID, logger) + } +} diff --git a/trace_utils.go b/trace_utils.go new file mode 100644 index 0000000..62ce6c3 --- /dev/null +++ b/trace_utils.go @@ -0,0 +1,102 @@ +package opentelemetrygithubactionsjunitreceiver + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/google/go-github/v62/github" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +func checkDuplicateStepNames(steps []*github.TaskStep) map[string]int { + nameCount := make(map[string]int) + for _, step := range steps { + nameCount[step.GetName()]++ + } + return nameCount +} + +func convertPRURL(apiURL string) string { + apiURL = strings.Replace(apiURL, "/repos", "", 1) + apiURL = strings.Replace(apiURL, "/pulls", "/pull", 1) + return strings.Replace(apiURL, "api.", "", 1) +} +func generateTraceID(runID int64, runAttempt int) (pcommon.TraceID, error) { + input := fmt.Sprintf("%d%dt", runID, runAttempt) + hash := sha256.Sum256([]byte(input)) + traceIDHex := hex.EncodeToString(hash[:]) + + var traceID pcommon.TraceID + _, err := hex.Decode(traceID[:], []byte(traceIDHex[:32])) + if err != nil { + return pcommon.TraceID{}, err + } + + return traceID, nil +} + +func generateJobSpanID(runID int64, runAttempt int, job string) (pcommon.SpanID, error) { + input := fmt.Sprintf("%d%d%s", runID, runAttempt, job) + hash := sha256.Sum256([]byte(input)) + spanIDHex := hex.EncodeToString(hash[:]) + + var spanID pcommon.SpanID + _, err := hex.Decode(spanID[:], []byte(spanIDHex[16:32])) + if err != nil { + return pcommon.SpanID{}, err + } + + return spanID, nil +} + +func generateParentSpanID(runID int64, runAttempt int) (pcommon.SpanID, error) { + input := fmt.Sprintf("%d%ds", runID, runAttempt) + hash := sha256.Sum256([]byte(input)) + spanIDHex := hex.EncodeToString(hash[:]) + + var spanID pcommon.SpanID + _, err := hex.Decode(spanID[:], []byte(spanIDHex[16:32])) + if err != nil { + return pcommon.SpanID{}, err + } + + return spanID, nil +} + +func generateServiceName(config *Config, fullName string) string { + formattedName := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(fullName, "/", "-"), "_", "-")) + return fmt.Sprintf("%s", formattedName) +} + +func generateStepSpanID(runID int64, runAttempt int, jobName, stepName string, stepNumber ...int) (pcommon.SpanID, error) { + var input string + if len(stepNumber) > 0 && stepNumber[0] > 0 { + input = fmt.Sprintf("%d%d%s%s%d", runID, runAttempt, jobName, stepName, stepNumber[0]) + } else { + input = fmt.Sprintf("%d%d%s%s", runID, runAttempt, jobName, stepName) + } + hash := sha256.Sum256([]byte(input)) + spanIDHex := hex.EncodeToString(hash[:]) + + var spanID pcommon.SpanID + _, err := hex.Decode(spanID[:], []byte(spanIDHex[16:32])) + if err != nil { + return pcommon.SpanID{}, err + } + + return spanID, nil +} + +func setSpanTimes(span ptrace.Span, start, end time.Time) { + span.SetStartTimestamp(pcommon.NewTimestampFromTime(start)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(end)) +} + +func transformGitHubAPIURL(apiURL string) string { + htmlURL := strings.Replace(apiURL, "api.github.com/repos", "github.com", 1) + return htmlURL +} diff --git a/traces.go b/traces.go new file mode 100644 index 0000000..771a7df --- /dev/null +++ b/traces.go @@ -0,0 +1,54 @@ +package opentelemetrygithubactionsjunitreceiver + +import ( + crand "crypto/rand" + "encoding/binary" + "github.com/joshdk/go-junit" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "math/rand" + "time" +) + +func convertSuiteToTraces(suites []junit.Suite, startTimestamp time.Time, scopeSpans ptrace.ScopeSpans, rootSpan ptrace.Span) { + for _, suite := range suites { + suiteSpan := scopeSpans.Spans().AppendEmpty() + suiteSpan.SetName(suite.Name) + suiteSpan.SetKind(ptrace.SpanKindInternal) + suiteSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(startTimestamp)) + suiteSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(startTimestamp.Add(suite.Totals.Duration))) + suiteSpan.SetTraceID(rootSpan.TraceID()) + suiteSpan.SetSpanID(NewSpanID()) + suiteSpan.SetParentSpanID(rootSpan.SpanID()) + createResourceAttributesTestSuite(suiteSpan, suite) + for _, test := range suite.Tests { + span := scopeSpans.Spans().AppendEmpty() + span.SetName(test.Name) + span.SetKind(ptrace.SpanKindInternal) + span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTimestamp)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(startTimestamp.Add(test.Duration))) + span.SetTraceID(suiteSpan.TraceID()) + span.SetSpanID(NewSpanID()) + span.SetParentSpanID(suiteSpan.SpanID()) + createResourceAttributesTest(span, test) + switch test.Status { + case junit.StatusPassed: + span.Status().SetCode(ptrace.StatusCodeOk) + case junit.StatusFailed: + span.Status().SetCode(ptrace.StatusCodeError) + case junit.StatusSkipped: + span.Status().SetCode(ptrace.StatusCodeUnset) + } + } + } +} + +func NewSpanID() pcommon.SpanID { + var rngSeed int64 + _ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed) + randSource := rand.New(rand.NewSource(rngSeed)) + var sid [8]byte + randSource.Read(sid[:]) + spanID := pcommon.SpanID(sid) + return spanID +}