From eece5aa297797ba57eaf64e35334175e11ed9dcb Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 10:27:40 +0200 Subject: [PATCH 1/7] feat: Add OpenTelemetry tracing support With this it should be possible to keep track of performance issues also when called from wrapper tools. This tries to adhere to the environment specification laid out in https://github.com/open-telemetry/opentelemetry-specification/pull/4454 --- go.mod | 22 +++++- go.sum | 50 +++++++++++-- go.work.sum | 31 ++++++-- internal/telemetry/attributes.go | 18 +++++ internal/telemetry/otel.go | 123 +++++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 internal/telemetry/attributes.go create mode 100644 internal/telemetry/otel.go diff --git a/go.mod b/go.mod index 3dadb6a63..729ff6d15 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,10 @@ require ( github.com/stretchr/objx v0.5.2 github.com/stretchr/testify v1.11.0 github.com/thoas/go-funk v0.9.3 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/trace v1.37.0 golang.org/x/term v0.34.0 golang.org/x/text v0.28.0 gopkg.in/yaml.v2 v2.4.0 @@ -33,9 +37,12 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -46,9 +53,18 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 18f658304..8b9e63b06 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -20,17 +22,24 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-clix/cli v0.2.0 h1:rqpcyS/cvshOhXkwii0V+7nWetDVC8cp4pKI7JiCIS8= github.com/go-clix/cli v0.2.0/go.mod h1:yWI9abpv187r47lDjz8Z9TWev93aUTWaW2seSb5JmPQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -39,11 +48,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -85,12 +91,34 @@ github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQ github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -100,6 +128,14 @@ golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= diff --git a/go.work.sum b/go.work.sum index fdea6615d..5f985c692 100644 --- a/go.work.sum +++ b/go.work.sum @@ -3,18 +3,21 @@ cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/99designs/gqlgen v0.17.69 h1:9lGNxnxEnrlTkDn8g6IzcRi9Io3XyMLscrHWDKgdXWQ= github.com/99designs/gqlgen v0.17.69/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ= github.com/99designs/gqlgen v0.17.70 h1:xgLIgQuG+Q2L/AE9cW595CT7xCWCe/bpPIFGSfsGSGs= github.com/99designs/gqlgen v0.17.70/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= @@ -47,6 +50,7 @@ github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1Ig github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -65,6 +69,9 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= @@ -75,10 +82,12 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -143,6 +152,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= 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/otlp/otlplog/otlploggrpc v0.0.0-20240625175500-6d45f283c7ab h1:2eq3ZKzQLi1gFNwUw+4n+gsu9klL34RHm93MHitSLYo= @@ -154,14 +164,11 @@ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:51 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= -go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= 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/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= -go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= 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= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= @@ -174,8 +181,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -185,6 +192,7 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= @@ -201,6 +209,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -211,16 +220,20 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -237,17 +250,23 @@ golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/telemetry/attributes.go b/internal/telemetry/attributes.go new file mode 100644 index 000000000..3d9d663b5 --- /dev/null +++ b/internal/telemetry/attributes.go @@ -0,0 +1,18 @@ +package telemetry + +import ( + "fmt" + + "github.com/grafana/tanka/pkg/spec/v1alpha1" + "go.opentelemetry.io/otel/attribute" +) + +func AttrPath(v string) attribute.KeyValue { + return attribute.String("tanka.path", v) +} + +func AttrEnv(v *v1alpha1.Environment) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("tanka.env.id", fmt.Sprintf("%s@%s", v.Metadata.Name, v.Spec.APIServer)), + } +} diff --git a/internal/telemetry/otel.go b/internal/telemetry/otel.go new file mode 100644 index 000000000..752889201 --- /dev/null +++ b/internal/telemetry/otel.go @@ -0,0 +1,123 @@ +package telemetry + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func generateTracerProvider(ctx context.Context, res *resource.Resource) (*sdktrace.TracerProvider, error) { + if !hasOTELConfig(os.Environ()) { + return nil, nil + } + traceExporter, err := otlptracehttp.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to set up trace exporter: %w", err) + } + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithBatcher(traceExporter, + sdktrace.WithBatchTimeout(time.Second*5)), + ) + return tracerProvider, nil +} + +func hasOTELConfig(env []string) bool { + for _, envName := range env { + if strings.HasPrefix(envName, "OTEL_") { + return true + } + } + return false +} + +func generatePropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func Setup(ctx context.Context, res *resource.Resource) (func(context.Context) error, error) { + var shutdownFuncs []func(context.Context) error + var err error + + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + handleError := func(e error) { + err = errors.Join(e, shutdown(ctx)) + } + + finalRes, err := resource.Merge(resource.Default(), res) + if err != nil { + return nil, err + } + + prop := generatePropagator() + otel.SetTextMapPropagator(prop) + + tracerProvider, err := generateTracerProvider(ctx, finalRes) + if err != nil { + handleError(err) + return shutdown, err + } + if tracerProvider != nil { + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + } + + return shutdown, err +} + +func Tracer(name string) trace.Tracer { + return otel.Tracer(name) +} + +func FailSpanWithError(span trace.Span, err error) { + if err == nil { + return + } + span.RecordError(err) + span.SetStatus(codes.Error, "") +} + +func SucceedSpan(span trace.Span) { + span.SetStatus(codes.Ok, "") +} + +func InjectIntoEnv(ctx context.Context, env []string) []string { + carrier := make(propagation.MapCarrier) + prop := otel.GetTextMapPropagator() + prop.Inject(ctx, &carrier) + env = append(env, fmt.Sprintf("BAGGAGE=%s", carrier.Get("baggage"))) + env = append(env, fmt.Sprintf("TRACEPARENT=%s", carrier.Get("traceparent"))) + env = append(env, fmt.Sprintf("TRACESTATE=%s", carrier.Get("tracestate"))) + return env +} + +func LoadEnvironmentCarrier() propagation.TextMapCarrier { + carrier := make(propagation.MapCarrier) + carrier.Set("baggage", os.Getenv("BAGGAGE")) + carrier.Set("traceparent", os.Getenv("TRACEPARENT")) + carrier.Set("tracestate", os.Getenv("TRACESTATE")) + fmt.Println(carrier) + return carrier +} From 851675742a7eaf32140f6b0364ae652bb5f5480c Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 10:33:09 +0200 Subject: [PATCH 2/7] Add spans to core paths --- cmd/tk/args.go | 64 ++++++++++--------- cmd/tk/env.go | 38 +++++++----- cmd/tk/export.go | 14 +++-- cmd/tk/fmt.go | 3 +- cmd/tk/init.go | 3 +- cmd/tk/jsonnet.go | 7 ++- cmd/tk/lint.go | 4 +- cmd/tk/main.go | 47 ++++++++++---- cmd/tk/status.go | 7 ++- cmd/tk/tool.go | 41 ++++++++----- cmd/tk/toolCharts.go | 3 +- cmd/tk/workflow.go | 42 ++++++++----- go.work.sum | 17 +++++ pkg/jsonnet/eval.go | 21 ++++++- pkg/jsonnet/eval_test.go | 20 +++--- pkg/jsonnet/find_importers.go | 32 +++++++--- pkg/jsonnet/find_importers_test.go | 8 +-- pkg/jsonnet/imports.go | 3 +- pkg/jsonnet/imports_test.go | 2 +- pkg/jsonnet/jpath/jpath_test.go | 2 +- pkg/jsonnet/otel.go | 5 ++ pkg/kubernetes/diff.go | 5 +- pkg/kubernetes/kubernetes.go | 3 + pkg/tanka/evaluators.go | 9 +-- pkg/tanka/evaluators_test.go | 14 ++--- pkg/tanka/export.go | 99 ++++++++++++++++-------------- pkg/tanka/export_test.go | 24 ++++---- pkg/tanka/find.go | 28 ++++++--- pkg/tanka/find_test.go | 4 +- pkg/tanka/inline.go | 49 ++++++++++++--- pkg/tanka/load.go | 60 +++++++++++++----- pkg/tanka/load_test.go | 22 +++---- pkg/tanka/parallel.go | 14 +++-- pkg/tanka/prune.go | 5 +- pkg/tanka/static.go | 40 +++++++++--- pkg/tanka/status.go | 6 +- pkg/tanka/tanka.go | 3 + pkg/tanka/workflow.go | 21 ++++--- 38 files changed, 512 insertions(+), 277 deletions(-) create mode 100644 pkg/jsonnet/otel.go diff --git a/cmd/tk/args.go b/cmd/tk/args.go index 0a55e2f50..c09d63fb0 100644 --- a/cmd/tk/args.go +++ b/cmd/tk/args.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "os" "path/filepath" @@ -12,37 +13,40 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -var workflowArgs = cli.Args{ - Validator: cli.ValidateExact(1), - Predictor: cli.PredictFunc(func(args complete.Args) []string { - pwd, err := os.Getwd() - if err != nil { - return nil - } - - root, err := jpath.FindRoot(pwd) - if err != nil { - return nil - } - - envs, err := tanka.FindEnvs(pwd, tanka.FindOpts{}) - if err != nil && !errors.As(err, &tanka.ErrParallel{}) { - return nil - } - - var reldirs []string - for _, env := range envs { - path := filepath.Join(root, env.Metadata.Namespace) // namespace == path on disk - reldir, err := filepath.Rel(pwd, path) - if err == nil { - reldirs = append(reldirs, reldir) +func generateWorkflowArgs(ctx context.Context) cli.Args { + var workflowArgs = cli.Args{ + Validator: cli.ValidateExact(1), + Predictor: cli.PredictFunc(func(args complete.Args) []string { + pwd, err := os.Getwd() + if err != nil { + return nil } - } - if len(reldirs) != 0 { - return reldirs - } + root, err := jpath.FindRoot(pwd) + if err != nil { + return nil + } + + envs, err := tanka.FindEnvs(ctx, pwd, tanka.FindOpts{}) + if err != nil && !errors.As(err, &tanka.ErrParallel{}) { + return nil + } + + var reldirs []string + for _, env := range envs { + path := filepath.Join(root, env.Metadata.Namespace) // namespace == path on disk + reldir, err := filepath.Rel(pwd, path) + if err == nil { + reldirs = append(reldirs, reldir) + } + } + + if len(reldirs) != 0 { + return reldirs + } - return complete.PredictFiles("*").Predict(args) - }), + return complete.PredictFiles("*").Predict(args) + }), + } + return workflowArgs } diff --git a/cmd/tk/env.go b/cmd/tk/env.go index c958c64e7..bdfc97db3 100644 --- a/cmd/tk/env.go +++ b/cmd/tk/env.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "os" @@ -14,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/posener/complete" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -21,7 +23,7 @@ import ( "github.com/grafana/tanka/pkg/term" ) -func envCmd() *cli.Command { +func envCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "env [action]", Short: "manipulate environments", @@ -30,10 +32,10 @@ func envCmd() *cli.Command { addCommandsWithLogLevelOption( cmd, - envAddCmd(), - envSetCmd(), - envListCmd(), - envRemoveCmd(), + envAddCmd(ctx), + envSetCmd(ctx), + envListCmd(ctx), + envRemoveCmd(ctx), ) return cmd @@ -46,11 +48,11 @@ var kubectlContexts = cli.PredictFunc( }, ) -func envSetCmd() *cli.Command { +func envSetCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "set ", Short: "update properties of an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "server-from-context": kubectlContexts, }, @@ -82,7 +84,7 @@ func envSetCmd() *cli.Command { tmp.Spec.APIServer = server } - cfg, err := tanka.Peek(path, tanka.Opts{}) + cfg, err := tanka.Peek(ctx, path, tanka.Opts{}) if err != nil { return err } @@ -119,7 +121,7 @@ func envSetCmd() *cli.Command { return cmd } -func envAddCmd() *cli.Command { +func envAddCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add ", Short: "create a new environment", @@ -200,12 +202,12 @@ func addEnv(dir string, cfg *v1alpha1.Environment, inline bool) error { return nil } -func envRemoveCmd() *cli.Command { +func envRemoveCmd(ctx context.Context) *cli.Command { return &cli.Command{ Use: "remove ", Aliases: []string{"rm"}, Short: "delete an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Run: func(_ *cli.Command, args []string) error { for _, arg := range args { path, err := filepath.Abs(arg) @@ -225,8 +227,8 @@ func envRemoveCmd() *cli.Command { } } -func envListCmd() *cli.Command { - args := workflowArgs +func envListCmd(ctx context.Context) *cli.Command { + args := generateWorkflowArgs(ctx) args.Validator = cli.ArgsRange(0, 1) cmd := &cli.Command{ @@ -246,6 +248,9 @@ func envListCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "envListCmd") + defer span.End() + var path string var err error if len(args) == 1 { @@ -257,8 +262,9 @@ func envListCmd() *cli.Command { } } - envs, err := tanka.FindEnvs(path, tanka.FindOpts{JsonnetImplementation: jsonnetImplementation, Selector: getLabelSelector(), JsonnetOpts: getJsonnetOpts()}) + envs, err := tanka.FindEnvs(ctx, path, tanka.FindOpts{JsonnetImplementation: jsonnetImplementation, Selector: getLabelSelector(), JsonnetOpts: getJsonnetOpts()}) if err != nil { + telemetry.FailSpanWithError(span, err) return err } sort.SliceStable(envs, func(i, j int) bool { return envs[i].Metadata.Name < envs[j].Metadata.Name }) @@ -266,7 +272,9 @@ func envListCmd() *cli.Command { if *useJSON { j, err := json.Marshal(envs) if err != nil { - return fmt.Errorf("formatting as json: %s", err) + err = fmt.Errorf("formatting as json: %s", err) + telemetry.FailSpanWithError(span, err) + return err } fmt.Println(string(j)) return nil diff --git a/cmd/tk/export.go b/cmd/tk/export.go index 4d54bb813..b28a4d39f 100644 --- a/cmd/tk/export.go +++ b/cmd/tk/export.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "regexp" @@ -13,8 +14,8 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func exportCmd() *cli.Command { - args := workflowArgs +func exportCmd(ctx context.Context) *cli.Command { + args := generateWorkflowArgs(ctx) args.Validator = cli.ArgsMin(2) cmd := &cli.Command{ @@ -49,6 +50,9 @@ func exportCmd() *cli.Command { recursive := cmd.Flags().BoolP("recursive", "r", false, "Look recursively for Tanka environments") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "exportCmd") + defer span.End() + // Allocate a block of memory to alter GC behaviour. See https://github.com/golang/go/issues/23044 ballast := make([]byte, *ballastBytes) defer runtime.KeepAlive(ballast) @@ -89,7 +93,7 @@ func exportCmd() *cli.Command { // find possible environments if *recursive { // get absolute path to Environment - envs, err := tanka.FindEnvsFromPaths(args[1:], tanka.FindOpts{Selector: opts.Selector, Parallelism: opts.Parallelism, JsonnetOpts: opts.Opts.JsonnetOpts}) + envs, err := tanka.FindEnvsFromPaths(ctx, args[1:], tanka.FindOpts{Selector: opts.Selector, Parallelism: opts.Parallelism, JsonnetOpts: opts.Opts.JsonnetOpts}) if err != nil { return err } @@ -106,7 +110,7 @@ func exportCmd() *cli.Command { } // validate environment - env, err := tanka.Peek(args[1], opts.Opts) + env, err := tanka.Peek(ctx, args[1], opts.Opts) if err != nil { switch err.(type) { case tanka.ErrMultipleEnvs: @@ -121,7 +125,7 @@ func exportCmd() *cli.Command { } // export them - return tanka.ExportEnvironments(exportEnvs, args[0], &opts) + return tanka.ExportEnvironments(ctx, exportEnvs, args[0], &opts) } return cmd } diff --git a/cmd/tk/fmt.go b/cmd/tk/fmt.go index f73b67c06..1a066b301 100644 --- a/cmd/tk/fmt.go +++ b/cmd/tk/fmt.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "os" @@ -15,7 +16,7 @@ import ( // ArgStdin is the "magic" argument for reading from stdin const ArgStdin = "-" -func fmtCmd() *cli.Command { +func fmtCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "fmt ", Short: "format Jsonnet code", diff --git a/cmd/tk/init.go b/cmd/tk/init.go index 8b49be2d1..0401b9f82 100644 --- a/cmd/tk/init.go +++ b/cmd/tk/init.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "os" @@ -15,7 +16,7 @@ import ( const defaultK8sVersion = "1.29" // initCmd creates a new application -func initCmd() *cli.Command { +func initCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "init", Short: "Create the directory structure", diff --git a/cmd/tk/jsonnet.go b/cmd/tk/jsonnet.go index c6dfbcf4e..7a1ee434c 100644 --- a/cmd/tk/jsonnet.go +++ b/cmd/tk/jsonnet.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "github.com/go-clix/cli" @@ -8,11 +9,11 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func evalCmd() *cli.Command { +func evalCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "evaluate the jsonnet to json", Use: "eval ", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } var jsonnetImplementation string @@ -29,7 +30,7 @@ func evalCmd() *cli.Command { if *evalPattern != "" { jsonnetOpts.EvalScript = tanka.PatternEvalScript(*evalPattern) } - raw, err := tanka.Eval(args[0], jsonnetOpts) + raw, err := tanka.Eval(ctx, args[0], jsonnetOpts) if raw == nil && err != nil { return err diff --git a/cmd/tk/lint.go b/cmd/tk/lint.go index bad3679ba..39a8595af 100644 --- a/cmd/tk/lint.go +++ b/cmd/tk/lint.go @@ -1,6 +1,8 @@ package main import ( + "context" + "github.com/go-clix/cli" "github.com/gobwas/glob" "github.com/posener/complete" @@ -8,7 +10,7 @@ import ( "github.com/grafana/tanka/pkg/jsonnet" ) -func lintCmd() *cli.Command { +func lintCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "lint ", Short: "lint Jsonnet code", diff --git a/cmd/tk/main.go b/cmd/tk/main.go index b988b4b53..602fc7c2e 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -1,19 +1,26 @@ package main import ( + "context" "fmt" "os" "strings" "github.com/go-clix/cli" "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" "golang.org/x/term" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/tanka" ) var interactive = term.IsTerminal(int(os.Stdout.Fd())) +var tracer = telemetry.Tracer("tanka") + func main() { rootCmd := &cli.Command{ Use: "tk", @@ -21,34 +28,46 @@ func main() { Version: tanka.CurrentVersion, } + ctx := context.Background() + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("tanka"), + ) + shutdownOtel, err := telemetry.Setup(ctx, res) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + ctx = otel.GetTextMapPropagator().Extract(ctx, telemetry.LoadEnvironmentCarrier()) + // set default logging level early; not all commands parse --log-level zerolog.SetGlobalLevel(zerolog.InfoLevel) // workflow commands addCommandsWithLogLevelOption( rootCmd, - applyCmd(), - showCmd(), - diffCmd(), - pruneCmd(), - deleteCmd(), + applyCmd(ctx), + showCmd(ctx), + diffCmd(ctx), + pruneCmd(ctx), + deleteCmd(ctx), ) addCommandsWithLogLevelOption( rootCmd, - envCmd(), - statusCmd(), - exportCmd(), + envCmd(ctx), + statusCmd(ctx), + exportCmd(ctx), ) // jsonnet commands addCommandsWithLogLevelOption( rootCmd, - fmtCmd(), - lintCmd(), - evalCmd(), - initCmd(), - toolCmd(), + fmtCmd(ctx), + lintCmd(ctx), + evalCmd(ctx), + initCmd(ctx), + toolCmd(ctx), ) // external commands prefixed with "tk-" @@ -59,9 +78,11 @@ func main() { // Run! if err := rootCmd.Execute(); err != nil { + shutdownOtel(context.Background()) fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } + shutdownOtel(context.Background()) } func addCommandsWithLogLevelOption(rootCmd *cli.Command, cmds ...*cli.Command) { diff --git a/cmd/tk/status.go b/cmd/tk/status.go index 8d76c5676..2698e67d4 100644 --- a/cmd/tk/status.go +++ b/cmd/tk/status.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "sort" @@ -13,18 +14,18 @@ import ( "github.com/grafana/tanka/pkg/tanka" ) -func statusCmd() *cli.Command { +func statusCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "status ", Short: "display an overview of the environment, including contents and metadata.", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } vars := workflowFlags(cmd.Flags()) getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { - status, err := tanka.Status(args[0], tanka.Opts{ + status, err := tanka.Status(ctx, args[0], tanka.Opts{ JsonnetImplementation: vars.jsonnetImplementation, JsonnetOpts: getJsonnetOpts(), Name: vars.name, diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go index 7f25fcabb..cdc0f0abc 100644 --- a/cmd/tk/tool.go +++ b/cmd/tk/tool.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "os" @@ -16,7 +17,7 @@ import ( "github.com/grafana/tanka/pkg/jsonnet/jpath" ) -func toolCmd() *cli.Command { +func toolCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "handy utilities for working with jsonnet", Use: "tool [command]", @@ -25,20 +26,20 @@ func toolCmd() *cli.Command { addCommandsWithLogLevelOption( cmd, - jpathCmd(), - importsCmd(), - importersCmd(), - importersCountCmd(), - chartsCmd(), + jpathCmd(ctx), + importsCmd(ctx), + importersCmd(ctx), + importersCountCmd(ctx), + chartsCmd(ctx), ) return cmd } -func jpathCmd() *cli.Command { +func jpathCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Short: "export JSONNET_PATH for use with other jsonnet tools", Use: "jpath ", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } debug := cmd.Flags().BoolP("debug", "d", false, "show debug info") @@ -73,16 +74,19 @@ func jpathCmd() *cli.Command { return cmd } -func importsCmd() *cli.Command { +func importsCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "imports ", Short: "list all transitive imports of an environment", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } check := cmd.Flags().StringP("check", "c", "", "git commit hash to check against") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importsCmd") + defer span.End() + var modFiles []string if *check != "" { var err error @@ -97,7 +101,7 @@ func importsCmd() *cli.Command { return fmt.Errorf("loading environment: %s", err) } - deps, err := jsonnet.TransitiveImports(path) + deps, err := jsonnet.TransitiveImports(ctx, path) if err != nil { return fmt.Errorf("resolving imports: %s", err) } @@ -136,7 +140,7 @@ func importsCmd() *cli.Command { return cmd } -func importersCmd() *cli.Command { +func importersCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "importers ", Short: "list all environments that either directly or transitively import the given files", @@ -157,7 +161,10 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca } root := cmd.Flags().String("root", ".", "root directory to search for environments") - cmd.Run = func(_ *cli.Command, args []string) error { + cmd.Run = func(cctx *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importersCmd") + defer span.End() + root, err := filepath.Abs(*root) if err != nil { return fmt.Errorf("resolving root: %w", err) @@ -172,7 +179,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca } } - envs, err := jsonnet.FindImporterForFiles(root, args) + envs, err := jsonnet.FindImporterForFiles(ctx, root, args) if err != nil { return fmt.Errorf("resolving imports: %s", err) } @@ -185,7 +192,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca return cmd } -func importersCountCmd() *cli.Command { +func importersCountCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "importers-count ", Short: "for each file in the given directory, list the number of environments that import it", @@ -209,6 +216,8 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca filenameRegex := cmd.Flags().String("filename-regex", "", "only count files that match the given regex. Matches only jsonnet files by default") cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "importersCountCmd") + defer span.End() dir := args[0] root, err := filepath.Abs(*root) @@ -216,7 +225,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca return fmt.Errorf("resolving root: %w", err) } - result, err := jsonnet.CountImporters(root, dir, *recursive, *filenameRegex) + result, err := jsonnet.CountImporters(ctx, root, dir, *recursive, *filenameRegex) if err != nil { return fmt.Errorf("resolving imports: %s", err) } diff --git a/cmd/tk/toolCharts.go b/cmd/tk/toolCharts.go index 57047fb23..7de8b3150 100644 --- a/cmd/tk/toolCharts.go +++ b/cmd/tk/toolCharts.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "os" @@ -13,7 +14,7 @@ import ( const repoConfigFlagUsage = "specify a local helm repository config file to use instead of the repositories in the chartfile.yaml. For use with private repositories" -func chartsCmd() *cli.Command { +func chartsCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "charts", Short: "Declarative vendoring of Helm Charts", diff --git a/cmd/tk/workflow.go b/cmd/tk/workflow.go index 64fbdb2b9..1af871b83 100644 --- a/cmd/tk/workflow.go +++ b/cmd/tk/workflow.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" @@ -69,11 +70,11 @@ func validateAutoApprove(autoApproveDeprecated bool, autoApproveString string) ( return result, nil } -func applyCmd() *cli.Command { +func applyCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "apply ", Short: "apply the configuration to the cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, "diff-strategy": cli.PredictSet("native", "subset", "validate", "server", "none"), @@ -96,6 +97,8 @@ func applyCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "applyCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -116,16 +119,16 @@ func applyCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - return tanka.Apply(args[0], opts) + return tanka.Apply(ctx, args[0], opts) } return cmd } -func pruneCmd() *cli.Command { +func pruneCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "prune ", Short: "delete resources removed from Jsonnet", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, }, @@ -142,6 +145,8 @@ func pruneCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "pruneCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -155,17 +160,17 @@ func pruneCmd() *cli.Command { opts.JsonnetOpts = getJsonnetOpts() - return tanka.Prune(args[0], opts) + return tanka.Prune(ctx, args[0], opts) } return cmd } -func deleteCmd() *cli.Command { +func deleteCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "delete ", Short: "delete the environment from cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, }, @@ -183,6 +188,8 @@ func deleteCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "deleteCmd") + defer span.End() err := validateDryRun(opts.DryRun) if err != nil { return err @@ -203,16 +210,16 @@ func deleteCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - return tanka.Delete(args[0], opts) + return tanka.Delete(ctx, args[0], opts) } return cmd } -func diffCmd() *cli.Command { +func diffCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "diff ", Short: "differences between the configuration and the cluster", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), Predictors: complete.Flags{ "color": colorValues, "diff-strategy": cli.PredictSet("native", "subset", "validate", "server"), @@ -230,6 +237,8 @@ func diffCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "diffCmd") + defer span.End() if err := setForceColor(&opts.DiffBaseOpts); err != nil { return err } @@ -242,7 +251,7 @@ func diffCmd() *cli.Command { opts.Name = vars.name opts.JsonnetImplementation = vars.jsonnetImplementation - changes, err := tanka.Diff(args[0], opts) + changes, err := tanka.Diff(ctx, args[0], opts) if err != nil { return err } @@ -261,6 +270,7 @@ func diffCmd() *cli.Command { if opts.ExitZero { exitStatusDiff = ExitStatusClean } + span.End() os.Exit(exitStatusDiff) return nil } @@ -268,11 +278,11 @@ func diffCmd() *cli.Command { return cmd } -func showCmd() *cli.Command { +func showCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "show ", Short: "jsonnet as yaml", - Args: workflowArgs, + Args: generateWorkflowArgs(ctx), } allowRedirectFlag := cmd.Flags().Bool("dangerous-allow-redirect", false, "allow redirecting output to a file or a pipe.") @@ -281,6 +291,8 @@ func showCmd() *cli.Command { getJsonnetOpts := jsonnetFlags(cmd.Flags()) cmd.Run = func(_ *cli.Command, args []string) error { + ctx, span := tracer.Start(ctx, "showCmd") + defer span.End() allowRedirectEnv := os.Getenv("TANKA_DANGEROUS_ALLOW_REDIRECT") == "true" allowRedirect := allowRedirectEnv || *allowRedirectFlag @@ -300,7 +312,7 @@ to bypass this check.`) return err } - pretty, err := tanka.Show(args[0], tanka.Opts{ + pretty, err := tanka.Show(ctx, args[0], tanka.Opts{ JsonnetOpts: getJsonnetOpts(), Filters: filters, Name: vars.name, diff --git a/go.work.sum b/go.work.sum index 5f985c692..80463a807 100644 --- a/go.work.sum +++ b/go.work.sum @@ -11,6 +11,10 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/99designs/gqlgen v0.17.69 h1:9lGNxnxEnrlTkDn8g6IzcRi9Io3XyMLscrHWDKgdXWQ= github.com/99designs/gqlgen v0.17.69/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ= github.com/99designs/gqlgen v0.17.70 h1:xgLIgQuG+Q2L/AE9cW595CT7xCWCe/bpPIFGSfsGSGs= @@ -18,6 +22,7 @@ github.com/99designs/gqlgen v0.17.70/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KT github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= @@ -25,6 +30,7 @@ github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= @@ -37,6 +43,7 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -83,6 +90,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -102,11 +111,13 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= github.com/matryer/moq v0.3.4/go.mod h1:wqm9QObyoMuUtH81zFfs3EK6mXEcByy+TjvSROOXJ2U= @@ -128,9 +139,11 @@ github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKi github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -183,6 +196,7 @@ golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -252,6 +266,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= @@ -267,6 +282,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -281,5 +297,6 @@ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4Va k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go index 16901982f..c874e8ecc 100644 --- a/pkg/jsonnet/eval.go +++ b/pkg/jsonnet/eval.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "os" "regexp" "time" @@ -9,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/jsonnet/jpath" @@ -79,21 +81,34 @@ func (o Opts) Clone() Opts { // EvaluateFile evaluates the Jsonnet code in the given file and returns the // result in JSON form. It disregards opts.ImportPaths in favor of automatically // resolving these according to the specified file. -func EvaluateFile(impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { +func EvaluateFile(ctx context.Context, impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { + ctx, span := tracer.Start(ctx, "jsonnet.EvaluateFile") + defer span.End() + span.SetAttributes(telemetry.AttrPath(jsonnetFile)) + evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateFile(jsonnetFile) } data, err := os.ReadFile(jsonnetFile) if err != nil { + telemetry.FailSpanWithError(span, err) return "", err } - return evaluateSnippet(impl, evalFunc, jsonnetFile, string(data), opts) + output, err := evaluateSnippet(impl, evalFunc, jsonnetFile, string(data), opts) + if err != nil { + telemetry.FailSpanWithError(span, err) + } + return output, err } // Evaluate renders the given jsonnet into a string // If cache options are given, a hash from the data will be computed and // the resulting string will be cached for future retrieval -func Evaluate(path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { +func Evaluate(ctx context.Context, path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { + ctx, span := tracer.Start(ctx, "jsonnet.Evaluate") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateAnonymousSnippet(data) } diff --git a/pkg/jsonnet/eval_test.go b/pkg/jsonnet/eval_test.go index a1051502a..345888ce0 100644 --- a/pkg/jsonnet/eval_test.go +++ b/pkg/jsonnet/eval_test.go @@ -56,14 +56,14 @@ const thisFileResult = `{ // To be consistent with the jsonnet executable, // when evaluating a file, `std.thisFile` should point to the given path func TestEvaluateFile(t *testing.T) { - result, err := EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) } func TestEvaluateFileWithInvalidBinary(t *testing.T) { binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "this-file-doesnt-exist"} - result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.Equal(t, result, "") assert.ErrorIs(t, err, exec.ErrNotFound) } @@ -71,13 +71,13 @@ func TestEvaluateFileWithInvalidBinary(t *testing.T) { // This test requires jsonnet to be installed and available in the PATH func TestEvaluateFileWithJsonnetBinary(t *testing.T) { binaryImpl := &binary.JsonnetBinaryImplementation{BinPath: "jsonnet"} - result, err := EvaluateFile(binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), binaryImpl, "testdata/thisFile/main.jsonnet", Opts{}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) } func TestEvaluateFileDoesntExist(t *testing.T) { - result, err := EvaluateFile(jsonnetImpl, "testdata/doesnt-exist/main.jsonnet", Opts{}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/doesnt-exist/main.jsonnet", Opts{}) assert.EqualError(t, err, "open testdata/doesnt-exist/main.jsonnet: no such file or directory") assert.Equal(t, "", result) } @@ -89,10 +89,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { cachePath := filepath.Join(tmp, "cache") // Should be created during caching // Evaluate two files - result, err := EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err := EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, importTreeResult, result) @@ -102,10 +102,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { assert.Len(t, readCache, 2) // Evaluate two files again, same result - result, err = EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, thisFileResult, result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, importTreeResult, result) @@ -115,10 +115,10 @@ func TestEvaluateFileWithCaching(t *testing.T) { } // Evaluate two files again, modified cache is returned instead of the actual result - result, err = EvaluateFile(jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/thisFile/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, "BYfdlr1ZOVwiOfbd89JYTcK-eRQh05bi8ky3k1vVW5o=.json", result) - result, err = EvaluateFile(jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) + result, err = EvaluateFile(t.Context(), jsonnetImpl, "testdata/importTree/main.jsonnet", Opts{CachePath: cachePath}) assert.NoError(t, err) assert.Equal(t, "R_3hy-dRfOwXN-fezQ50ZF4dnrFcBcbQ9LztR_XWzJA=.json", result) } diff --git a/pkg/jsonnet/find_importers.go b/pkg/jsonnet/find_importers.go index 492bc512b..3b4b3d1d1 100644 --- a/pkg/jsonnet/find_importers.go +++ b/pkg/jsonnet/find_importers.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "fmt" "os" "path/filepath" @@ -8,7 +9,9 @@ import ( "sort" "strings" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/jpath" + "go.opentelemetry.io/otel/attribute" ) var ( @@ -27,9 +30,14 @@ type cachedJsonnetFile struct { // FindImporterForFiles finds the entrypoints (main.jsonnet files) that import the given files. // It looks through imports transitively, so if a file is imported through a chain, it will still be reported. // If the given file is a main.jsonnet file, it will be returned as well. -func FindImporterForFiles(root string, files []string) ([]string, error) { +func FindImporterForFiles(ctx context.Context, root string, files []string) ([]string, error) { + ctx, span := tracer.Start(ctx, "jsonnet.FindImporterForFiles") + defer span.End() + + span.SetAttributes(attribute.StringSlice("tanka.files", files)) transitiveImporters, err := FindTransitiveImportersForFile(root, files) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } @@ -97,10 +105,14 @@ func FindTransitiveImportersForFile(root string, files []string) ([]string, erro } // CountImporters lists all the files in the given directory and for each file counts the number of environments that import it. -func CountImporters(root string, dir string, recursive bool, filenameRegexStr string) (string, error) { +func CountImporters(ctx context.Context, root string, dir string, recursive bool, filenameRegexStr string) (string, error) { + ctx, span := tracer.Start(ctx, "jsonnet.CountImporters") + defer span.End() root, err := filepath.Abs(root) if err != nil { - return "", fmt.Errorf("resolving root: %w", err) + err = fmt.Errorf("resolving root: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } if filenameRegexStr == "" { @@ -108,7 +120,9 @@ func CountImporters(root string, dir string, recursive bool, filenameRegexStr st } filenameRegexp, err := regexp.Compile(filenameRegexStr) if err != nil { - return "", fmt.Errorf("compiling filename regex: %w", err) + err = fmt.Errorf("compiling filename regex: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } var files []string err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -141,14 +155,18 @@ func CountImporters(root string, dir string, recursive bool, filenameRegexStr st return nil }) if err != nil { - return "", fmt.Errorf("walking directory: %w", err) + err = fmt.Errorf("walking directory: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } importers := map[string]int{} for _, file := range files { - importersList, err := FindImporterForFiles(root, []string{file}) + importersList, err := FindImporterForFiles(ctx, root, []string{file}) if err != nil { - return "", fmt.Errorf("resolving imports: %w", err) + err = fmt.Errorf("resolving imports: %w", err) + telemetry.FailSpanWithError(span, err) + return "", err } importers[file] = len(importersList) } diff --git a/pkg/jsonnet/find_importers_test.go b/pkg/jsonnet/find_importers_test.go index 821562089..750395b9d 100644 --- a/pkg/jsonnet/find_importers_test.go +++ b/pkg/jsonnet/find_importers_test.go @@ -21,7 +21,7 @@ type findImportersTestCase struct { } func (tc findImportersTestCase) run(t testing.TB) { - importers, err := FindImporterForFiles("testdata/findImporters", tc.files) + importers, err := FindImporterForFiles(t.Context(), "testdata/findImporters", tc.files) if tc.expectedErr != nil { require.EqualError(t, err, tc.expectedErr.Error()) @@ -248,7 +248,7 @@ func TestFindImportersForFiles(t *testing.T) { if filepath.Base(file) != jpath.DefaultEntrypoint { continue } - _, err := EvaluateFile(jsonnetImpl, file, Opts{}) + _, err := EvaluateFile(t.Context(), jsonnetImpl, file, Opts{}) require.NoError(t, err, "failed to eval %s", file) } @@ -299,7 +299,7 @@ testdata/findImporters/lib/lib1/subfolder/test.libsonnet: 0 } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - count, err := CountImporters("testdata/findImporters", tc.dir, tc.recursive, tc.fileRegexp) + count, err := CountImporters(t.Context(), "testdata/findImporters", tc.dir, tc.recursive, tc.fileRegexp) require.NoError(t, err) require.Equal(t, tc.expected, count) }) @@ -319,7 +319,7 @@ func BenchmarkFindImporters(b *testing.B) { importersCache = make(map[string][]string) jsonnetFilesCache = make(map[string]map[string]*cachedJsonnetFile) symlinkCache = make(map[string]string) - importers, err := FindImporterForFiles(tempDir, []string{filepath.Join(tempDir, "file10.libsonnet")}) + importers, err := FindImporterForFiles(b.Context(), tempDir, []string{filepath.Join(tempDir, "file10.libsonnet")}) require.NoError(b, err) require.Equal(b, expectedImporters, importers) diff --git a/pkg/jsonnet/imports.go b/pkg/jsonnet/imports.go index 59c27171d..4396c98e5 100644 --- a/pkg/jsonnet/imports.go +++ b/pkg/jsonnet/imports.go @@ -1,6 +1,7 @@ package jsonnet import ( + "context" "crypto/sha256" "encoding/base64" "fmt" @@ -22,7 +23,7 @@ import ( var importsRegexp = regexp.MustCompile(`import(str)?\s+['"]([^'"%()]+)['"]`) // TransitiveImports returns all recursive imports of an environment -func TransitiveImports(dir string) ([]string, error) { +func TransitiveImports(ctx context.Context, dir string) ([]string, error) { dir, err := filepath.Abs(dir) if err != nil { return nil, err diff --git a/pkg/jsonnet/imports_test.go b/pkg/jsonnet/imports_test.go index 24e0ccd26..925450c21 100644 --- a/pkg/jsonnet/imports_test.go +++ b/pkg/jsonnet/imports_test.go @@ -16,7 +16,7 @@ import ( // TestTransitiveImports checks that TransitiveImports is able to report all // recursive imports of a file func TestTransitiveImports(t *testing.T) { - imports, err := TransitiveImports("testdata/importTree") + imports, err := TransitiveImports(t.Context(), "testdata/importTree") fmt.Println(imports) require.NoError(t, err) assert.Equal(t, []string{ diff --git a/pkg/jsonnet/jpath/jpath_test.go b/pkg/jsonnet/jpath/jpath_test.go index 31e31c33e..f2afbe123 100644 --- a/pkg/jsonnet/jpath/jpath_test.go +++ b/pkg/jsonnet/jpath/jpath_test.go @@ -14,7 +14,7 @@ import ( var jsonnetImpl = &goimpl.JsonnetGoImplementation{} func TestResolvePrecedence(t *testing.T) { - s, err := jsonnet.EvaluateFile(jsonnetImpl, "./testdata/precedence/environments/default/main.jsonnet", jsonnet.Opts{}) + s, err := jsonnet.EvaluateFile(t.Context(), jsonnetImpl, "./testdata/precedence/environments/default/main.jsonnet", jsonnet.Opts{}) require.NoError(t, err) want := map[string]string{ diff --git a/pkg/jsonnet/otel.go b/pkg/jsonnet/otel.go new file mode 100644 index 000000000..736426487 --- /dev/null +++ b/pkg/jsonnet/otel.go @@ -0,0 +1,5 @@ +package jsonnet + +import "github.com/grafana/tanka/internal/telemetry" + +var tracer = telemetry.Tracer("jsonnet") diff --git a/pkg/kubernetes/diff.go b/pkg/kubernetes/diff.go index 7eac9b902..a9d8a09ca 100644 --- a/pkg/kubernetes/diff.go +++ b/pkg/kubernetes/diff.go @@ -1,6 +1,7 @@ package kubernetes import ( + "context" "fmt" "github.com/Masterminds/semver" @@ -12,7 +13,9 @@ import ( ) // Diff takes the desired state and returns the differences from the cluster -func (k *Kubernetes) Diff(state manifest.List, opts DiffOpts) (*string, error) { +func (k *Kubernetes) Diff(ctx context.Context, state manifest.List, opts DiffOpts) (*string, error) { + ctx, span := tracer.Start(ctx, "kubernetes.Diff") + span.End() // prevent https://github.com/kubernetes/kubernetes/issues/89762 until fixed if k.ctl.Info().ClientVersion.Equal(semver.MustParse("1.18.0")) { return nil, fmt.Errorf(`you seem to be using kubectl 1.18.0, which contains an unfixed issue diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 8bbbb9bc5..842c54aa0 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -3,11 +3,14 @@ package kubernetes import ( "github.com/Masterminds/semver" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" ) +var tracer = telemetry.Tracer("kubernetes") + // Kubernetes exposes methods to work with the Kubernetes orchestrator type Kubernetes struct { Env v1alpha1.Environment diff --git a/pkg/tanka/evaluators.go b/pkg/tanka/evaluators.go index 796459ea3..701cd8ed6 100644 --- a/pkg/tanka/evaluators.go +++ b/pkg/tanka/evaluators.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "strings" @@ -12,7 +13,7 @@ import ( ) // EvalJsonnet evaluates the jsonnet environment at the given file system path -func evalJsonnet(path string, impl types.JsonnetImplementation, opts jsonnet.Opts) (raw string, err error) { +func evalJsonnet(ctx context.Context, path string, impl types.JsonnetImplementation, opts jsonnet.Opts) (raw string, err error) { entrypoint, err := jpath.Entrypoint(path) if err != nil { return "", err @@ -21,7 +22,7 @@ func evalJsonnet(path string, impl types.JsonnetImplementation, opts jsonnet.Opt // evaluate Jsonnet if opts.EvalScript != "" { // Determine if the entrypoint is a function. - isFunction, err := jsonnet.Evaluate(path, impl, fmt.Sprintf("std.isFunction(import '%s')", entrypoint), opts) + isFunction, err := jsonnet.Evaluate(ctx, path, impl, fmt.Sprintf("std.isFunction(import '%s')", entrypoint), opts) if err != nil { return "", fmt.Errorf("evaluating jsonnet in path '%s': %w", path, err) } @@ -43,14 +44,14 @@ function(%s) `, tlaJoin, entrypoint, tlaJoin, opts.EvalScript) } - raw, err = jsonnet.Evaluate(path, impl, evalScript, opts) + raw, err = jsonnet.Evaluate(ctx, path, impl, evalScript, opts) if err != nil { return "", fmt.Errorf("evaluating jsonnet in path '%s': %w", path, err) } return raw, nil } - raw, err = jsonnet.EvaluateFile(impl, entrypoint, opts) + raw, err = jsonnet.EvaluateFile(ctx, impl, entrypoint, opts) if err != nil { return "", errors.Wrap(err, "evaluating jsonnet") } diff --git a/pkg/tanka/evaluators_test.go b/pkg/tanka/evaluators_test.go index 0802a2152..acf4a6a3d 100644 --- a/pkg/tanka/evaluators_test.go +++ b/pkg/tanka/evaluators_test.go @@ -30,7 +30,7 @@ func TestEvalJsonnet(t *testing.T) { // This will fail intermittently if TLAs are passed as positional // parameters. - json, err := evalJsonnet("testdata/cases/withtlas", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/withtlas", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"foovalue"`, strings.TrimSpace(json)) } @@ -46,7 +46,7 @@ func TestEvalJsonnetWithExpression(t *testing.T) { // This will fail intermittently if TLAs are passed as positional // parameters. - json, err := evalJsonnet("testdata/cases/object", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/object", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"object"`, strings.TrimSpace(json)) }) @@ -59,7 +59,7 @@ func TestEvalWithOptionalTlas(t *testing.T) { opts := jsonnet.Opts{ EvalScript: "main.metadata.name", } - json, err := evalJsonnet("testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"bar-baz"`, strings.TrimSpace(json)) } @@ -71,7 +71,7 @@ func TestEvalWithOptionalTlasSpecifiedArg2(t *testing.T) { EvalScript: "main.metadata.name", TLACode: jsonnet.InjectedCode{"baz": "'changed'"}, } - json, err := evalJsonnet("testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/with-optional-tlas/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"bar-changed"`, strings.TrimSpace(json)) } @@ -82,7 +82,7 @@ func TestEvalFunctionWithNoTlas(t *testing.T) { opts := jsonnet.Opts{ EvalScript: "main.metadata.name", } - json, err := evalJsonnet("testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.Equal(t, `"inline"`, strings.TrimSpace(json)) } @@ -94,7 +94,7 @@ func TestInvalidTlaArg(t *testing.T) { EvalScript: "main", TLACode: jsonnet.InjectedCode{"foo": "'bar'"}, } - json, err := evalJsonnet("testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/function-with-zero-params/main.jsonnet", jsonnetImpl, opts) assert.Contains(t, err.Error(), "function has no parameter foo") assert.Equal(t, "", json) } @@ -106,7 +106,7 @@ func TestTlaWithNonFunction(t *testing.T) { EvalScript: "main", TLACode: jsonnet.InjectedCode{"foo": "'bar'"}, } - json, err := evalJsonnet("testdata/cases/withenv/main.jsonnet", jsonnetImpl, opts) + json, err := evalJsonnet(t.Context(), "testdata/cases/withenv/main.jsonnet", jsonnetImpl, opts) assert.NoError(t, err) assert.NotEmpty(t, json) } diff --git a/pkg/tanka/export.go b/pkg/tanka/export.go index 8346f4fca..0847ce56f 100644 --- a/pkg/tanka/export.go +++ b/pkg/tanka/export.go @@ -2,6 +2,7 @@ package tanka import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -61,7 +62,7 @@ type ExportEnvOpts struct { MergeDeletedEnvs []string } -func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error { +func ExportEnvironments(ctx context.Context, envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error { // Keep track of which file maps to which environment fileToEnv := map[string]string{} @@ -87,7 +88,7 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv } // get all environments for paths - loadedEnvs, err := parallelLoadEnvironments(envs, parallelOpts{ + loadedEnvs, err := parallelLoadEnvironments(ctx, envs, parallelOpts{ Opts: opts.Opts, Selector: opts.Selector, Parallelism: opts.Parallelism, @@ -96,59 +97,65 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv return err } - for _, env := range loadedEnvs { - // get the manifests - loaded, err := LoadManifests(env, opts.Opts.Filters) - if err != nil { - return err - } - - env := loaded.Env - res := loaded.Resources - - // create raw manifest version of env for templating - env.Data = nil - raw, err := json.Marshal(env) - if err != nil { - return err - } - var menv manifest.Manifest - if err := json.Unmarshal(raw, &menv); err != nil { - return err - } - - // create template - manifestTemplate, err := createTemplate(opts.Format, menv) - if err != nil { - return fmt.Errorf("parsing format: %s", err) - } + { + ctx, span := tracer.Start(ctx, "generateManifests") + defer span.End() - // write each to a file - for _, m := range res { - // apply template - name, err := applyTemplate(manifestTemplate, m) + // FINDING: Generating the export files takes some time. Perhaps we should parallelize this. + for _, env := range loadedEnvs { + // get the manifests + loaded, err := LoadManifests(ctx, env, opts.Opts.Filters) if err != nil { - return fmt.Errorf("executing name template: %w", err) + return err } - // Create all subfolders in path - relpath := name + "." + opts.Extension - path := filepath.Join(to, relpath) + env := loaded.Env + res := loaded.Resources - fileToEnv[relpath] = env.Metadata.Namespace - - // Abort if already exists - if exists, err := fileExists(path); err != nil { + // create raw manifest version of env for templating + env.Data = nil + raw, err := json.Marshal(env) + if err != nil { return err - } else if exists { - return fmt.Errorf("file '%s' already exists. Aborting", path) } - - // Write manifest - data := m.String() - if err := writeExportFile(path, []byte(data)); err != nil { + var menv manifest.Manifest + if err := json.Unmarshal(raw, &menv); err != nil { return err } + + // create template + manifestTemplate, err := createTemplate(opts.Format, menv) + if err != nil { + return fmt.Errorf("parsing format: %s", err) + } + + // write each to a file + for _, m := range res { + // apply template + name, err := applyTemplate(manifestTemplate, m) + if err != nil { + return fmt.Errorf("executing name template: %w", err) + } + + // Create all subfolders in path + relpath := name + "." + opts.Extension + path := filepath.Join(to, relpath) + + fileToEnv[relpath] = env.Metadata.Namespace + + // Abort if already exists + if exists, err := fileExists(path); err != nil { + return err + } else if exists { + return fmt.Errorf("file '%s' already exists. Aborting", path) + } + + // Write manifest + data := m.String() + if err := writeExportFile(path, []byte(data)); err != nil { + return err + } + } } } diff --git a/pkg/tanka/export_test.go b/pkg/tanka/export_test.go index 7bf000d8b..276c9fe1e 100644 --- a/pkg/tanka/export_test.go +++ b/pkg/tanka/export_test.go @@ -50,7 +50,7 @@ func TestExportEnvironments(t *testing.T) { defer func() { require.NoError(t, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(t.Context(), "test-export-envs", FindOpts{Selector: labels.Everything()}) require.NoError(t, err) // Export all envs @@ -62,7 +62,7 @@ func TestExportEnvironments(t *testing.T) { "deploymentName": "'initial-deployment'", "serviceName": "'initial-service'", } - require.NoError(t, ExportEnvironments(envs, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), envs, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "inline-namespace1", "my-configmap.yaml"), filepath.Join(tempDir, "inline-namespace1", "my-deployment.yaml"), @@ -86,11 +86,11 @@ func TestExportEnvironments(t *testing.T) { }`) // Try to re-export - assert.EqualError(t, ExportEnvironments(envs, tempDir, opts), fmt.Sprintf("output dir `%s` not empty. Pass a different --merge-strategy to ignore this", tempDir)) + assert.EqualError(t, ExportEnvironments(t.Context(), envs, tempDir, opts), fmt.Sprintf("output dir `%s` not empty. Pass a different --merge-strategy to ignore this", tempDir)) // Try to re-export with the --merge-strategy=fail-on-conflicts flag. Will still fail because Tanka will not overwrite manifests silently opts.MergeStrategy = ExportMergeStrategyFailConflicts - assert.ErrorContains(t, ExportEnvironments(envs, tempDir, opts), "already exists. Aborting") + assert.ErrorContains(t, ExportEnvironments(t.Context(), envs, tempDir, opts), "already exists. Aborting") // Re-export only one env with --merge-stategy=replace-envs flag opts.Opts.ExtCode = jsonnet.InjectedCode{ @@ -98,9 +98,9 @@ func TestExportEnvironments(t *testing.T) { "serviceName": "'updated-service'", } opts.MergeStrategy = ExportMergeStrategyReplaceEnvs - staticEnv, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.SelectorFromSet(labels.Set{"type": "static"})}) + staticEnv, err := FindEnvs(t.Context(), "test-export-envs", FindOpts{Selector: labels.SelectorFromSet(labels.Set{"type": "static"})}) require.NoError(t, err) - require.NoError(t, ExportEnvironments(staticEnv, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), staticEnv, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "inline-namespace1", "my-configmap.yaml"), filepath.Join(tempDir, "inline-namespace1", "my-deployment.yaml"), @@ -129,7 +129,7 @@ func TestExportEnvironments(t *testing.T) { "serviceName": "'updated-again-service'", } opts.MergeDeletedEnvs = []string{"test-export-envs/inline-envs/main.jsonnet"} - require.NoError(t, ExportEnvironments(staticEnv, tempDir, opts)) + require.NoError(t, ExportEnvironments(t.Context(), staticEnv, tempDir, opts)) checkFiles(t, tempDir, []string{ filepath.Join(tempDir, "static", "updated-again-deployment.yaml"), filepath.Join(tempDir, "static", "updated-again-service.yaml"), @@ -149,7 +149,7 @@ func TestExportEnvironmentsBroken(t *testing.T) { defer func() { require.NoError(t, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs-broken", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(t.Context(), "test-export-envs-broken", FindOpts{Selector: labels.Everything()}) require.NoError(t, err) // Export all envs @@ -159,7 +159,7 @@ func TestExportEnvironmentsBroken(t *testing.T) { } var schemaError *manifest.SchemaError - require.ErrorAs(t, ExportEnvironments(envs, tempDir, opts), &schemaError) + require.ErrorAs(t, ExportEnvironments(t.Context(), envs, tempDir, opts), &schemaError) } func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { @@ -169,7 +169,7 @@ func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { defer func() { require.NoError(b, os.Chdir("..")) }() // Find envs - envs, err := FindEnvs("test-export-envs", FindOpts{Selector: labels.Everything()}) + envs, err := FindEnvs(b.Context(), "test-export-envs", FindOpts{Selector: labels.Everything()}) require.NoError(b, err) // Export all envs @@ -183,12 +183,12 @@ func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) { "serviceName": "'initial-service'", } // Export a first time so that the benchmark loops are identical - require.NoError(b, ExportEnvironments(envs, tempDir, opts)) + require.NoError(b, ExportEnvironments(b.Context(), envs, tempDir, opts)) // On every loop, delete manifests from previous envs + reexport all envs b.ResetTimer() for i := 0; i < b.N; i++ { - require.NoError(b, ExportEnvironments(envs, tempDir, opts), "failed on iteration %d", i) + require.NoError(b, ExportEnvironments(b.Context(), envs, tempDir, opts), "failed on iteration %d", i) } } diff --git a/pkg/tanka/find.go b/pkg/tanka/find.go index 4dad7b3ae..2d6d2c440 100644 --- a/pkg/tanka/find.go +++ b/pkg/tanka/find.go @@ -1,11 +1,13 @@ package tanka import ( + "context" "fmt" "path/filepath" "runtime" "time" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -26,16 +28,18 @@ type FindOpts struct { // Each directory is tested and included if it is a valid environment, either // static or inline. If a directory is a valid environment, its subdirectories // are not checked. -func FindEnvs(path string, opts FindOpts) ([]*v1alpha1.Environment, error) { - return findEnvsFromPaths([]string{path}, opts) +func FindEnvs(ctx context.Context, path string, opts FindOpts) ([]*v1alpha1.Environment, error) { + return findEnvsFromPaths(ctx, []string{path}, opts) } // FindEnvsFromPaths does the same as FindEnvs but takes a list of paths instead -func FindEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { - return findEnvsFromPaths(paths, opts) +func FindEnvsFromPaths(ctx context.Context, paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { + return findEnvsFromPaths(ctx, paths, opts) } -func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { +func findEnvsFromPaths(ctx context.Context, paths []string, opts FindOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.findEnvsFromPaths") + defer span.End() if opts.Parallelism <= 0 { opts.Parallelism = runtime.NumCPU() } @@ -45,14 +49,18 @@ func findEnvsFromPaths(paths []string, opts FindOpts) ([]*v1alpha1.Environment, jsonnetFiles, err := findJsonnetFilesFromPaths(paths, opts) if err != nil { - return nil, fmt.Errorf("finding jsonnet files: %w", err) + err = fmt.Errorf("finding jsonnet files: %w", err) + telemetry.FailSpanWithError(span, err) + return nil, err } findJsonnetFilesEndTime := time.Now() - envs, err := findEnvsFromJsonnetFiles(jsonnetFiles, opts) + envs, err := findEnvsFromJsonnetFiles(ctx, jsonnetFiles, opts) if err != nil { - return nil, fmt.Errorf("finding environments: %w", err) + err = fmt.Errorf("finding environments: %w", err) + telemetry.FailSpanWithError(span, err) + return nil, err } findEnvsEndTime := time.Now() @@ -117,7 +125,7 @@ func findJsonnetFilesFromPaths(paths []string, opts FindOpts) ([]string, error) } // find all environments within jsonnet files -func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) { +func findEnvsFromJsonnetFiles(ctx context.Context, jsonnetFiles []string, opts FindOpts) ([]*v1alpha1.Environment, error) { type findEnvsOut struct { envs []*v1alpha1.Environment err error @@ -134,7 +142,7 @@ func findEnvsFromJsonnetFiles(jsonnetFiles []string, opts FindOpts) ([]*v1alpha1 for jsonnetFile := range jsonnetFilesChan { // try if this has envs - list, err := List(jsonnetFile, Opts{JsonnetOpts: jsonnetOpts, JsonnetImplementation: opts.JsonnetImplementation}) + list, err := List(ctx, jsonnetFile, Opts{JsonnetOpts: jsonnetOpts, JsonnetImplementation: opts.JsonnetImplementation}) if err != nil && // expected when looking for environments !errors.As(err, &jpath.ErrorNoBase{}) && diff --git a/pkg/tanka/find_test.go b/pkg/tanka/find_test.go index 5f2f20a03..381a4eea4 100644 --- a/pkg/tanka/find_test.go +++ b/pkg/tanka/find_test.go @@ -15,7 +15,7 @@ func BenchmarkFindEnvsFromSinglePath(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - envs, err := FindEnvs(tempDir, FindOpts{}) + envs, err := FindEnvs(b.Context(), tempDir, FindOpts{}) require.Len(b, envs, 200) require.NoError(b, err) } @@ -27,7 +27,7 @@ func BenchmarkFindEnvsFromPaths(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - envs, err := FindEnvsFromPaths(envPaths, FindOpts{}) + envs, err := FindEnvsFromPaths(b.Context(), envPaths, FindOpts{}) require.Len(b, envs, 200) require.NoError(b, err) } diff --git a/pkg/tanka/inline.go b/pkg/tanka/inline.go index aa614aca3..0a8e02ab9 100644 --- a/pkg/tanka/inline.go +++ b/pkg/tanka/inline.go @@ -1,11 +1,13 @@ package tanka import ( + "context" "encoding/json" "fmt" "path/filepath" "sort" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/kubernetes/manifest" @@ -21,25 +23,36 @@ type InlineLoader struct { jsonnetImpl types.JsonnetImplementation } -func (i *InlineLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { +func (i *InlineLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "inlineLoader.Load") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(SingleEnvEvalScript, opts.Name) } - return i.load(path, opts) + return i.load(ctx, path, opts) } -func (i *InlineLoader) Peek(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { +func (i *InlineLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "inlineLoader.Peek") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + opts.JsonnetOpts.EvalScript = MetadataEvalScript if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(MetadataSingleEnvEvalScript, opts.Name) } - env, err := i.load(path, opts) + env, err := i.load(ctx, path, opts) + telemetry.FailSpanWithError(span, err) return env, err } // abstracted out as Peek and Load need different JsonnetOpts -func (i *InlineLoader) load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - data, err := i.Eval(path, opts) +func (i *InlineLoader) load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + data, err := i.Eval(ctx, path, opts) if err != nil { return nil, err } @@ -84,15 +97,22 @@ func (i *InlineLoader) load(path string, opts LoaderOpts) (*v1alpha1.Environment return env, nil } -func (i *InlineLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { +func (i *InlineLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "inlineLoader.List") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + opts.JsonnetOpts.EvalScript = MetadataEvalScript - data, err := i.Eval(path, opts) + data, err := i.Eval(ctx, path, opts) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } list, err := extractEnvs(data) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } @@ -100,11 +120,13 @@ func (i *InlineLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environme for _, raw := range list { data, err := json.Marshal(raw) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } env, err := inlineParse(path, data) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } @@ -114,17 +136,24 @@ func (i *InlineLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environme return envs, nil } -func (i *InlineLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { +func (i *InlineLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { + ctx, span := tracer.Start(ctx, "inlineLoader.Eval") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + // Can't provide env as extVar, as we need to evaluate Jsonnet first to know it opts.ExtCode.Set(environmentExtCode, `error "Using tk.env and std.extVar('tanka.dev/environment') is only supported for static environments. Directly access this data using standard Jsonnet instead."`) - raw, err := evalJsonnet(path, i.jsonnetImpl, opts.JsonnetOpts) + raw, err := evalJsonnet(ctx, path, i.jsonnetImpl, opts.JsonnetOpts) if err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } var data interface{} if err := json.Unmarshal([]byte(raw), &data); err != nil { + telemetry.FailSpanWithError(span, err) return nil, err } diff --git a/pkg/tanka/load.go b/pkg/tanka/load.go index 0721c6f7b..55f52c673 100644 --- a/pkg/tanka/load.go +++ b/pkg/tanka/load.go @@ -1,11 +1,13 @@ package tanka import ( + "context" "fmt" "os" "path/filepath" "strings" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/binary" "github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" @@ -17,6 +19,7 @@ import ( "github.com/grafana/tanka/pkg/spec/v1alpha1" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel/attribute" ) // environmentExtCode is the extCode ID `tk.env` uses underneath @@ -25,13 +28,13 @@ const environmentExtCode = spec.APIGroup + "/environment" // Load loads the Environment at `path`. It automatically detects whether to // load inline or statically -func Load(path string, opts Opts) (*LoadResult, error) { - env, err := LoadEnvironment(path, opts) +func Load(ctx context.Context, path string, opts Opts) (*LoadResult, error) { + env, err := LoadEnvironment(ctx, path, opts) if err != nil { return nil, err } - result, err := LoadManifests(env, opts.Filters) + result, err := LoadManifests(ctx, env, opts.Filters) if err != nil { return nil, err } @@ -45,7 +48,11 @@ func Load(path string, opts Opts) (*LoadResult, error) { return result, nil } -func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { +func LoadEnvironment(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.LoadEnvironment") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path), attribute.String("tanka.nameFilter", opts.Name)) + _, err := os.Stat(path) if os.IsNotExist(err) { log.Info().Msgf("Path %q does not exist, trying to use it as an environment name", path) @@ -60,7 +67,7 @@ func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { return nil, err } - env, err := loader.Load(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + env, err := loader.Load(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) if err != nil { return nil, err } @@ -68,7 +75,11 @@ func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) { return env, nil } -func LoadManifests(env *v1alpha1.Environment, filters process.Matchers) (*LoadResult, error) { +func LoadManifests(ctx context.Context, env *v1alpha1.Environment, filters process.Matchers) (*LoadResult, error) { + _, span := tracer.Start(ctx, "tanka.LoadManifests") + defer span.End() + span.SetAttributes(telemetry.AttrEnv(env)...) + if err := checkVersion(env.Spec.ExpectVersions.Tanka); err != nil { return nil, err } @@ -83,25 +94,32 @@ func LoadManifests(env *v1alpha1.Environment, filters process.Matchers) (*LoadRe // Peek loads the metadata of the environment at path. To get resources as well, // use Load -func Peek(path string, opts Opts) (*v1alpha1.Environment, error) { +func Peek(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.Peek") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + loader, err := DetectLoader(path, opts) if err != nil { return nil, err } - return loader.Peek(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.Peek(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } // List finds metadata of all environments at path that could possibly be // loaded. List can be used to deal with multiple inline environments, by first // listing them, choosing the right one and then only loading that one -func List(path string, opts Opts) ([]*v1alpha1.Environment, error) { +func List(ctx context.Context, path string, opts Opts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.Peek") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) loader, err := DetectLoader(path, opts) if err != nil { return nil, err } - return loader.List(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.List(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementation, error) { @@ -133,13 +151,16 @@ func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementati } // Eval returns the raw evaluated Jsonnet -func Eval(path string, opts Opts) (interface{}, error) { +func Eval(ctx context.Context, path string, opts Opts) (interface{}, error) { + ctx, span := tracer.Start(ctx, "tanka.Eval") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) loader, err := DetectLoader(path, opts) if err != nil { return nil, err } - return loader.Eval(path, LoaderOpts{opts.JsonnetOpts, opts.Name}) + return loader.Eval(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } // DetectLoader detects whether the environment is inline or static and picks @@ -173,17 +194,17 @@ func DetectLoader(path string, opts Opts) (Loader, error) { // Loader is an abstraction over the process of loading Environments type Loader interface { // Load a single environment at path - Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) + Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) // Peek only loads metadata and omits the actual resources - Peek(path string, opts LoaderOpts) (*v1alpha1.Environment, error) + Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) // List returns metadata of all possible environments at path that can be // loaded - List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) + List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) // Eval returns the raw evaluated Jsonnet - Eval(path string, opts LoaderOpts) (interface{}, error) + Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) } type LoaderOpts struct { @@ -191,6 +212,13 @@ type LoaderOpts struct { Name string } +func OTELAttrFromLoaderOpts(opts *LoaderOpts) []attribute.KeyValue { + result := make([]attribute.KeyValue, 0, 2) + result = append(result, attribute.String("tanka.loader.options.name", opts.Name)) + result = append(result, attribute.String("tanka.loader.options.cache_path", opts.CachePath)) + return result +} + type LoadResult struct { Env *v1alpha1.Environment Resources manifest.List diff --git a/pkg/tanka/load_test.go b/pkg/tanka/load_test.go index b2f44d6fc..9843b6ef6 100644 --- a/pkg/tanka/load_test.go +++ b/pkg/tanka/load_test.go @@ -147,7 +147,7 @@ func TestLoad(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { - l, err := Load(test.baseDir, Opts{}) + l, err := Load(t.Context(), test.baseDir, Opts{}) require.NoError(t, err) assert.Equal(t, test.expected, l.Resources) @@ -158,24 +158,24 @@ func TestLoad(t *testing.T) { func TestLoadSelectEnvironment(t *testing.T) { // No match - _, err := Load("./testdata/cases/multiple-inline-envs", Opts{Name: "no match"}) + _, err := Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "no match"}) assert.EqualError(t, err, "found no matching environments; run 'tk env list ./testdata/cases/multiple-inline-envs' to view available options") // Empty options, match all environments - _, err = Load("./testdata/cases/multiple-inline-envs", Opts{}) + _, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{}) assert.EqualError(t, err, "found multiple Environments in \"./testdata/cases/multiple-inline-envs\". Use `--name` to select a single one: \n - project1-env1\n - project1-env2\n - project2-env1") // Partial match two environments - _, err = Load("./testdata/cases/multiple-inline-envs", Opts{Name: "env1"}) + _, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "env1"}) assert.EqualError(t, err, "found multiple Environments in \"./testdata/cases/multiple-inline-envs\" matching \"env1\". Provide a more specific name that matches a single one: \n - project1-env1\n - project2-env1") // Partial match - result, err := Load("./testdata/cases/multiple-inline-envs", Opts{Name: "project2"}) + result, err := Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "project2"}) assert.NoError(t, err) assert.Equal(t, "project2-env1", result.Env.Metadata.Name) // Full match - result, err = Load("./testdata/cases/multiple-inline-envs", Opts{Name: "project1-env1"}) + result, err = Load(t.Context(), "./testdata/cases/multiple-inline-envs", Opts{Name: "project1-env1"}) assert.NoError(t, err) assert.Equal(t, "project1-env1", result.Env.Metadata.Name) } @@ -190,16 +190,16 @@ func TestLoadEnvironmentFallbackToName(t *testing.T) { defer func() { require.NoError(t, os.Chdir(cwd)) }() // Partial match two environments - _, err = Load("env1", Opts{}) + _, err = Load(t.Context(), "env1", Opts{}) assert.EqualError(t, err, "found multiple Environments in \".\" matching \"env1\". Provide a more specific name that matches a single one: \n - project1-env1\n - project2-env1") // Partial match - result, err := Load("project2", Opts{}) + result, err := Load(t.Context(), "project2", Opts{}) require.NoError(t, err) assert.Equal(t, "project2-env1", result.Env.Metadata.Name) // Full match - result, err = Load("project1-env1", Opts{}) + result, err = Load(t.Context(), "project1-env1", Opts{}) require.NoError(t, err) assert.Equal(t, "project1-env1", result.Env.Metadata.Name) } @@ -207,13 +207,13 @@ func TestLoadEnvironmentFallbackToName(t *testing.T) { func TestLoadSelectEnvironmentFullMatchHasPriority(t *testing.T) { // `base` matches both `base` and `base-and-more` // However, the full match should win - result, err := Load("./testdata/cases/inline-name-conflict", Opts{Name: "base"}) + result, err := Load(t.Context(), "./testdata/cases/inline-name-conflict", Opts{Name: "base"}) assert.NoError(t, err) assert.Equal(t, "base", result.Env.Metadata.Name) } func TestLoadFailsWhenBothSpecAndInline(t *testing.T) { - _, err := Load("./testdata/cases/static-and-inline", Opts{Name: "inline"}) + _, err := Load(t.Context(), "./testdata/cases/static-and-inline", Opts{Name: "inline"}) assert.EqualError(t, err, "found a tanka Environment resource. Check that you aren't using a spec.json and inline environments simultaneously") } diff --git a/pkg/tanka/parallel.go b/pkg/tanka/parallel.go index 483e07391..ae34e043f 100644 --- a/pkg/tanka/parallel.go +++ b/pkg/tanka/parallel.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "path/filepath" "time" @@ -22,7 +23,10 @@ type parallelOpts struct { } // parallelLoadEnvironments evaluates multiple environments in parallel -func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) ([]*v1alpha1.Environment, error) { +func parallelLoadEnvironments(ctx context.Context, envs []*v1alpha1.Environment, opts parallelOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "tanka.parallelLoadEnvironments") + defer span.End() + jobsCh := make(chan parallelJob) outCh := make(chan parallelOut, len(envs)) @@ -36,7 +40,7 @@ func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) ( } for i := 0; i < opts.Parallelism; i++ { - go parallelWorker(jobsCh, outCh) + go parallelWorker(ctx, jobsCh, outCh) } for _, env := range envs { @@ -100,12 +104,14 @@ type parallelOut struct { err error } -func parallelWorker(jobsCh <-chan parallelJob, outCh chan parallelOut) { +func parallelWorker(ctx context.Context, jobsCh <-chan parallelJob, outCh chan parallelOut) { + ctx, span := tracer.Start(ctx, "tanka.parallelWorker") + defer span.End() for job := range jobsCh { log.Debug().Str("name", job.opts.Name).Str("path", job.path).Msg("Loading environment") startTime := time.Now() - env, err := LoadEnvironment(job.path, job.opts) + env, err := LoadEnvironment(ctx, job.path, job.opts) if err != nil { err = fmt.Errorf("%s:\n %w", job.path, err) } diff --git a/pkg/tanka/prune.go b/pkg/tanka/prune.go index a377f0a81..ae3f71106 100644 --- a/pkg/tanka/prune.go +++ b/pkg/tanka/prune.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "os" @@ -17,9 +18,9 @@ type PruneOpts struct { // Prune deletes all resources from the cluster, that are no longer present in // Jsonnet. It uses the `tanka.dev/environment` label to identify those. -func Prune(baseDir string, opts PruneOpts) error { +func Prune(ctx context.Context, baseDir string, opts PruneOpts) error { // parse jsonnet, init k8s client - p, err := Load(baseDir, opts.Opts) + p, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } diff --git a/pkg/tanka/static.go b/pkg/tanka/static.go index d978b283c..c56957bdd 100644 --- a/pkg/tanka/static.go +++ b/pkg/tanka/static.go @@ -1,8 +1,10 @@ package tanka import ( + "context" "encoding/json" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/spec" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -15,13 +17,18 @@ type StaticLoader struct { jsonnetImpl types.JsonnetImplementation } -func (s StaticLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - config, err := s.Peek(path, opts) +func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "staticLoader.Load") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + + config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } - data, err := s.Eval(path, opts) + data, err := s.Eval(ctx, path, opts) if err != nil { return nil, err } @@ -30,7 +37,12 @@ func (s StaticLoader) Load(path string, opts LoaderOpts) (*v1alpha1.Environment, return config, nil } -func (s StaticLoader) Peek(path string, _ LoaderOpts) (*v1alpha1.Environment, error) { +func (s StaticLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "staticLoader.Peek") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + config, err := parseStaticSpec(path) if err != nil { return nil, err @@ -39,8 +51,13 @@ func (s StaticLoader) Peek(path string, _ LoaderOpts) (*v1alpha1.Environment, er return config, nil } -func (s StaticLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { - env, err := s.Peek(path, opts) +func (s StaticLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { + ctx, span := tracer.Start(ctx, "staticLoader.List") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + + env, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } @@ -48,8 +65,13 @@ func (s StaticLoader) List(path string, opts LoaderOpts) ([]*v1alpha1.Environmen return []*v1alpha1.Environment{env}, nil } -func (s *StaticLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { - config, err := s.Peek(path, opts) +func (s *StaticLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { + ctx, span := tracer.Start(ctx, "staticLoader.Eval") + defer span.End() + span.SetAttributes(telemetry.AttrPath(path)) + span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) + + config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err } @@ -60,7 +82,7 @@ func (s *StaticLoader) Eval(path string, opts LoaderOpts) (interface{}, error) { } opts.ExtCode.Set(environmentExtCode, envCode) - raw, err := evalJsonnet(path, s.jsonnetImpl, opts.JsonnetOpts) + raw, err := evalJsonnet(ctx, path, s.jsonnetImpl, opts.JsonnetOpts) if err != nil { return nil, err } diff --git a/pkg/tanka/status.go b/pkg/tanka/status.go index eb8cdb659..756d8aa73 100644 --- a/pkg/tanka/status.go +++ b/pkg/tanka/status.go @@ -1,6 +1,8 @@ package tanka import ( + "context" + "github.com/grafana/tanka/pkg/kubernetes/client" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -16,8 +18,8 @@ type Info struct { } // Status returns information about the particular environment -func Status(baseDir string, opts Opts) (*Info, error) { - r, err := Load(baseDir, opts) +func Status(ctx context.Context, baseDir string, opts Opts) (*Info, error) { + r, err := Load(ctx, baseDir, opts) if err != nil { return nil, err } diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go index eee730ce7..7a85ad61c 100644 --- a/pkg/tanka/tanka.go +++ b/pkg/tanka/tanka.go @@ -9,6 +9,7 @@ import ( "github.com/Masterminds/semver" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet" "github.com/grafana/tanka/pkg/process" ) @@ -31,6 +32,8 @@ type Opts struct { // provided using ldflags const defaultDevVersion = "dev" +var tracer = telemetry.Tracer("tanka") + // CurrentVersion is the current version of the running Tanka code var CurrentVersion = defaultDevVersion diff --git a/pkg/tanka/workflow.go b/pkg/tanka/workflow.go index b965e2b85..830639e46 100644 --- a/pkg/tanka/workflow.go +++ b/pkg/tanka/workflow.go @@ -1,6 +1,7 @@ package tanka import ( + "context" "fmt" "os" @@ -73,8 +74,8 @@ func (e ErrorApplyStrategyUnknown) Error() string { // Apply parses the environment at the given directory (a `baseDir`) and applies // the evaluated jsonnet to the Kubernetes cluster defined in the environments // `spec.json`. -func Apply(baseDir string, opts ApplyOpts) error { - l, err := Load(baseDir, opts.Opts) +func Apply(ctx context.Context, baseDir string, opts ApplyOpts) error { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } @@ -105,7 +106,7 @@ func Apply(baseDir string, opts ApplyOpts) error { var noChanges bool if opts.DiffStrategy != "none" { // show diff - diff, err := kube.Diff(l.Resources, kubernetes.DiffOpts{Strategy: opts.DiffStrategy}) + diff, err := kube.Diff(ctx, l.Resources, kubernetes.DiffOpts{Strategy: opts.DiffStrategy}) switch { case err != nil: // This is not fatal, the diff is not strictly required @@ -176,8 +177,8 @@ type DiffOpts struct { // `WithDiffSummarize` modifier is used, a histogram is returned instead. // The cluster information is retrieved from the environments `spec.json`. // NOTE: This function requires on `diff(1)` and `kubectl(1)` -func Diff(baseDir string, opts DiffOpts) (*string, error) { - l, err := Load(baseDir, opts.Opts) +func Diff(ctx context.Context, baseDir string, opts DiffOpts) (*string, error) { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return nil, err } @@ -187,7 +188,7 @@ func Diff(baseDir string, opts DiffOpts) (*string, error) { } defer kube.Close() - return kube.Diff(l.Resources, kubernetes.DiffOpts{ + return kube.Diff(ctx, l.Resources, kubernetes.DiffOpts{ Summarize: opts.Summarize, Strategy: opts.Strategy, WithPrune: opts.WithPrune, @@ -202,8 +203,8 @@ type DeleteOpts struct { // Delete parses the environment at the given directory (a `baseDir`) and deletes // the generated objects from the Kubernetes cluster defined in the environment's // `spec.json`. -func Delete(baseDir string, opts DeleteOpts) error { - l, err := Load(baseDir, opts.Opts) +func Delete(ctx context.Context, baseDir string, opts DeleteOpts) error { + l, err := Load(ctx, baseDir, opts.Opts) if err != nil { return err } @@ -245,8 +246,8 @@ func Delete(baseDir string, opts DeleteOpts) error { // Show parses the environment at the given directory (a `baseDir`) and returns // the list of Kubernetes objects. // Tip: use the `String()` function on the returned list to get the familiar yaml stream -func Show(baseDir string, opts Opts) (manifest.List, error) { - l, err := Load(baseDir, opts) +func Show(ctx context.Context, baseDir string, opts Opts) (manifest.List, error) { + l, err := Load(ctx, baseDir, opts) if err != nil { return nil, err } From c5bf3c968d849730936036c744f7d8ad6c6b50cd Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 11:29:16 +0200 Subject: [PATCH 3/7] Add docs --- docs/astro.config.ts | 4 ++++ docs/src/content/docs/telemetry.md | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/src/content/docs/telemetry.md diff --git a/docs/astro.config.ts b/docs/astro.config.ts index a737c0884..dc730179d 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -104,6 +104,10 @@ export default defineConfig({ label: 'Server-Side Apply', link: '/server-side-apply', }, + { + label: 'Telemetry', + link: '/telemetry', + }, ], }, { diff --git a/docs/src/content/docs/telemetry.md b/docs/src/content/docs/telemetry.md new file mode 100644 index 000000000..b6f506909 --- /dev/null +++ b/docs/src/content/docs/telemetry.md @@ -0,0 +1,12 @@ +--- +title: Telemetry +--- + +Tanka supports sending OpenTelemetry traces to a `http/protobuf` endpoint. +To use this, export the following environment variable: + +``` +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +``` + +Note that we try to keep traces to the critical paths around the `export` command since these are usually the areas where performance is most important in automated workflows. From 6982bafcce5cbc9df543440a2c55716540569ba1 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 13:11:21 +0200 Subject: [PATCH 4/7] Remove too noisy span on jsonnet.Evaluate --- pkg/jsonnet/eval.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go index c874e8ecc..113fd011c 100644 --- a/pkg/jsonnet/eval.go +++ b/pkg/jsonnet/eval.go @@ -105,10 +105,6 @@ func EvaluateFile(ctx context.Context, impl types.JsonnetImplementation, jsonnet // If cache options are given, a hash from the data will be computed and // the resulting string will be cached for future retrieval func Evaluate(ctx context.Context, path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { - ctx, span := tracer.Start(ctx, "jsonnet.Evaluate") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateAnonymousSnippet(data) } From 587424ca64cce68cf802d8a71a3a43be699038c1 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 13:50:23 +0200 Subject: [PATCH 5/7] Remove spans on the loader level to reduce noise --- internal/telemetry/attributes.go | 4 ++++ pkg/tanka/inline.go | 32 ++++---------------------------- pkg/tanka/load.go | 9 ++++++++- pkg/tanka/static.go | 25 ++++--------------------- 4 files changed, 20 insertions(+), 50 deletions(-) diff --git a/internal/telemetry/attributes.go b/internal/telemetry/attributes.go index 3d9d663b5..0fa650945 100644 --- a/internal/telemetry/attributes.go +++ b/internal/telemetry/attributes.go @@ -11,6 +11,10 @@ func AttrPath(v string) attribute.KeyValue { return attribute.String("tanka.path", v) } +func AttrLoader(v string) attribute.KeyValue { + return attribute.String("tanka.loader", v) +} + func AttrEnv(v *v1alpha1.Environment) []attribute.KeyValue { return []attribute.KeyValue{ attribute.String("tanka.env.id", fmt.Sprintf("%s@%s", v.Metadata.Name, v.Spec.APIServer)), diff --git a/pkg/tanka/inline.go b/pkg/tanka/inline.go index 0a8e02ab9..75d21cf9c 100644 --- a/pkg/tanka/inline.go +++ b/pkg/tanka/inline.go @@ -7,7 +7,6 @@ import ( "path/filepath" "sort" - "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/jsonnet/jpath" "github.com/grafana/tanka/pkg/kubernetes/manifest" @@ -23,12 +22,11 @@ type InlineLoader struct { jsonnetImpl types.JsonnetImplementation } -func (i *InlineLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "inlineLoader.Load") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) +func (i *InlineLoader) Name() string { + return "inline" +} +func (i *InlineLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(SingleEnvEvalScript, opts.Name) } @@ -36,17 +34,11 @@ func (i *InlineLoader) Load(ctx context.Context, path string, opts LoaderOpts) ( } func (i *InlineLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "inlineLoader.Peek") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - opts.JsonnetOpts.EvalScript = MetadataEvalScript if opts.Name != "" { opts.JsonnetOpts.EvalScript = fmt.Sprintf(MetadataSingleEnvEvalScript, opts.Name) } env, err := i.load(ctx, path, opts) - telemetry.FailSpanWithError(span, err) return env, err } @@ -98,21 +90,14 @@ func (i *InlineLoader) load(ctx context.Context, path string, opts LoaderOpts) ( } func (i *InlineLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "inlineLoader.List") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - opts.JsonnetOpts.EvalScript = MetadataEvalScript data, err := i.Eval(ctx, path, opts) if err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } list, err := extractEnvs(data) if err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } @@ -120,13 +105,11 @@ func (i *InlineLoader) List(ctx context.Context, path string, opts LoaderOpts) ( for _, raw := range list { data, err := json.Marshal(raw) if err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } env, err := inlineParse(path, data) if err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } @@ -137,23 +120,16 @@ func (i *InlineLoader) List(ctx context.Context, path string, opts LoaderOpts) ( } func (i *InlineLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { - ctx, span := tracer.Start(ctx, "inlineLoader.Eval") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - // Can't provide env as extVar, as we need to evaluate Jsonnet first to know it opts.ExtCode.Set(environmentExtCode, `error "Using tk.env and std.extVar('tanka.dev/environment') is only supported for static environments. Directly access this data using standard Jsonnet instead."`) raw, err := evalJsonnet(ctx, path, i.jsonnetImpl, opts.JsonnetOpts) if err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } var data interface{} if err := json.Unmarshal([]byte(raw), &data); err != nil { - telemetry.FailSpanWithError(span, err) return nil, err } diff --git a/pkg/tanka/load.go b/pkg/tanka/load.go index 55f52c673..aa1bbe08f 100644 --- a/pkg/tanka/load.go +++ b/pkg/tanka/load.go @@ -66,6 +66,7 @@ func LoadEnvironment(ctx context.Context, path string, opts Opts) (*v1alpha1.Env if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) env, err := loader.Load(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) if err != nil { @@ -103,6 +104,7 @@ func Peek(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, e if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) return loader.Peek(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } @@ -111,13 +113,14 @@ func Peek(ctx context.Context, path string, opts Opts) (*v1alpha1.Environment, e // loaded. List can be used to deal with multiple inline environments, by first // listing them, choosing the right one and then only loading that one func List(ctx context.Context, path string, opts Opts) ([]*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "tanka.Peek") + ctx, span := tracer.Start(ctx, "tanka.List") defer span.End() span.SetAttributes(telemetry.AttrPath(path)) loader, err := DetectLoader(path, opts) if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) return loader.List(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } @@ -159,6 +162,7 @@ func Eval(ctx context.Context, path string, opts Opts) (interface{}, error) { if err != nil { return nil, err } + span.SetAttributes(telemetry.AttrLoader(loader.Name())) return loader.Eval(ctx, path, LoaderOpts{opts.JsonnetOpts, opts.Name}) } @@ -193,6 +197,9 @@ func DetectLoader(path string, opts Opts) (Loader, error) { // Loader is an abstraction over the process of loading Environments type Loader interface { + // Name of the loader + Name() string + // Load a single environment at path Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) diff --git a/pkg/tanka/static.go b/pkg/tanka/static.go index c56957bdd..8527ac78a 100644 --- a/pkg/tanka/static.go +++ b/pkg/tanka/static.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" - "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/spec" "github.com/grafana/tanka/pkg/spec/v1alpha1" @@ -17,12 +16,11 @@ type StaticLoader struct { jsonnetImpl types.JsonnetImplementation } -func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "staticLoader.Load") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) +func (s StaticLoader) Name() string { + return "static" +} +func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err @@ -38,11 +36,6 @@ func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (* } func (s StaticLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "staticLoader.Peek") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - config, err := parseStaticSpec(path) if err != nil { return nil, err @@ -52,11 +45,6 @@ func (s StaticLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (* } func (s StaticLoader) List(ctx context.Context, path string, opts LoaderOpts) ([]*v1alpha1.Environment, error) { - ctx, span := tracer.Start(ctx, "staticLoader.List") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - env, err := s.Peek(ctx, path, opts) if err != nil { return nil, err @@ -66,11 +54,6 @@ func (s StaticLoader) List(ctx context.Context, path string, opts LoaderOpts) ([ } func (s *StaticLoader) Eval(ctx context.Context, path string, opts LoaderOpts) (interface{}, error) { - ctx, span := tracer.Start(ctx, "staticLoader.Eval") - defer span.End() - span.SetAttributes(telemetry.AttrPath(path)) - span.SetAttributes(OTELAttrFromLoaderOpts(&opts)...) - config, err := s.Peek(ctx, path, opts) if err != nil { return nil, err From e40cd33de4e0393137a5ce412826877080e5bc6b Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 14:12:41 +0200 Subject: [PATCH 6/7] Reduce noise further --- internal/telemetry/attributes.go | 4 ++++ pkg/jsonnet/eval.go | 12 +----------- pkg/tanka/export.go | 6 ++++++ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/telemetry/attributes.go b/internal/telemetry/attributes.go index 0fa650945..0b2d63015 100644 --- a/internal/telemetry/attributes.go +++ b/internal/telemetry/attributes.go @@ -15,6 +15,10 @@ func AttrLoader(v string) attribute.KeyValue { return attribute.String("tanka.loader", v) } +func AttrNumEnvs(v int) attribute.KeyValue { + return attribute.Int("tanka.envs.num", v) +} + func AttrEnv(v *v1alpha1.Environment) []attribute.KeyValue { return []attribute.KeyValue{ attribute.String("tanka.env.id", fmt.Sprintf("%s@%s", v.Metadata.Name, v.Spec.APIServer)), diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go index 113fd011c..577ae0da0 100644 --- a/pkg/jsonnet/eval.go +++ b/pkg/jsonnet/eval.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl" "github.com/grafana/tanka/pkg/jsonnet/implementations/types" "github.com/grafana/tanka/pkg/jsonnet/jpath" @@ -82,23 +81,14 @@ func (o Opts) Clone() Opts { // result in JSON form. It disregards opts.ImportPaths in favor of automatically // resolving these according to the specified file. func EvaluateFile(ctx context.Context, impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { - ctx, span := tracer.Start(ctx, "jsonnet.EvaluateFile") - defer span.End() - span.SetAttributes(telemetry.AttrPath(jsonnetFile)) - evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateFile(jsonnetFile) } data, err := os.ReadFile(jsonnetFile) if err != nil { - telemetry.FailSpanWithError(span, err) return "", err } - output, err := evaluateSnippet(impl, evalFunc, jsonnetFile, string(data), opts) - if err != nil { - telemetry.FailSpanWithError(span, err) - } - return output, err + return evaluateSnippet(impl, evalFunc, jsonnetFile, string(data), opts) } // Evaluate renders the given jsonnet into a string diff --git a/pkg/tanka/export.go b/pkg/tanka/export.go index 0847ce56f..67fac9fda 100644 --- a/pkg/tanka/export.go +++ b/pkg/tanka/export.go @@ -17,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/labels" + "github.com/grafana/tanka/internal/telemetry" "github.com/grafana/tanka/pkg/kubernetes/manifest" "github.com/grafana/tanka/pkg/spec/v1alpha1" ) @@ -63,6 +64,11 @@ type ExportEnvOpts struct { } func ExportEnvironments(ctx context.Context, envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error { + ctx, span := tracer.Start(ctx, "tanka.ExportEnvironments") + defer span.End() + + span.SetAttributes(telemetry.AttrNumEnvs(len(envs))) + // Keep track of which file maps to which environment fileToEnv := map[string]string{} From b21836349231f0ed54ed767125d3d6d819c8e948 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 28 Aug 2025 16:15:56 +0200 Subject: [PATCH 7/7] Improve consistency --- cmd/tk/env.go | 6 ++++++ cmd/tk/fmt.go | 2 ++ cmd/tk/init.go | 2 ++ cmd/tk/lint.go | 2 +- cmd/tk/main.go | 8 ++++++-- cmd/tk/tool.go | 2 +- cmd/tk/toolCharts.go | 36 +++++++++++++++++++++++------------ pkg/jsonnet/eval.go | 4 ++-- pkg/jsonnet/find_importers.go | 2 +- pkg/jsonnet/imports.go | 2 +- pkg/kubernetes/diff.go | 2 +- pkg/tanka/static.go | 2 +- 12 files changed, 48 insertions(+), 22 deletions(-) diff --git a/cmd/tk/env.go b/cmd/tk/env.go index bdfc97db3..43b3e1339 100644 --- a/cmd/tk/env.go +++ b/cmd/tk/env.go @@ -67,6 +67,8 @@ func envSetCmd(ctx context.Context) *cli.Command { _ = cmd.Flags().MarkHidden("name") cmd.Run = func(cmd *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envSetCmd") + defer span.End() if *name != "" { return fmt.Errorf("it looks like you attempted to rename the environment using `--name`. However, this is not possible with Tanka, because the environments name is inferred from the directories name. To rename the environment, rename its directory instead") } @@ -132,6 +134,8 @@ func envAddCmd(ctx context.Context) *cli.Command { inline := cmd.Flags().BoolP("inline", "i", false, "create an inline environment") cmd.Run = func(cmd *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envAddCmd") + defer span.End() if cmd.Flags().Changed("server-from-context") { server, err := client.IPFromContext(cfg.Spec.APIServer) if err != nil { @@ -209,6 +213,8 @@ func envRemoveCmd(ctx context.Context) *cli.Command { Short: "delete an environment", Args: generateWorkflowArgs(ctx), Run: func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "envRemoveCmd") + defer span.End() for _, arg := range args { path, err := filepath.Abs(arg) if err != nil { diff --git a/cmd/tk/fmt.go b/cmd/tk/fmt.go index 1a066b301..12bbd2de8 100644 --- a/cmd/tk/fmt.go +++ b/cmd/tk/fmt.go @@ -32,6 +32,8 @@ func fmtCmd(ctx context.Context) *cli.Command { verbose := cmd.Flags().BoolP("verbose", "v", false, "print each checked file") cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "fmtCmd") + defer span.End() if len(args) == 1 && args[0] == ArgStdin { return fmtStdin(*test) } diff --git a/cmd/tk/init.go b/cmd/tk/init.go index 0401b9f82..95b2dafa0 100644 --- a/cmd/tk/init.go +++ b/cmd/tk/init.go @@ -28,6 +28,8 @@ func initCmd(ctx context.Context) *cli.Command { inline := cmd.Flags().BoolP("inline", "i", false, "create an inline environment") cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "initCmd") + defer span.End() failed := false files, err := os.ReadDir(".") diff --git a/cmd/tk/lint.go b/cmd/tk/lint.go index 39a8595af..655b255ad 100644 --- a/cmd/tk/lint.go +++ b/cmd/tk/lint.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/tanka/pkg/jsonnet" ) -func lintCmd(ctx context.Context) *cli.Command { +func lintCmd(_ context.Context) *cli.Command { cmd := &cli.Command{ Use: "lint ", Short: "lint Jsonnet code", diff --git a/cmd/tk/main.go b/cmd/tk/main.go index 602fc7c2e..b5e745289 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -78,11 +78,15 @@ func main() { // Run! if err := rootCmd.Execute(); err != nil { - shutdownOtel(context.Background()) + if err := shutdownOtel(context.Background()); err != nil { + fmt.Fprintln(os.Stderr, "OTEL shutdown error:", err) + } fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } - shutdownOtel(context.Background()) + if err := shutdownOtel(context.Background()); err != nil { + fmt.Fprintln(os.Stderr, "OTEL shutdown error:", err) + } } func addCommandsWithLogLevelOption(rootCmd *cli.Command, cmds ...*cli.Command) { diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go index cdc0f0abc..69cf336a2 100644 --- a/cmd/tk/tool.go +++ b/cmd/tk/tool.go @@ -161,7 +161,7 @@ if the file is not a vendored (located at /vendor/) or a lib file (loca } root := cmd.Flags().String("root", ".", "root directory to search for environments") - cmd.Run = func(cctx *cli.Command, args []string) error { + cmd.Run = func(_ *cli.Command, args []string) error { ctx, span := tracer.Start(ctx, "importersCmd") defer span.End() diff --git a/cmd/tk/toolCharts.go b/cmd/tk/toolCharts.go index 7de8b3150..440beaa9b 100644 --- a/cmd/tk/toolCharts.go +++ b/cmd/tk/toolCharts.go @@ -23,18 +23,18 @@ func chartsCmd(ctx context.Context) *cli.Command { addCommandsWithLogLevelOption( cmd, - chartsInitCmd(), - chartsAddCmd(), - chartsAddRepoCmd(), - chartsVendorCmd(), - chartsConfigCmd(), - chartsVersionCheckCmd(), + chartsInitCmd(ctx), + chartsAddCmd(ctx), + chartsAddRepoCmd(ctx), + chartsVendorCmd(ctx), + chartsConfigCmd(ctx), + chartsVersionCheckCmd(ctx), ) return cmd } -func chartsVendorCmd() *cli.Command { +func chartsVendorCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "vendor", Short: "Download Charts to a local folder", @@ -43,6 +43,8 @@ func chartsVendorCmd() *cli.Command { repoConfigPath := cmd.Flags().String("repository-config", "", repoConfigFlagUsage) cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -54,7 +56,7 @@ func chartsVendorCmd() *cli.Command { return cmd } -func chartsAddCmd() *cli.Command { +func chartsAddCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add [chart@version] [...]", Short: "Adds Charts to the chartfile", @@ -62,6 +64,8 @@ func chartsAddCmd() *cli.Command { repoConfigPath := cmd.Flags().String("repository-config", "", repoConfigFlagUsage) cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -73,7 +77,7 @@ func chartsAddCmd() *cli.Command { return cmd } -func chartsAddRepoCmd() *cli.Command { +func chartsAddRepoCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "add-repo [NAME] [URL]", Short: "Adds a repository to the chartfile", @@ -81,6 +85,8 @@ func chartsAddRepoCmd() *cli.Command { } cmd.Run = func(_ *cli.Command, args []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -95,13 +101,15 @@ func chartsAddRepoCmd() *cli.Command { return cmd } -func chartsConfigCmd() *cli.Command { +func chartsConfigCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "config", Short: "Displays the current manifest", } cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err @@ -120,13 +128,15 @@ func chartsConfigCmd() *cli.Command { return cmd } -func chartsInitCmd() *cli.Command { +func chartsInitCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "init", Short: "Create a new Chartfile", } cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() wd, err := os.Getwd() if err != nil { return err @@ -148,7 +158,7 @@ func chartsInitCmd() *cli.Command { return cmd } -func chartsVersionCheckCmd() *cli.Command { +func chartsVersionCheckCmd(ctx context.Context) *cli.Command { cmd := &cli.Command{ Use: "version-check", Short: "Check required charts for updated versions", @@ -157,6 +167,8 @@ func chartsVersionCheckCmd() *cli.Command { prettyPrint := cmd.Flags().Bool("pretty-print", false, "pretty print json output with indents") cmd.Run = func(_ *cli.Command, _ []string) error { + _, span := tracer.Start(ctx, "chartsVendorCmd") + defer span.End() c, err := loadChartfile() if err != nil { return err diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go index 577ae0da0..529014800 100644 --- a/pkg/jsonnet/eval.go +++ b/pkg/jsonnet/eval.go @@ -80,7 +80,7 @@ func (o Opts) Clone() Opts { // EvaluateFile evaluates the Jsonnet code in the given file and returns the // result in JSON form. It disregards opts.ImportPaths in favor of automatically // resolving these according to the specified file. -func EvaluateFile(ctx context.Context, impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { +func EvaluateFile(_ context.Context, impl types.JsonnetImplementation, jsonnetFile string, opts Opts) (string, error) { evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateFile(jsonnetFile) } @@ -94,7 +94,7 @@ func EvaluateFile(ctx context.Context, impl types.JsonnetImplementation, jsonnet // Evaluate renders the given jsonnet into a string // If cache options are given, a hash from the data will be computed and // the resulting string will be cached for future retrieval -func Evaluate(ctx context.Context, path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { +func Evaluate(_ context.Context, path string, impl types.JsonnetImplementation, data string, opts Opts) (string, error) { evalFunc := func(evaluator types.JsonnetEvaluator) (string, error) { return evaluator.EvaluateAnonymousSnippet(data) } diff --git a/pkg/jsonnet/find_importers.go b/pkg/jsonnet/find_importers.go index 3b4b3d1d1..b682e3b1c 100644 --- a/pkg/jsonnet/find_importers.go +++ b/pkg/jsonnet/find_importers.go @@ -31,7 +31,7 @@ type cachedJsonnetFile struct { // It looks through imports transitively, so if a file is imported through a chain, it will still be reported. // If the given file is a main.jsonnet file, it will be returned as well. func FindImporterForFiles(ctx context.Context, root string, files []string) ([]string, error) { - ctx, span := tracer.Start(ctx, "jsonnet.FindImporterForFiles") + _, span := tracer.Start(ctx, "jsonnet.FindImporterForFiles") defer span.End() span.SetAttributes(attribute.StringSlice("tanka.files", files)) diff --git a/pkg/jsonnet/imports.go b/pkg/jsonnet/imports.go index 4396c98e5..c87d3df78 100644 --- a/pkg/jsonnet/imports.go +++ b/pkg/jsonnet/imports.go @@ -23,7 +23,7 @@ import ( var importsRegexp = regexp.MustCompile(`import(str)?\s+['"]([^'"%()]+)['"]`) // TransitiveImports returns all recursive imports of an environment -func TransitiveImports(ctx context.Context, dir string) ([]string, error) { +func TransitiveImports(_ context.Context, dir string) ([]string, error) { dir, err := filepath.Abs(dir) if err != nil { return nil, err diff --git a/pkg/kubernetes/diff.go b/pkg/kubernetes/diff.go index a9d8a09ca..c005b43f0 100644 --- a/pkg/kubernetes/diff.go +++ b/pkg/kubernetes/diff.go @@ -14,7 +14,7 @@ import ( // Diff takes the desired state and returns the differences from the cluster func (k *Kubernetes) Diff(ctx context.Context, state manifest.List, opts DiffOpts) (*string, error) { - ctx, span := tracer.Start(ctx, "kubernetes.Diff") + _, span := tracer.Start(ctx, "kubernetes.Diff") span.End() // prevent https://github.com/kubernetes/kubernetes/issues/89762 until fixed if k.ctl.Info().ClientVersion.Equal(semver.MustParse("1.18.0")) { diff --git a/pkg/tanka/static.go b/pkg/tanka/static.go index 8527ac78a..178959912 100644 --- a/pkg/tanka/static.go +++ b/pkg/tanka/static.go @@ -35,7 +35,7 @@ func (s StaticLoader) Load(ctx context.Context, path string, opts LoaderOpts) (* return config, nil } -func (s StaticLoader) Peek(ctx context.Context, path string, opts LoaderOpts) (*v1alpha1.Environment, error) { +func (s StaticLoader) Peek(_ context.Context, path string, _ LoaderOpts) (*v1alpha1.Environment, error) { config, err := parseStaticSpec(path) if err != nil { return nil, err