diff --git a/go.mod b/go.mod index b6d35d96b2bc..a0341f58e4bc 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib go 1.22.5 require ( + github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor v0.0.0 github.com/open-telemetry/opentelemetry-collector-contrib/connector/countconnector v0.103.0 github.com/open-telemetry/opentelemetry-collector-contrib/connector/datadogconnector v0.103.0 github.com/open-telemetry/opentelemetry-collector-contrib/connector/exceptionsconnector v0.103.0 @@ -195,10 +196,11 @@ require ( require ( bitbucket.org/atlassian/go-asap/v2 v2.8.0 // indirect + cel.dev/expr v0.16.0 // indirect cloud.google.com/go v0.114.0 // indirect cloud.google.com/go/auth v0.5.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.1.8 // indirect cloud.google.com/go/logging v1.10.0 // indirect cloud.google.com/go/longrunning v0.5.7 // indirect @@ -374,7 +376,7 @@ require ( github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/cilium/ebpf v0.11.0 // indirect github.com/cloudfoundry-incubator/uaago v0.0.0-20190307164349-8136b7bbe76e // indirect - github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect + github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 // indirect github.com/containerd/cgroups/v3 v3.0.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/containerd/errdefs v0.1.0 // indirect @@ -385,6 +387,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/devigned/tab v0.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -406,8 +409,8 @@ require ( github.com/elastic/go-sysinfo v1.7.1 // indirect github.com/elastic/go-windows v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/envoyproxy/go-control-plane v0.12.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/envoyproxy/go-control-plane v0.13.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/euank/go-kmsg-parser v2.0.0+incompatible // indirect github.com/expr-lang/expr v1.16.9 // indirect github.com/facebook/time v0.0.0-20240510113249-fa89cc575891 // indirect @@ -430,7 +433,7 @@ require ( github.com/go-openapi/swag v0.22.9 // indirect github.com/go-resty/resty/v2 v2.12.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-zookeeper/zk v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -626,6 +629,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect @@ -723,7 +727,7 @@ require ( go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0 // indirect go.opentelemetry.io/collector/consumer v0.103.0 // indirect go.opentelemetry.io/collector/extension/auth v0.103.0 // indirect - go.opentelemetry.io/collector/featuregate v1.10.0 // indirect + go.opentelemetry.io/collector/featuregate v1.17.0 // indirect go.opentelemetry.io/collector/filter v0.103.0 // indirect go.opentelemetry.io/collector/pdata v1.10.0 // indirect go.opentelemetry.io/collector/semconv v0.103.0 // indirect @@ -733,7 +737,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.27.0 // indirect go.opentelemetry.io/contrib/zpages v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect go.opentelemetry.io/otel/bridge/opencensus v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 // indirect @@ -743,35 +747,35 @@ require ( go.opentelemetry.io/otel/exporters/prometheus v0.49.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/sdk v1.27.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/fx v1.18.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.15.0 // indirect google.golang.org/api v0.183.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.64.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect @@ -783,10 +787,10 @@ require ( k8s.io/api v0.30.0 // indirect k8s.io/apimachinery v0.30.0 // indirect k8s.io/client-go v0.30.0 // indirect - k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/kubelet v0.30.0 // indirect - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/controller-runtime v0.17.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect @@ -1249,3 +1253,5 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdata replace github.com/amazon-contributing/opentelemetry-collector-contrib/extension/awsmiddleware => ./extension/awsmiddleware replace github.com/amazon-contributing/opentelemetry-collector-contrib/override/aws => ./override/aws + +replace github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor => ./processor/awsapplicationsignalsprocessor diff --git a/go.sum b/go.sum index 9bb707f1ffec..110aabccc88d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ bitbucket.org/atlassian/go-asap/v2 v2.8.0 h1:JL4jktrZT3H8GHkkeJ1RCdhJ5YcOyVccA/2TJ4ae3I0= bitbucket.org/atlassian/go-asap/v2 v2.8.0/go.mod h1:cuRgWb7eeGtsocKmqY2kPlMMlwpkeNh+QOIkjAMlUBc= +cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -184,8 +186,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -1124,8 +1126,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -1170,6 +1172,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= @@ -1241,14 +1245,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= -github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= -github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/euank/go-kmsg-parser v2.0.0+incompatible h1:cHD53+PLQuuQyLZeriD1V/esuG4MuU0Pjs5y6iknohY= github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1355,8 +1359,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -2008,6 +2012,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -2371,8 +2377,8 @@ go.opentelemetry.io/collector/extension/ballastextension v0.103.0 h1:U8f6b6xnkD0 go.opentelemetry.io/collector/extension/ballastextension v0.103.0/go.mod h1:/B3xE2YhUgGXSsFH8Qg0kq4WpfkuTyWDJGNyfRgMax8= go.opentelemetry.io/collector/extension/zpagesextension v0.103.0 h1:jgSEQY++zOI6hFQygwuvS6ulJ/Yu4xXgUg+Ijoxx51I= go.opentelemetry.io/collector/extension/zpagesextension v0.103.0/go.mod h1:2OUi0Hp+3zPUJmi7goJ6d1/kGgFAw3SDESRX7xQ0QHE= -go.opentelemetry.io/collector/featuregate v1.10.0 h1:krSqokHTp7JthgmtewysqHuOAkcuuZl7G2n91s7HygE= -go.opentelemetry.io/collector/featuregate v1.10.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= +go.opentelemetry.io/collector/featuregate v1.17.0 h1:vpfXyWe7DFqCsDArsR9rAKKtVpt72PKjzjeqPegViws= +go.opentelemetry.io/collector/featuregate v1.17.0/go.mod h1:47xrISO71vJ83LSMm8+yIDsUbKktUp48Ovt7RR6VbRs= go.opentelemetry.io/collector/filter v0.103.0 h1:5pyfcT8ZCD5xKWI4pgnMdCI8a/YtDp0zzWXn7gcqp0A= go.opentelemetry.io/collector/filter v0.103.0/go.mod h1:SvQIh3hd3b393qVHWu2GXGt8tDacFZblYhg6vVqrRs8= go.opentelemetry.io/collector/otelcol v0.103.0 h1:Skqnc2mxDdk3eiYioUuG7ST6ur5k83SOv7mIBt60fBw= @@ -2405,8 +2411,8 @@ go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= go.opentelemetry.io/contrib/zpages v0.52.0 h1:MPgkMy0Cp3O5EdfVXP0ss3ujhEibysTM4eszx7E7d+E= go.opentelemetry.io/contrib/zpages v0.52.0/go.mod h1:fqG5AFdoYru3A3DnhibVuaaEfQV2WKxE7fYE1jgDRwk= -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 v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/bridge/opencensus v1.27.0 h1:ao9aGGHd+G4YfjBpGs6vbkvt5hoC67STlJA9fCnOAcs= go.opentelemetry.io/otel/bridge/opencensus v1.27.0/go.mod h1:uRvWtAAXzyVOST0WMPX5JHGBaAvBws+2F8PcC5gMnTk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= @@ -2425,14 +2431,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= -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/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -2487,8 +2493,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2632,8 +2638,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2664,8 +2670,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2682,8 +2688,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2804,8 +2810,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2821,8 +2827,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2841,8 +2847,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -3153,10 +3159,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -3200,8 +3206,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -3221,8 +3227,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -3295,16 +3301,16 @@ k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAE k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kubelet v0.30.0 h1:/pqHVR2Rn8ExCpn211wL3pMtqRFpcBcJPl4+1INbIMk= k8s.io/kubelet v0.30.0/go.mod h1:WukdKqbQxnj+csn3K8XOKeX7Sh60J/da25IILjvvB5s= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= diff --git a/internal/components/components.go b/internal/components/components.go index 47809d68925e..47d7fa5a7d95 100644 --- a/internal/components/components.go +++ b/internal/components/components.go @@ -85,6 +85,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage/dbstorage" "github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage/filestorage" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/cumulativetodeltaprocessor" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatorateprocessor" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor" @@ -361,6 +362,7 @@ func Components() (otelcol.Factories, error) { processors := []processor.Factory{ attributesprocessor.NewFactory(), + awsapplicationsignalsprocessor.NewFactory(), batchprocessor.NewFactory(), filterprocessor.NewFactory(), groupbyattrsprocessor.NewFactory(), diff --git a/processor/awsapplicationsignalsprocessor/Makefile b/processor/awsapplicationsignalsprocessor/Makefile new file mode 100644 index 000000000000..ded7a36092dc --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/processor/awsapplicationsignalsprocessor/README.md b/processor/awsapplicationsignalsprocessor/README.md new file mode 100644 index 000000000000..36893c2528d2 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/README.md @@ -0,0 +1,130 @@ +# AWS AppSignals Processor for Amazon Cloudwatch Agent + +The AWS AppSignals processor is used to reduce the cardinality of telemetry metrics and traces before exporting them to CloudWatch Logs via [EMF](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/awsemfexporter) and [X-Ray](github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter) respectively. +It reduces the cardinality of metrics/traces via 3 types of actions, `keep`, `drop` and `replace`, which are configured by users. CloudWatch Agent(CWA) customers will configure these rules with their CWA configurations. + +Note: Traces support only `replace` actions and are implicitly pulled from the logs section of the CWA configuration + +| Status | | +| ------------------------ |---------------------------| +| Stability | [beta] | +| Supported pipeline types | metrics, traces | +| Distributions | [amazon-cloudwatch-agent] | + +## Exporter Configuration + +The following exporter configuration parameters are supported. + +| Name | Description | Default | +|:---------------------------------------------|:------------------------------------------------------------------------------------------------------------------|---------| +| `resolvers` | Platform processor is being configured for. Currently supports EKS. EC2 platform will be supported in the future. | [eks] | +| `rules` | Custom configuration rules used for filtering metrics/traces. Can be of type `drop`, `keep`, `replace`. | [] | + +### rules +The rules section defines the rules (filters) to be applied + +| Name | Description | Default | +|:---------------|:-------------------------------------------------------------------------------------------------------------------------| --- | +| `selectors` | List of metrics/traces dimension matchers. | [] | +| `action` | Action being applied for the specified selector. `keep`, `drop`, `replace` | "" | +| `rule_name` | (Optional) Name of rule. | [] | +| `replacements` | (Optional) List of metrics/traces replacements to be executed. Based on specified selectors. requires `action = replace` | [] | + +#### selectors +A selectors section defines a matching against the dimensions of incoming metrics/traces. + +| Name | Description | Default | +|:------------|:--------------------------------------------------------------| ------ | +| `dimension` | Dimension of metrics/traces | "" | +| `match` | glob used for matching values of dimensions | "" | + +### replacements +A replacements section defines a matching against the dimensions of incoming metrics/traces for which value replacements will be done. action must be `replace` + +| Name | Description | Default | +|:-------------------|:----------------------------------------------| ------ | +| `target_dimension` | Dimension to replace | "" | +| `value` | Value to replace current dimension value with | "" | + + +## AWS AppSignals Processor Configuration Example + +```yaml +awsapplicationsignals: + resolvers: ["eks"] + rules: + - selectors: + - dimension: Operation + match: "POST *" + - dimension: RemoteService + match: "*" + action: keep + rule_name: "keep01" + - selectors: + - dimension: Operation + match: "GET *" + - dimension: RemoteService + match: "*" + action: keep + rule_name: "keep02" + - selectors: + - dimension: Operation + match: "POST *" + action: drop + rule_name: "drop01" + - selectors: + - dimension: Operation + match: "*" + replacements: + - target_dimension: RemoteOperation + value: "This is a test string" + action: replace + rule_name: "replace01" +``` + +## Amazon CloudWatch Agent Configuration Example + +```json +{ + "agent": { + "region": "us-west-2", + "debug": true + }, + "traces": { + "traces_collected": { + "app_signals": {} + } + }, + "logs": { + "metrics_collected": { + "app_signals": { + "rules": [ + { + "selectors": [ + { + "dimension": "Service", + "match": "pet-clinic-frontend" + }, + { + "dimension": "RemoteService", + "match": "customers-service" + } + ], + "action": "keep", + "rule_name": "keep01" + }, + { + "selectors": [ + { + "dimension": "Operation", + "match": "GET *" + } + ], + "action": "drop", + "rule_name": "drop01" + } + } + } + } + } +``` \ No newline at end of file diff --git a/processor/awsapplicationsignalsprocessor/common/types.go b/processor/awsapplicationsignalsprocessor/common/types.go new file mode 100644 index 000000000000..941d559a9353 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/common/types.go @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package common + +// Metric attribute used as CloudWatch metric dimension. +const ( + CWMetricAttributeLocalService = "Service" + CWMetricAttributeLocalOperation = "Operation" + CWMetricAttributeEnvironment = "Environment" + CWMetricAttributeRemoteService = "RemoteService" + CWMetricAttributeRemoteEnvironment = "RemoteEnvironment" + CWMetricAttributeRemoteOperation = "RemoteOperation" + CWMetricAttributeRemoteResourceIdentifier = "RemoteResourceIdentifier" + CWMetricAttributeRemoteResourceType = "RemoteResourceType" +) + +// Platform attribute used as CloudWatch EMF log field and X-Ray trace annotation. +const ( + AttributePlatformType = "PlatformType" + AttributeEKSClusterName = "EKS.Cluster" + AttributeK8SClusterName = "K8s.Cluster" + AttributeK8SNamespace = "K8s.Namespace" + AttributeK8SWorkload = "K8s.Workload" + AttributeK8SPod = "K8s.Pod" + AttributeEC2AutoScalingGroup = "EC2.AutoScalingGroup" + AttributeEC2InstanceId = "EC2.InstanceId" + AttributeHost = "Host" +) + +// Platform attribute used as CloudWatch EMF log field. +const ( + MetricAttributeECSCluster = "ECS.Cluster" + MetricAttributeECSTaskId = "ECS.TaskId" + MetricAttributeECSTaskDefinitionFamily = "ECS.TaskDefinitionFamily" + MetricAttributeECSTaskDefinitionRevision = "ECS.TaskDefinitionRevision" +) + +// Telemetry attributes used as CloudWatch EMF log fields. +const ( + MetricAttributeTelemetrySDK = "Telemetry.SDK" + MetricAttributeTelemetryAgent = "Telemetry.Agent" + MetricAttributeTelemetrySource = "Telemetry.Source" +) + +// Resource attributes used as CloudWatch EMF log fields. +const ( + MetricAttributeRemoteDbUser = "RemoteDbUser" + MetricAttributeRemoteResourceCfnPrimaryIdentifier = "RemoteResourceCfnPrimaryIdentifier" +) + +const ( + AttributeTmpReserved = "aws.tmp.reserved" +) + +var CWMetricAttributes = []string{ + CWMetricAttributeLocalService, + CWMetricAttributeLocalOperation, + CWMetricAttributeEnvironment, + CWMetricAttributeRemoteService, + CWMetricAttributeRemoteEnvironment, + CWMetricAttributeRemoteOperation, + CWMetricAttributeRemoteResourceIdentifier, + CWMetricAttributeRemoteResourceType, +} diff --git a/processor/awsapplicationsignalsprocessor/config/config.go b/processor/awsapplicationsignalsprocessor/config/config.go new file mode 100644 index 000000000000..55091487c371 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/config/config.go @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "context" + "errors" + "time" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/rules" +) + +type Config struct { + Resolvers []Resolver `mapstructure:"resolvers"` + Rules []rules.Rule `mapstructure:"rules"` + Limiter *LimiterConfig `mapstructure:"limiter"` +} + +type LimiterConfig struct { + Threshold int `mapstructure:"drop_threshold"` + Disabled bool `mapstructure:"disabled"` + LogDroppedMetrics bool `mapstructure:"log_dropped_metrics"` + RotationInterval time.Duration `mapstructure:"rotation_interval"` + GarbageCollectionInterval time.Duration `mapstructure:"garbage_collection_interval"` + ParentContext context.Context `mapstructure:"-"` +} + +const ( + DefaultThreshold = 500 + DefaultRotationInterval = 1 * time.Hour + DefaultGCInterval = 10 * time.Minute +) + +func NewDefaultLimiterConfig() *LimiterConfig { + return &LimiterConfig{ + Threshold: DefaultThreshold, + Disabled: false, + LogDroppedMetrics: false, + RotationInterval: DefaultRotationInterval, + GarbageCollectionInterval: DefaultGCInterval, + } +} + +func (lc *LimiterConfig) Validate() { + if lc.GarbageCollectionInterval == 0 { + lc.GarbageCollectionInterval = DefaultGCInterval + } +} + +func (cfg *Config) Validate() error { + if len(cfg.Resolvers) == 0 { + return errors.New("resolvers must not be empty") + } + for _, resolver := range cfg.Resolvers { + switch resolver.Platform { + case PlatformEKS: + if resolver.Name == "" { + return errors.New("name must not be empty for eks resolver") + } + case PlatformK8s: + if resolver.Name == "" { + return errors.New("name must not be empty for k8s resolver") + } + case PlatformEC2, PlatformECS, PlatformGeneric: + default: + return errors.New("unknown resolver") + } + } + + if cfg.Limiter != nil { + cfg.Limiter.Validate() + } + return nil +} diff --git a/processor/awsapplicationsignalsprocessor/config/config_test.go b/processor/awsapplicationsignalsprocessor/config/config_test.go new file mode 100644 index 000000000000..75762e214582 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/config/config_test.go @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidatePassed(t *testing.T) { + tests := []struct { + name string + resolver Resolver + }{ + { + "testEKS", + NewEKSResolver("test"), + }, + { + "testK8S", + NewK8sResolver("test"), + }, + { + "testEC2", + NewEC2Resolver("test"), + }, + { + "testECS", + NewECSResolver("test"), + }, + { + "testGeneric", + NewGenericResolver("test"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := Config{ + Resolvers: []Resolver{tt.resolver}, + Rules: nil, + } + assert.Nil(t, config.Validate()) + + }) + } +} + +func TestValidateFailedOnEmptyResolver(t *testing.T) { + config := Config{ + Resolvers: []Resolver{}, + Rules: nil, + } + assert.NotNil(t, config.Validate()) +} + +func TestValidateFailedOnEmptyResolverName(t *testing.T) { + tests := []struct { + name string + resolver Resolver + }{ + { + "testEKS", + NewEKSResolver(""), + }, + { + "testK8S", + NewK8sResolver(""), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := Config{ + Resolvers: []Resolver{tt.resolver}, + Rules: nil, + } + assert.NotNil(t, config.Validate()) + + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/config/resolvers.go b/processor/awsapplicationsignalsprocessor/config/resolvers.go new file mode 100644 index 000000000000..b61d76a7d6ba --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/config/resolvers.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package config + +const ( + // PlatformGeneric Platforms other than Amazon EKS + PlatformGeneric = "generic" + // PlatformEKS Amazon EKS platform + PlatformEKS = "eks" + // PlatformK8s Native Kubernetes + PlatformK8s = "k8s" + // PlatformEC2 Amazon EC2 platform + PlatformEC2 = "ec2" + // PlatformECS Amazon ECS + PlatformECS = "ecs" +) + +type Resolver struct { + Name string `mapstructure:"name"` + Platform string `mapstructure:"platform"` +} + +func NewEKSResolver(name string) Resolver { + return Resolver{ + Name: name, + Platform: PlatformEKS, + } +} + +func NewK8sResolver(name string) Resolver { + return Resolver{ + Name: name, + Platform: PlatformK8s, + } +} + +func NewEC2Resolver(name string) Resolver { + return Resolver{ + Name: name, + Platform: PlatformEC2, + } +} + +func NewECSResolver(name string) Resolver { + return Resolver{ + Name: name, + Platform: PlatformECS, + } +} + +func NewGenericResolver(name string) Resolver { + return Resolver{ + Name: name, + Platform: PlatformGeneric, + } +} diff --git a/processor/awsapplicationsignalsprocessor/config/resolvers_test.go b/processor/awsapplicationsignalsprocessor/config/resolvers_test.go new file mode 100644 index 000000000000..a68ab706f438 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/config/resolvers_test.go @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewEKSResolver(t *testing.T) { + resolver := NewEKSResolver("test") + assert.Equal(t, "eks", resolver.Platform) +} + +func TestK8sResolver(t *testing.T) { + resolver := NewK8sResolver("test") + assert.Equal(t, "k8s", resolver.Platform) +} + +func TestEC2Resolver(t *testing.T) { + resolver := NewEC2Resolver("test") + assert.Equal(t, "ec2", resolver.Platform) +} + +func TestNewGenericResolver(t *testing.T) { + resolver := NewGenericResolver("") + assert.Equal(t, "generic", resolver.Platform) +} diff --git a/processor/awsapplicationsignalsprocessor/factory.go b/processor/awsapplicationsignalsprocessor/factory.go new file mode 100644 index 000000000000..5cef58512c56 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/factory.go @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package awsapplicationsignalsprocessor + +import ( + "context" + "errors" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/processor" + "go.opentelemetry.io/collector/processor/processorhelper" + + appsignalsconfig "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" +) + +const ( + // The stability level of the processor. + stability = component.StabilityLevelBeta +) + +var ( + // The value of "type" key in configuration. + typeStr, _ = component.NewType("awsapplicationsignals") + consumerCapabilities = consumer.Capabilities{MutatesData: true} +) + +// NewFactory returns a new factory for the aws attributes processor. +func NewFactory() processor.Factory { + return processor.NewFactory( + typeStr, + createDefaultConfig, + processor.WithTraces(createTracesProcessor, stability), + processor.WithMetrics(createMetricsProcessor, stability), + ) +} + +func createDefaultConfig() component.Config { + return &appsignalsconfig.Config{ + Resolvers: []appsignalsconfig.Resolver{}, + } +} + +func createTracesProcessor( + ctx context.Context, + set processor.CreateSettings, + cfg component.Config, + next consumer.Traces, +) (processor.Traces, error) { + ap, err := createProcessor(set, cfg) + if err != nil { + return nil, err + } + + return processorhelper.NewTracesProcessor( + ctx, + set, + cfg, + next, + ap.processTraces, + processorhelper.WithCapabilities(consumerCapabilities), + processorhelper.WithStart(ap.StartTraces), + processorhelper.WithShutdown(ap.Shutdown)) +} + +func createMetricsProcessor( + ctx context.Context, + set processor.CreateSettings, + cfg component.Config, + nextMetricsConsumer consumer.Metrics, +) (processor.Metrics, error) { + ap, err := createProcessor(set, cfg) + if err != nil { + return nil, err + } + + return processorhelper.NewMetricsProcessor( + ctx, + set, + cfg, + nextMetricsConsumer, + ap.processMetrics, + processorhelper.WithCapabilities(consumerCapabilities), + processorhelper.WithStart(ap.StartMetrics), + processorhelper.WithShutdown(ap.Shutdown)) +} + +func createProcessor( + params processor.CreateSettings, + cfg component.Config, +) (*awsapplicationsignalsprocessor, error) { + pCfg, ok := cfg.(*appsignalsconfig.Config) + if !ok { + return nil, errors.New("could not initialize awsapplicationsignalsprocessor") + } + ap := &awsapplicationsignalsprocessor{logger: params.Logger, config: pCfg} + + return ap, nil +} diff --git a/processor/awsapplicationsignalsprocessor/factory_test.go b/processor/awsapplicationsignalsprocessor/factory_test.go new file mode 100644 index 000000000000..0d12b04cf1af --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/factory_test.go @@ -0,0 +1,156 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package awsapplicationsignalsprocessor + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/rules" +) + +var expectedRules = []rules.Rule{ + { + Selectors: []rules.Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Action: "keep", + RuleName: "keep01", + }, + { + Selectors: []rules.Selector{ + { + Dimension: "RemoteService", + Match: "UnknownRemoteService", + }, + { + Dimension: "RemoteOperation", + Match: "GetShardIterator", + }, + }, + Action: "drop", + }, + { + Selectors: []rules.Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Replacements: []rules.Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "ListPetsByCustomer", + }, + { + TargetDimension: "ResourceTarget", + Value: " ", + }, + }, + Action: "replace", + }, +} + +func TestLoadEKSConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expected component.Config + errorMessage string + }{ + { + name: "awsapplicationsignals", + expected: &config.Config{ + Resolvers: []config.Resolver{config.NewEKSResolver("test")}, + Rules: expectedRules, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newType, _ := component.NewType(tt.name) + id := component.NewIDWithName(newType, "") + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config_eks.yaml")) + require.NoError(t, err) + + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*config.Config) + + sub, err := cm.Sub(id.String()) + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(cfg)) + + if tt.expected == nil { + assert.EqualError(t, component.ValidateConfig(cfg), tt.errorMessage) + return + } + assert.NoError(t, component.ValidateConfig(cfg)) + assert.Equal(t, tt.expected, cfg) + + validateErr := cfg.Validate() + assert.Nil(t, validateErr, validateErr) + }) + } +} + +func TestLoadGenericConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expected component.Config + errorMessage string + }{ + { + name: "awsapplicationsignals", + expected: &config.Config{ + Resolvers: []config.Resolver{config.NewGenericResolver("")}, + Rules: expectedRules, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newType, _ := component.NewType(tt.name) + id := component.NewIDWithName(newType, "") + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config_generic.yaml")) + require.NoError(t, err) + + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*config.Config) + + sub, err := cm.Sub(id.String()) + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(cfg)) + + if tt.expected == nil { + assert.EqualError(t, component.ValidateConfig(cfg), tt.errorMessage) + return + } + assert.NoError(t, component.ValidateConfig(cfg)) + assert.Equal(t, tt.expected, cfg) + + validateErr := cfg.Validate() + assert.Nil(t, validateErr, validateErr) + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/go.mod b/processor/awsapplicationsignalsprocessor/go.mod new file mode 100644 index 000000000000..4b9ab11a4482 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/go.mod @@ -0,0 +1,78 @@ +module github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor + +go 1.22.5 + +require ( + github.com/deckarep/golang-set/v2 v2.6.0 + github.com/gobwas/glob v0.2.3 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/component v0.103.0 + go.opentelemetry.io/collector/confmap v0.103.0 + go.opentelemetry.io/collector/consumer v0.103.0 + go.opentelemetry.io/collector/pdata v1.10.0 + go.opentelemetry.io/collector/processor v0.103.0 + go.opentelemetry.io/collector/semconv v0.103.0 + go.uber.org/zap v1.27.0 + golang.org/x/text v0.19.0 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + go.opentelemetry.io/collector v0.103.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.103.0 // indirect + go.opentelemetry.io/collector/featuregate v1.17.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/processor/awsapplicationsignalsprocessor/go.sum b/processor/awsapplicationsignalsprocessor/go.sum new file mode 100644 index 000000000000..4867b9206ab9 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/go.sum @@ -0,0 +1,230 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/kr/pretty v0.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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= +github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= +github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector v0.103.0 h1:mssWo1y31p1F/SRsSBnVUX6YocgawCqM1blpE+hkWog= +go.opentelemetry.io/collector v0.103.0/go.mod h1:mgqdTFB7QCYiOeEdJSSEktovPqy+2fw4oTKJzyeSB0U= +go.opentelemetry.io/collector/component v0.103.0 h1:j52YAsp8EmqYUotVUwhovkqFZGuxArEkk65V4TI46NE= +go.opentelemetry.io/collector/component v0.103.0/go.mod h1:jKs19tGtCO8Hr5/YM0F+PoFcl8SVe/p4Ge30R6srkbc= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0 h1:KLbhkFqdw9D31t0IhJ/rnhMRvz/s14eie0fKfm5xWns= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= +go.opentelemetry.io/collector/confmap v0.103.0 h1:qKKZyWzropSKfgtGv12JzADOXNgThqH1Vx6qzblBE24= +go.opentelemetry.io/collector/confmap v0.103.0/go.mod h1:TlOmqe/Km3K6WgxyhEAdCb/V1Yp6eSU76fCoiluEa88= +go.opentelemetry.io/collector/consumer v0.103.0 h1:L/7SA/U2ua5L4yTLChnI9I+IFGKYU5ufNQ76QKYcPYs= +go.opentelemetry.io/collector/consumer v0.103.0/go.mod h1:7jdYb9kSSOsu2R618VRX0VJ+Jt3OrDvvUsDToHTEOLI= +go.opentelemetry.io/collector/featuregate v1.17.0 h1:vpfXyWe7DFqCsDArsR9rAKKtVpt72PKjzjeqPegViws= +go.opentelemetry.io/collector/featuregate v1.17.0/go.mod h1:47xrISO71vJ83LSMm8+yIDsUbKktUp48Ovt7RR6VbRs= +go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE= +go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= +go.opentelemetry.io/collector/pdata/testdata v0.103.0 h1:iI6NOE0L2je/bxlWzAWHQ/yCtnGupgv42Hl9Al1q/g4= +go.opentelemetry.io/collector/pdata/testdata v0.103.0/go.mod h1:tLzRhb/h37/9wFRQVr+CxjKi5qmhSRpCAiOlhwRkeEk= +go.opentelemetry.io/collector/processor v0.103.0 h1:YZ+LRuHKtOam7SCeLkJAP6bS1d6XxeYP22OyMN3VP0s= +go.opentelemetry.io/collector/processor v0.103.0/go.mod h1:/mxyh0NpJgpZycm7iHDpM7i5PdtWvKKdCZf0cyADJfU= +go.opentelemetry.io/collector/semconv v0.103.0 h1:5tlVoZlo9USHAU2Bz4YrEste0Vm5AMufXkYJhAVve1Q= +go.opentelemetry.io/collector/semconv v0.103.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= +go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/processor/awsapplicationsignalsprocessor/internal/attributes/attributes.go b/processor/awsapplicationsignalsprocessor/internal/attributes/attributes.go new file mode 100644 index 000000000000..4bc191eba71e --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/attributes/attributes.go @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package attributes + +const ( + // aws attributes + AWSSpanKind = "aws.span.kind" + AWSLocalService = "aws.local.service" + AWSLocalEnvironment = "aws.local.environment" + AWSLocalOperation = "aws.local.operation" + AWSRemoteService = "aws.remote.service" + AWSRemoteOperation = "aws.remote.operation" + AWSRemoteEnvironment = "aws.remote.environment" + AWSRemoteTarget = "aws.remote.target" + AWSRemoteResourceIdentifier = "aws.remote.resource.identifier" + AWSRemoteResourceType = "aws.remote.resource.type" + AWSHostedInEnvironment = "aws.hostedin.environment" + AWSRemoteDbUser = "aws.remote.db.user" + AWSRemoteResourceCfnPrimaryIdentifier = "aws.remote.resource.cfn.primary.identifier" + + AWSECSClusterName = "aws.ecs.cluster.name" + AWSECSTaskID = "aws.ecs.task.id" + + // resource detection processor attributes + ResourceDetectionHostId = "host.id" + ResourceDetectionHostName = "host.name" + ResourceDetectionASG = "ec2.tag.aws:autoscaling:groupName" +) diff --git a/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch.go b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch.go new file mode 100644 index 000000000000..99a1eabae596 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch.go @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cardinalitycontrol + +import ( + "hash/adler32" + "hash/crc32" + "hash/fnv" +) + +type CountMinSketchHashFunc func(hashKey string) int64 + +type CountMinSketchEntry interface { + HashKey() string + Frequency() int +} + +type CountMinSketch struct { + depth int + maxDepth int + width int + matrix [][]int + hashFuncs []CountMinSketchHashFunc +} + +func (cms *CountMinSketch) Insert(obj CountMinSketchEntry) { + for i := 0; i < cms.depth; i++ { + hashFunc := cms.hashFuncs[i] + hashValue := hashFunc(obj.HashKey()) + pos := int(hashValue % int64(cms.width)) + + cms.matrix[i][pos] += obj.Frequency() + } +} + +func NewCountMinSketch(depth, width int, hashFuncs ...CountMinSketchHashFunc) *CountMinSketch { + matrix := make([][]int, depth) + for i := range matrix { + matrix[i] = make([]int, width) + } + cms := &CountMinSketch{ + depth: 0, + maxDepth: depth, + width: width, + matrix: matrix, + } + if hashFuncs != nil { + cms.RegisterHashFunc(hashFuncs...) + } else { + RegisterDefaultHashFuncs(cms) + } + return cms +} + +func RegisterDefaultHashFuncs(cms *CountMinSketch) { + hashFunc1 := func(hashKey string) int64 { + h := fnv.New32a() + h.Write([]byte(hashKey)) + return int64(h.Sum32()) + } + hashFunc2 := func(hashKey string) int64 { + hash := crc32.ChecksumIEEE([]byte(hashKey)) + return int64(hash) + } + hashFunc3 := func(hashKey string) int64 { + hash := adler32.Checksum([]byte(hashKey)) + return int64(hash) + } + cms.RegisterHashFunc(hashFunc1, hashFunc2, hashFunc3) +} + +func (cms *CountMinSketch) RegisterHashFunc(hashFuncs ...CountMinSketchHashFunc) { + if cms.hashFuncs == nil { + cms.hashFuncs = hashFuncs + } else { + cms.hashFuncs = append(cms.hashFuncs, hashFuncs...) + } + if cms.maxDepth < len(cms.hashFuncs) { + cms.depth = cms.maxDepth + } else { + cms.depth = len(cms.hashFuncs) + } +} + +func (cms *CountMinSketch) Get(obj CountMinSketchEntry) int { + minCount := int(^uint(0) >> 1) // Initialize with the maximum possible integer value + for i := 0; i < cms.depth; i++ { + hashFunc := cms.hashFuncs[i] + hashValue := hashFunc(obj.HashKey()) + pos := int(hashValue % int64(cms.width)) + + if cms.matrix[i][pos] < minCount { + minCount = cms.matrix[i][pos] + } + } + return minCount +} diff --git a/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch_test.go b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch_test.go new file mode 100644 index 000000000000..19cd7bcc2165 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/count_min_sketch_test.go @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cardinalitycontrol + +import ( + "math/rand" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +var metricNames = []string{"latency", "error", "fault"} + +func TestUpdateFrequency(t *testing.T) { + cms := NewCountMinSketch(3, 10) + for i := 0; i < 10; i++ { + md := MetricData{ + hashKey: "xxx", + name: "latency", + service: "app1", + frequency: 1, + } + cms.Insert(md) + val := cms.Get(md) + assert.Equal(t, 1+i, val) + } +} + +var testCases = []int{50, 100, 200, 500, 1000, 2000} + +func TestWriteMultipleEntries(t *testing.T) { + cms := NewCountMinSketch(3, 5000) + + maxCollisionRate := 0 + for _, dataCount := range testCases { + metricDataArray := make([]*MetricData, dataCount) + for i := 0; i < dataCount; i++ { + labels := map[string]string{ + "operation": "/api/customers/" + strconv.Itoa(rand.Int()), + } + for _, metricName := range metricNames { + freq := rand.Intn(5000) + md := MetricData{ + hashKey: sortAndConcatLabels(labels), + name: metricName, + service: "app", + frequency: freq, + } + cms.Insert(md) + if metricDataArray[i] == nil { + metricDataArray[i] = &md + } else { + metricDataArray[i].frequency = metricDataArray[i].frequency + freq + } + + } + } + + err := 0 + for _, data := range metricDataArray { + val := cms.Get(data) + if data.frequency != val { + err += 1 + } + } + collisionRate := err * 100 / len(metricDataArray) + if maxCollisionRate < collisionRate { + maxCollisionRate = collisionRate + } + t.Logf("When the item count is %d with even distribution, the collision rate is %d.\n", dataCount, collisionRate) + } + + // revisit the count min sketch setting if the assertion fails. + assert.True(t, maxCollisionRate < 30) +} + +func TestAdjustUnsupportedDepth(t *testing.T) { + cms := NewCountMinSketch(5, 10) + assert.Equal(t, 3, cms.depth) + for i := 0; i < 2; i++ { + cms.RegisterHashFunc(func(hashKey string) int64 { + return int64(0) + }) + } + assert.Equal(t, 5, cms.depth) + for i := 0; i < 2; i++ { + cms.RegisterHashFunc(func(hashKey string) int64 { + return int64(0) + }) + } + assert.Equal(t, 5, cms.depth) +} diff --git a/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter.go b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter.go new file mode 100644 index 000000000000..d22001e90c8d --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter.go @@ -0,0 +1,418 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cardinalitycontrol + +import ( + "context" + "fmt" + "sort" + "sync" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.uber.org/zap" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" +) + +const ( + UnprocessedServiceOperationValue = "AllOtherOperations" + UnprocessedRemoteServiceOperationValue = "AllOtherRemoteOperations" +) + +const ( + defaultCMSDepth = 3 + defaultCMSWidth = 5000 +) + +var awsDeclaredMetricAttributes = []string{ + common.AttributeEKSClusterName, + common.AttributeK8SClusterName, + common.AttributeK8SNamespace, + common.CWMetricAttributeEnvironment, + common.CWMetricAttributeLocalService, + common.CWMetricAttributeLocalOperation, + common.CWMetricAttributeRemoteService, + common.CWMetricAttributeRemoteOperation, + common.CWMetricAttributeRemoteResourceIdentifier, + common.CWMetricAttributeRemoteEnvironment, +} + +type Limiter interface { + Admit(name string, attributes, resourceAttributes pcommon.Map) (bool, error) +} + +type MetricsLimiter struct { + DropThreshold int + LogDroppedMetrics bool + RotationInterval time.Duration + + logger *zap.Logger + ctx context.Context + mapLock sync.RWMutex + services map[string]*service +} + +func NewMetricsLimiter(config *config.LimiterConfig, logger *zap.Logger) Limiter { + logger.Info("creating metrics limiter with config", zap.Any("config", config)) + + ctx := config.ParentContext + if ctx == nil { + ctx = context.TODO() + } + + limiter := &MetricsLimiter{ + DropThreshold: config.Threshold, + LogDroppedMetrics: config.LogDroppedMetrics, + RotationInterval: config.RotationInterval, + + logger: logger, + ctx: ctx, + services: map[string]*service{}, + } + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + limiter.removeStaleServices() + time.Sleep(config.GarbageCollectionInterval) + } + } + }() + + logger.Info("metrics limiter created.") + + return limiter +} + +func (m *MetricsLimiter) Admit(metricName string, attributes, resourceAttributes pcommon.Map) (bool, error) { + labels, serviceName, found := m.filterAWSDeclaredAttributes(attributes, resourceAttributes) + if !found { + return true, nil + } + admitted := true + + m.mapLock.RLock() + svc := m.services[serviceName] + m.mapLock.RUnlock() + if svc == nil { + m.mapLock.Lock() + svc = m.services[serviceName] + if svc == nil { + svc = newService(serviceName, m.DropThreshold, m.RotationInterval, m.ctx, m.logger) + m.services[serviceName] = svc + } + m.mapLock.Unlock() + } + + metricData := newMetricData(serviceName, metricName, labels) + + reserved, _ := attributes.Get(common.AttributeTmpReserved) + if reserved.Bool() { + attributes.Remove(common.AttributeTmpReserved) + return true, nil + } + + if !svc.admitMetricData(metricData) { + svc.rollupMetricData(attributes) + + svc.totalRollup++ + admitted = false + + if m.LogDroppedMetrics { + m.logger.Debug(fmt.Sprintf("[%s] drop metric data", svc.name), zap.Any("labels", labels)) + } + } + + svc.totalMetricSent++ + + svc.rwLock.RLock() + defer svc.rwLock.RUnlock() + + svc.totalCount++ + svc.InsertMetricDataToPrimary(metricData) + svc.InsertMetricDataToSecondary(metricData) + return admitted, nil +} + +func (m *MetricsLimiter) filterAWSDeclaredAttributes(attributes, resourceAttributes pcommon.Map) (map[string]string, string, bool) { + svcNameAttr, exists := attributes.Get(common.CWMetricAttributeLocalService) + if !exists { + return nil, "", false + } + labels := map[string]string{} + svcName := svcNameAttr.AsString() + for _, attrKey := range awsDeclaredMetricAttributes { + if attr, ok := attributes.Get(attrKey); ok { + labels[attrKey] = attr.AsString() + } + } + return labels, svcName, true +} + +func (m *MetricsLimiter) removeStaleServices() { + var svcToRemove []string + for name, svc := range m.services { + if svc.rotations > 3 { + if svc.countSnapshot[0] == svc.countSnapshot[1] && svc.countSnapshot[1] == svc.countSnapshot[2] { + svc.cancelFunc() + svcToRemove = append(svcToRemove, name) + } + } + } + + m.mapLock.Lock() + defer m.mapLock.Unlock() + + for _, name := range svcToRemove { + m.logger.Info("remove stale service " + name + ".") + delete(m.services, name) + } +} + +type service struct { + logger *zap.Logger + name string + cancelFunc context.CancelFunc + + rwLock sync.RWMutex + primaryCMS *CountMinSketch + primaryTopK *topKMetrics + secondaryCMS *CountMinSketch + secondaryTopK *topKMetrics + + totalCount int + rotations int + countSnapshot []int + + totalRollup int + totalMetricSent int +} + +func (s *service) InsertMetricDataToPrimary(md *MetricData) { + s.primaryCMS.Insert(md) + updatedFrequency := s.primaryCMS.Get(md) + updatedMd := copyMetricDataWithUpdatedFrequency(md, updatedFrequency) + s.primaryTopK.Push(md, updatedMd) +} + +func (s *service) InsertMetricDataToSecondary(md *MetricData) { + if s.secondaryCMS != nil { + s.secondaryCMS.Insert(md) + updatedFrequency := s.secondaryCMS.Get(md) + updatedMd := copyMetricDataWithUpdatedFrequency(md, updatedFrequency) + s.secondaryTopK.Push(md, updatedMd) + } +} + +// MetricData represents a key-value pair. +type MetricData struct { + hashKey string + name string + service string + frequency int +} + +func (m MetricData) HashKey() string { + return m.hashKey +} + +func (m MetricData) Frequency() int { + return m.frequency +} + +func newMetricData(serviceName, metricName string, labels map[string]string) *MetricData { + hashID := sortAndConcatLabels(labels) + return &MetricData{ + hashKey: hashID, + name: metricName, + service: serviceName, + frequency: 1, + } +} + +func copyMetricDataWithUpdatedFrequency(md *MetricData, frequency int) *MetricData { + return &MetricData{ + hashKey: md.hashKey, + name: md.name, + service: md.service, + frequency: frequency, + } +} + +func sortAndConcatLabels(labels map[string]string) string { + keys := make([]string, 0, len(labels)) + for key := range labels { + keys = append(keys, key) + } + sort.Strings(keys) + + var concatenatedLabels string + for _, key := range keys { + concatenatedLabels += labels[key] + } + keys = nil + return concatenatedLabels +} + +// topKMetrics represents the priority queue with a map for key lookup and a size limit. +type topKMetrics struct { + metricMap map[string]*MetricData + minMetric *MetricData + sizeLimit int +} + +// newTopKMetrics creates a new topKMetrics with a specified size limit. +func newTopKMetrics(sizeLimit int) *topKMetrics { + return &topKMetrics{ + metricMap: make(map[string]*MetricData), + minMetric: nil, + sizeLimit: sizeLimit, + } +} + +// Push adds a key-value pair to the priority queue. If the value already exists, it updates the frequency. +func (t *topKMetrics) Push(oldMetric, newMetric *MetricData) { + hashValue := oldMetric.hashKey + if t.minMetric == nil { + t.minMetric = oldMetric + } + + _, found := t.metricMap[hashValue] + if found { + // Update the frequency. + t.metricMap[hashValue].frequency = newMetric.frequency + // Check if this oldMetric is the new minimum, find the new minMetric after the updates + if t.minMetric.hashKey == hashValue { + // Find the new minMetrics after update the frequency + t.minMetric = t.findMinMetric() + } + return + } + + // If exceeded size limit, delete the smallest + if len(t.metricMap) >= t.sizeLimit { + if newMetric.frequency > t.minMetric.frequency { + delete(t.metricMap, t.minMetric.hashKey) + t.metricMap[hashValue] = newMetric + t.minMetric = t.findMinMetric() + } + } else { + // Check if this newMetric is the new minimum. + if newMetric.frequency < t.minMetric.frequency { + t.minMetric = newMetric + } + t.metricMap[hashValue] = newMetric + } +} + +// findMinMetric removes and returns the key-value pair with the minimum value. +func (t *topKMetrics) findMinMetric() *MetricData { + // Find the new minimum metric and smallest frequency. + var newMinMetric *MetricData + smallestFrequency := int(^uint(0) >> 1) // Initialize with the maximum possible integer value + + for _, metric := range t.metricMap { + if metric.frequency < smallestFrequency { + smallestFrequency = metric.frequency + newMinMetric = metric + } + } + return newMinMetric +} + +func (s *service) admitMetricData(metric *MetricData) bool { + _, found := s.primaryTopK.metricMap[metric.hashKey] + if len(s.primaryTopK.metricMap) < s.primaryTopK.sizeLimit || found { + return true + } + return false +} + +func (s *service) rollupMetricData(attributes pcommon.Map) { + for _, indexAttr := range awsDeclaredMetricAttributes { + if (indexAttr == common.CWMetricAttributeEnvironment) || (indexAttr == common.CWMetricAttributeLocalService) || (indexAttr == common.CWMetricAttributeRemoteService) { + continue + } + if indexAttr == common.CWMetricAttributeLocalOperation { + attributes.PutStr(indexAttr, UnprocessedServiceOperationValue) + } else if indexAttr == common.CWMetricAttributeRemoteOperation { + attributes.PutStr(indexAttr, UnprocessedRemoteServiceOperationValue) + } else { + attributes.PutStr(indexAttr, "-") + } + } +} + +// As a starting point, you can use rules of thumb, such as setting the depth to be around 4-6 times the logarithm of the expected number of distinct items and the width based on your memory constraints. However, these are rough guidelines, and the optimal size will depend on your unique application and requirements. +func newService(name string, limit int, rotationInterval time.Duration, parentCtx context.Context, logger *zap.Logger) *service { + depth := defaultCMSDepth + width := defaultCMSWidth + + ctx, cancel := context.WithCancel(parentCtx) + svc := &service{ + logger: logger, + name: name, + cancelFunc: cancel, + primaryCMS: NewCountMinSketch(depth, width), + primaryTopK: newTopKMetrics(limit), + countSnapshot: make([]int, 3), + } + + // Create a ticker to create a new countMinSketch every 1 hour + rotationTicker := time.NewTicker(rotationInterval) + //defer rotationTicker.Stop() + + // Create a goroutine to handle rotationTicker.C + go func() { + for { + select { + case <-rotationTicker.C: + svc.logger.Info(fmt.Sprintf("[%s] rotating visit records, current rotation %d", name, svc.rotations)) + if err := rotateVisitRecords(svc); err != nil { + svc.logger.Error(fmt.Sprintf("[%s] failed to rotate visit records.", name), zap.Error(err)) + } + case <-ctx.Done(): + return + default: + // Continue running the main program + time.Sleep(1 * time.Second) + } + } + }() + + svc.logger.Info(fmt.Sprintf("[%s] service entry is created.\n", name)) + return svc +} + +func rotateVisitRecords(svc *service) error { + svc.rwLock.Lock() + defer svc.rwLock.Unlock() + + cmsDepth := svc.primaryCMS.depth + cmsWidth := svc.primaryCMS.width + topKLimit := svc.primaryTopK.sizeLimit + + nextPrimaryCMS := svc.secondaryCMS + nextPrimaryTopK := svc.secondaryTopK + + svc.secondaryCMS = NewCountMinSketch(cmsDepth, cmsWidth) + svc.secondaryTopK = newTopKMetrics(topKLimit) + + if nextPrimaryCMS != nil && nextPrimaryTopK != nil { + svc.primaryCMS = nextPrimaryCMS + svc.primaryTopK = nextPrimaryTopK + } else { + svc.logger.Info(fmt.Sprintf("[%s] secondary visit records are nil.", svc.name)) + } + + svc.countSnapshot[svc.rotations%3] = svc.totalCount + svc.rotations++ + + return nil +} diff --git a/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter_test.go b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter_test.go new file mode 100644 index 000000000000..af96c615bd4f --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol/metrics_limiter_test.go @@ -0,0 +1,272 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cardinalitycontrol + +import ( + "context" + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.uber.org/zap" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + awsapplicationsignalsconfig "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" +) + +var emptyResourceAttributes = pcommon.NewMap() +var logger, _ = zap.NewDevelopment() + +func TestAdmitAndRollup(t *testing.T) { + config := &awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 2, + Disabled: false, + LogDroppedMetrics: false, + RotationInterval: awsapplicationsignalsconfig.DefaultRotationInterval, + } + config.Validate() + + limiter := NewMetricsLimiter(config, logger) + + admittedAttributes := map[string]pcommon.Map{} + for i := 0; i < 10; i++ { + attr := newLowCardinalityAttributes(100) + if ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes); ok { + uniqKey, _ := attr.Get("RemoteOperation") + admittedAttributes[uniqKey.AsString()] = attr + } else { + for _, indexedAttrKey := range awsDeclaredMetricAttributes { + if indexedAttrKey == common.CWMetricAttributeEnvironment || + indexedAttrKey == common.CWMetricAttributeLocalService || + indexedAttrKey == common.CWMetricAttributeRemoteService { + continue + } + attrValue, _ := attr.Get(indexedAttrKey) + if indexedAttrKey == common.CWMetricAttributeLocalOperation { + assert.Equal(t, UnprocessedServiceOperationValue, attrValue.AsString()) + } else if indexedAttrKey == common.CWMetricAttributeRemoteOperation { + assert.Equal(t, UnprocessedRemoteServiceOperationValue, attrValue.AsString()) + } else { + assert.Equal(t, "-", attrValue.AsString()) + } + } + } + } + assert.Equal(t, 2, len(admittedAttributes), fmt.Sprintf("admitted attributes are %v", admittedAttributes)) +} + +func TestAdmitByTopK(t *testing.T) { + config := awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 100, + Disabled: false, + LogDroppedMetrics: false, + RotationInterval: awsapplicationsignalsconfig.DefaultRotationInterval, + } + config.Validate() + + limiter := NewMetricsLimiter(&config, logger) + + // fulfill topk with high cardinality attributes + for i := 0; i < 110; i++ { + attr := newHighCardinalityAttributes() + limiter.Admit("latency", attr, emptyResourceAttributes) + } + + // sending low cardinality attributes + for i := 0; i < 100; i++ { + attr := newFixedAttributes(i % 20) + limiter.Admit("latency", attr, emptyResourceAttributes) + } + + for i := 0; i < 20; i++ { + attr := newFixedAttributes(i) + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.True(t, ok) + } +} + +func TestAdmitLowCardinalityAttributes(t *testing.T) { + config := awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 10, + Disabled: false, + LogDroppedMetrics: false, + RotationInterval: awsapplicationsignalsconfig.DefaultRotationInterval, + } + config.Validate() + + limiter := NewMetricsLimiter(&config, logger) + + rejectCount := 0 + for i := 0; i < 100; i++ { + if ok, _ := limiter.Admit("latency", newLowCardinalityAttributes(10), emptyResourceAttributes); !ok { + rejectCount += 1 + } + } + assert.Equal(t, 0, rejectCount) +} + +func TestAdmitReservedMetrics(t *testing.T) { + config := awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 10, + Disabled: false, + LogDroppedMetrics: false, + RotationInterval: awsapplicationsignalsconfig.DefaultRotationInterval, + } + config.Validate() + + limiter := NewMetricsLimiter(&config, logger) + + // fulfill topk with high cardinality attributes + for i := 0; i < 20; i++ { + attr := newHighCardinalityAttributes() + limiter.Admit("latency", attr, emptyResourceAttributes) + } + + for i := 0; i < 20; i++ { + attr := newHighCardinalityAttributes() + // simulate attributes touched by customization rules + attr.PutBool(common.AttributeTmpReserved, true) + + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.True(t, ok) + _, exists := attr.Get(common.AttributeTmpReserved) + assert.False(t, exists) + } +} + +func TestClearStaleService(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + + config := awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 10, + Disabled: false, + LogDroppedMetrics: false, + + ParentContext: ctx, + RotationInterval: time.Second, + GarbageCollectionInterval: time.Second, + } + limiter := NewMetricsLimiter(&config, logger) + + for i := 0; i < 10; i++ { + appName := "app" + strconv.Itoa(i) + attr := pcommon.NewMap() + attr.PutStr("Service", appName) + limiter.Admit(appName, attr, emptyResourceAttributes) + } + + time.Sleep(10 * time.Second) + cancel() + + metricsLimiter := limiter.(*MetricsLimiter) + assert.Equal(t, 0, len(metricsLimiter.services)) +} + +func TestInheritanceAfterRotation(t *testing.T) { + config := awsapplicationsignalsconfig.LimiterConfig{ + Threshold: 10, + Disabled: false, + LogDroppedMetrics: true, + RotationInterval: 5 * time.Second, + } + config.Validate() + + limiter := NewMetricsLimiter(&config, logger) + + // fulfill primary with 0-10 + for i := 0; i < 10; i++ { + attr := newFixedAttributes(i) + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.True(t, ok) + } + + // wait for rotation + time.Sleep(6 * time.Second) + // validate 0-10 are admitted + for i := 0; i < 10; i++ { + attr := newFixedAttributes(i) + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.True(t, ok) + } + + // validate 10-20 are rejected + // promote 10-20 to top k + for j := 0; j < 2; j++ { + for i := 10; i < 20; i++ { + attr := newFixedAttributes(i) + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.False(t, ok) + } + } + + // wait for rotation + time.Sleep(6 * time.Second) + + // validate 1--20 are admitted + for i := 10; i < 20; i++ { + attr := newFixedAttributes(i) + ok, _ := limiter.Admit("latency", attr, emptyResourceAttributes) + assert.True(t, ok) + } +} + +func TestRotationInterval(t *testing.T) { + svc := newService("test", 1, 5*time.Second, context.Background(), logger) + // wait for secondary to be created + time.Sleep(7 * time.Second) + for i := 0; i < 5; i++ { + svc.secondaryCMS.matrix[0][0] = 1 + + // wait for rotation + time.Sleep(5 * time.Second) + + // verify secondary is promoted to primary + assert.Equal(t, 0, svc.secondaryCMS.matrix[0][0]) + assert.Equal(t, 1, svc.primaryCMS.matrix[0][0]) + } +} + +func newRandomIP() string { + rand.NewSource(time.Now().UnixNano()) + + ipPart1 := rand.Intn(256) + ipPart2 := rand.Intn(256) + ipPart3 := rand.Intn(256) + ipPart4 := rand.Intn(256) + + return fmt.Sprintf("%d.%d.%d.%d", ipPart1, ipPart2, ipPart3, ipPart4) +} + +func newFixedAttributes(val int) pcommon.Map { + methodName := "/test" + strconv.Itoa(val) + attr := pcommon.NewMap() + attr.PutStr("Service", "app") + attr.PutStr("Operation", "/api/gateway"+methodName) + attr.PutStr("RemoteService", "upstream1") + attr.PutStr("RemoteOperation", methodName) + return attr +} + +func newLowCardinalityAttributes(admitRange int) pcommon.Map { + methodName := "/test" + strconv.Itoa(rand.Intn(admitRange)) + attr := pcommon.NewMap() + attr.PutStr("Service", "app") + attr.PutStr("Operation", "/api/gateway"+methodName) + attr.PutStr("RemoteService", "upstream1") + attr.PutStr("RemoteOperation", methodName) + return attr +} + +func newHighCardinalityAttributes() pcommon.Map { + attr := pcommon.NewMap() + attr.PutStr("Service", "app") + attr.PutStr("Operation", "/api/gateway/test") + attr.PutStr("RemoteService", newRandomIP()) + attr.PutStr("RemoteOperation", "/test/"+strconv.Itoa(rand.Int())) + return attr +} diff --git a/processor/awsapplicationsignalsprocessor/internal/ecsutil/ecsutil.go b/processor/awsapplicationsignalsprocessor/internal/ecsutil/ecsutil.go new file mode 100644 index 000000000000..d2b1fe973b58 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/ecsutil/ecsutil.go @@ -0,0 +1,126 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package ecsutil + +import ( + "encoding/json" + "log" + "os" + "strings" + "sync" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/httpclient" +) + +const ( + v2MetadataEndpoint = "http://169.254.170.2/v2/metadata" + v3MetadataEndpointEnv = "ECS_CONTAINER_METADATA_URI" + v4MetadataEndpointEnv = "ECS_CONTAINER_METADATA_URI_V4" +) + +// The following values are borrowed from: +// - https://github.com/aws/amazon-cloudwatch-agent/blob/bde3bd9775ae1d4e4f8a2fdb92d7b6fdd5186fba/cfg/envconfig/envconfig.go +const ( + RunInContainer = "RUN_IN_CONTAINER" + TrueValue = "True" +) + +type ecsMetadataResponse struct { + Cluster string + TaskARN string +} + +type ecsUtil struct { + Cluster string + Region string + TaskARN string + httpClient *httpclient.HttpClient +} + +var ecsUtilInstance *ecsUtil + +var ecsUtilOnce sync.Once + +func GetECSUtilSingleton() *ecsUtil { + ecsUtilOnce.Do(func() { + ecsUtilInstance = initECSUtilSingleton() + }) + return ecsUtilInstance +} + +func initECSUtilSingleton() (newInstance *ecsUtil) { + newInstance = &ecsUtil{httpClient: httpclient.New()} + if os.Getenv(RunInContainer) != TrueValue { + return + } + log.Println("I! attempt to access ECS task metadata to determine whether I'm running in ECS.") + ecsMetadataResponse, err := newInstance.getECSMetadata() + + if err != nil { + log.Printf("I! access ECS task metadata fail with response %v, assuming I'm not running in ECS.\n", err) + return + } + + newInstance.parseRegion(ecsMetadataResponse) + newInstance.parseClusterName(ecsMetadataResponse) + newInstance.TaskARN = ecsMetadataResponse.TaskARN + return + +} + +func (e *ecsUtil) IsECS() bool { + return e.Region != "" +} + +func (e *ecsUtil) getECSMetadata() (em *ecsMetadataResponse, err error) { + // Based on endpoint to get ECS metadata, for more information on the respond, https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint.html + if v4MetadataEndpoint, ok := os.LookupEnv(v4MetadataEndpointEnv); ok { + em, err = e.getMetadataResponse(v4MetadataEndpoint + "/task") + } else if v3MetadataEndpoint, ok := os.LookupEnv(v3MetadataEndpointEnv); ok { + em, err = e.getMetadataResponse(v3MetadataEndpoint + "/task") + } else { + em, err = e.getMetadataResponse(v2MetadataEndpoint) + } + return +} + +func (e *ecsUtil) getMetadataResponse(endpoint string) (em *ecsMetadataResponse, err error) { + em = &ecsMetadataResponse{} + resp, err := e.httpClient.Request(endpoint) + + if err != nil { + return + } + + err = json.Unmarshal(resp, em) + if err != nil { + log.Printf("E! Unable to parse response from ecsmetadata endpoint, error: %v", err) + log.Printf("D! Content is %s", string(resp)) + } + return +} + +// There are two formats of Task ARN (https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs-account-settings.html#ecs-resource-ids) +// arn:aws:ecs:region:aws_account_id:task/task-id +// arn:aws:ecs:region:aws_account_id:task/cluster-name/task-id +// This function will return region extracted from Task ARN +func (e *ecsUtil) parseRegion(em *ecsMetadataResponse) { + splitedContent := strings.Split(em.TaskARN, ":") + // When splitting the ARN with ":", the 4th segment is the region + if len(splitedContent) < 4 { + log.Printf("E! Invalid ecs task arn: %s", em.TaskARN) + } + e.Region = splitedContent[3] +} + +// There is only one format for ClusterArn (https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Cluster.html) +// arn:aws:ecs:region:aws_account_id:cluster/cluster-name +func (e *ecsUtil) parseClusterName(em *ecsMetadataResponse) { + splitedContent := strings.Split(em.Cluster, "/") + // When splitting the ClusterName with /, the last is always the cluster name + if len(splitedContent) == 0 { + log.Printf("E! Invalid cluster arn: %s", em.Cluster) + } + e.Cluster = splitedContent[len(splitedContent)-1] +} diff --git a/processor/awsapplicationsignalsprocessor/internal/httpclient/httpclient.go b/processor/awsapplicationsignalsprocessor/internal/httpclient/httpclient.go new file mode 100644 index 000000000000..08df1ca8838b --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/httpclient/httpclient.go @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package httpclient + +import ( + "fmt" + "io" + "log" + "math" + "net/http" + "time" +) + +const ( + defaultMaxRetries = 3 + defaultTimeout = 1 * time.Second + defaultBackoffRetryBaseInMills = 200 +) + +type HttpClient struct { + maxRetries int + backoffRetryBaseInMills int + client *http.Client +} + +func New() *HttpClient { + return &HttpClient{ + maxRetries: defaultMaxRetries, + backoffRetryBaseInMills: defaultBackoffRetryBaseInMills, + client: &http.Client{Timeout: defaultTimeout}, + } +} + +func (h *HttpClient) backoffSleep(currentRetryCount int) { + backoffInMillis := int64(float64(h.backoffRetryBaseInMills) * math.Pow(2, float64(currentRetryCount))) + sleepDuration := time.Millisecond * time.Duration(backoffInMillis) + if sleepDuration > 60*1000 { + sleepDuration = 60 * 1000 + } + time.Sleep(sleepDuration) +} + +func (h *HttpClient) Request(endpoint string) (body []byte, err error) { + for i := 0; i < h.maxRetries; i++ { + body, err = h.request(endpoint) + if err != nil { + log.Printf("W! retry [%d/%d], unable to get http response from %s, error: %v", i, h.maxRetries, endpoint, err) + h.backoffSleep(i) + } + } + return +} + +func (h *HttpClient) request(endpoint string) ([]byte, error) { + resp, err := h.client.Get(endpoint) + if err != nil { + return nil, fmt.Errorf("unable to get response from %s, error: %v", endpoint, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unable to get response from %s, status code: %d", endpoint, resp.StatusCode) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body from %s, error: %v", endpoint, err) + } + + return body, nil +} diff --git a/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer.go b/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer.go new file mode 100644 index 000000000000..59d34f053378 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer.go @@ -0,0 +1,247 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package normalizer + +import ( + "fmt" + "strings" + + "go.opentelemetry.io/collector/pdata/pcommon" + deprecatedsemconv "go.opentelemetry.io/collector/semconv/v1.18.0" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + + // "github.com/aws/amazon-cloudwatch-agent/internal/version" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +const ( + // Length limits from Application Signals SLOs + maxEnvironmentLength = 259 + maxServiceNameLength = 255 + + // Length limits from CloudWatch Metrics + defaultMetricAttributeLength = 1024 +) + +type attributesNormalizer struct { + logger *zap.Logger +} + +var attributesRenamingForMetric = map[string]string{ + attr.AWSLocalService: common.CWMetricAttributeLocalService, + attr.AWSLocalOperation: common.CWMetricAttributeLocalOperation, + attr.AWSLocalEnvironment: common.CWMetricAttributeEnvironment, + attr.AWSRemoteService: common.CWMetricAttributeRemoteService, + attr.AWSRemoteOperation: common.CWMetricAttributeRemoteOperation, + attr.AWSRemoteEnvironment: common.CWMetricAttributeRemoteEnvironment, + attr.AWSRemoteTarget: common.CWMetricAttributeRemoteResourceIdentifier, + attr.AWSRemoteResourceIdentifier: common.CWMetricAttributeRemoteResourceIdentifier, + attr.AWSRemoteResourceType: common.CWMetricAttributeRemoteResourceType, + attr.AWSRemoteDbUser: common.MetricAttributeRemoteDbUser, + attr.AWSRemoteResourceCfnPrimaryIdentifier: common.MetricAttributeRemoteResourceCfnPrimaryIdentifier, + attr.AWSECSClusterName: common.MetricAttributeECSCluster, + attr.AWSECSTaskID: common.MetricAttributeECSTaskId, +} + +var resourceAttributesRenamingForTrace = map[string]string{ + // these kubernetes resource attributes are set by the OpenTelemetry operator + // see the code references from upstream: + // * https://github.com/open-telemetry/opentelemetry-operator/blob/0e39ee77693146e0924da3ca474a0fe14dc30b3a/pkg/instrumentation/sdk.go#L245 + // * https://github.com/open-telemetry/opentelemetry-operator/blob/0e39ee77693146e0924da3ca474a0fe14dc30b3a/pkg/instrumentation/sdk.go#L305C43-L305C43 + semconv.AttributeK8SDeploymentName: common.AttributeK8SWorkload, + semconv.AttributeK8SStatefulSetName: common.AttributeK8SWorkload, + semconv.AttributeK8SDaemonSetName: common.AttributeK8SWorkload, + semconv.AttributeK8SJobName: common.AttributeK8SWorkload, + semconv.AttributeK8SCronJobName: common.AttributeK8SWorkload, + semconv.AttributeK8SPodName: common.AttributeK8SPod, +} + +var attributesRenamingForTrace = map[string]string{ + attr.AWSRemoteTarget: attr.AWSRemoteResourceIdentifier, +} + +var resourceToMetricAttributes = map[string]string{ + // these kubernetes resource attributes are set by the OpenTelemetry operator + // see the code references from upstream: + // * https://github.com/open-telemetry/opentelemetry-operator/blob/0e39ee77693146e0924da3ca474a0fe14dc30b3a/pkg/instrumentation/sdk.go#L245 + // * https://github.com/open-telemetry/opentelemetry-operator/blob/0e39ee77693146e0924da3ca474a0fe14dc30b3a/pkg/instrumentation/sdk.go#L305C43-L305C43 + semconv.AttributeK8SDeploymentName: common.AttributeK8SWorkload, + semconv.AttributeK8SStatefulSetName: common.AttributeK8SWorkload, + semconv.AttributeK8SDaemonSetName: common.AttributeK8SWorkload, + semconv.AttributeK8SJobName: common.AttributeK8SWorkload, + semconv.AttributeK8SCronJobName: common.AttributeK8SWorkload, + semconv.AttributeK8SPodName: common.AttributeK8SPod, + semconv.AttributeAWSLogGroupNames: "aws.log.group.names", + semconv.AttributeAWSECSTaskRevision: common.MetricAttributeECSTaskDefinitionRevision, + semconv.AttributeAWSECSTaskFamily: common.MetricAttributeECSTaskDefinitionFamily, +} + +const ( + instrumentationModeAuto = "Auto" + instrumentationModeManual = "Manual" +) + +func NewAttributesNormalizer(logger *zap.Logger) *attributesNormalizer { + return &attributesNormalizer{ + logger: logger, + } +} + +func (n *attributesNormalizer) Process(attributes, resourceAttributes pcommon.Map, isTrace bool) error { + n.copyResourceAttributesToAttributes(attributes, resourceAttributes, isTrace) + truncateAttributesByLength(attributes) + n.renameAttributes(attributes, resourceAttributes, isTrace) + n.normalizeTelemetryAttributes(attributes, resourceAttributes, isTrace) + return nil +} + +func (n *attributesNormalizer) renameAttributes(attributes, resourceAttributes pcommon.Map, isTrace bool) { + if isTrace { + rename(resourceAttributes, resourceAttributesRenamingForTrace) + rename(attributes, attributesRenamingForTrace) + } else { + rename(attributes, attributesRenamingForMetric) + } +} + +func (n *attributesNormalizer) copyResourceAttributesToAttributes(attributes, resourceAttributes pcommon.Map, isTrace bool) { + if isTrace { + return + } + for k, v := range resourceToMetricAttributes { + if resourceAttrValue, ok := resourceAttributes.Get(k); ok { + // print some debug info when an attribute value is overwritten + if originalAttrValue, ok := attributes.Get(k); ok { + n.logger.Debug("attribute value is overwritten", zap.String("attribute", k), zap.String("original", originalAttrValue.AsString()), zap.String("new", resourceAttrValue.AsString())) + } + attributes.PutStr(v, resourceAttrValue.AsString()) + if k == semconv.AttributeK8SPodName { + // only copy "host.id" from resource attributes to "K8s.Node" in attributesif the pod name is set + if host, ok := resourceAttributes.Get("host.id"); ok { + attributes.PutStr("K8s.Node", host.AsString()) + } + } + } + } + // If a metric doesn't `aws.local.service` attribute, copy it from resource attributes. This is now only used in + // processing runtime metrics. + if serviceAttribute, ok := resourceAttributes.Get(attr.AWSLocalService); ok { + attributes.PutStr(attr.AWSLocalService, serviceAttribute.AsString()) + } +} + +func (n *attributesNormalizer) normalizeTelemetryAttributes(attributes, resourceAttributes pcommon.Map, isTrace bool) { + if isTrace { + return + } + + var ( + sdkName string + sdkVersion string + sdkLang string + ) + var ( + sdkAutoName string + sdkAutoVersion string + ) + sdkName, sdkVersion, sdkLang = "-", "-", "-" + mode := instrumentationModeManual + + resourceAttributes.Range(func(k string, v pcommon.Value) bool { + switch k { + case semconv.AttributeTelemetrySDKName: + sdkName = removeWhitespaces(v.Str()) + case semconv.AttributeTelemetrySDKLanguage: + sdkLang = removeWhitespaces(v.Str()) + case semconv.AttributeTelemetrySDKVersion: + sdkVersion = removeWhitespaces(v.Str()) + } + switch k { + case semconv.AttributeTelemetryDistroName: + sdkAutoName = removeWhitespaces(v.Str()) + case deprecatedsemconv.AttributeTelemetryAutoVersion, semconv.AttributeTelemetryDistroVersion: + sdkAutoVersion = removeWhitespaces(v.Str()) + } + return true + }) + if sdkAutoName != "" { + sdkName = sdkAutoName + mode = instrumentationModeAuto + } + if sdkAutoVersion != "" { + sdkVersion = sdkAutoVersion + mode = instrumentationModeAuto + } + attributes.PutStr(common.MetricAttributeTelemetrySDK, fmt.Sprintf("%s,%s,%s,%s", sdkName, sdkVersion, sdkLang, mode)) + // NOTE: In CWAgent, `opentelemetry-collector` is replaced by `CWAgent` + attributes.PutStr(common.MetricAttributeTelemetryAgent, fmt.Sprintf("opentelemetry-collector/%s", GetCollectorVersion())) + + var telemetrySource string + if val, ok := attributes.Get(attr.AWSSpanKind); ok { + switch val.Str() { + case "CLIENT": + telemetrySource = "ClientSpan" + case "SERVER": + telemetrySource = "ServerSpan" + case "PRODUCER": + telemetrySource = "ProducerSpan" + case "CONSUMER": + telemetrySource = "ConsumerSpan" + case "LOCAL_ROOT": + telemetrySource = "LocalRootSpan" + } + attributes.PutStr(common.MetricAttributeTelemetrySource, telemetrySource) + attributes.Remove(attr.AWSSpanKind) + } +} + +func rename(attrs pcommon.Map, renameMap map[string]string) { + for original, replacement := range renameMap { + if value, ok := attrs.Get(original); ok { + attrs.PutStr(replacement, value.AsString()) + attrs.Remove(original) + if original == semconv.AttributeK8SPodName { + // only rename host.id if the pod name is set + if host, ok := attrs.Get("host.id"); ok { + attrs.PutStr("K8s.Node", host.AsString()) + } + } + } + } +} + +func truncateAttributesByLength(attributes pcommon.Map) { + // It's assumed that all attributes are initially inserted as trace attribute, and attributesRenamingForMetric + // contains all attributes that will be used for CloudWatch metric dimension. Therefore, we iterate the keys + // for enforcing the limits on length. + for attrKey := range attributesRenamingForMetric { + switch attrKey { + case attr.AWSLocalEnvironment, attr.AWSRemoteEnvironment: + if val, ok := attributes.Get(attrKey); ok { + attributes.PutStr(attrKey, truncateStringByLength(val.Str(), maxEnvironmentLength)) + } + case attr.AWSLocalService, attr.AWSRemoteService: + if val, ok := attributes.Get(attrKey); ok { + attributes.PutStr(attrKey, truncateStringByLength(val.Str(), maxServiceNameLength)) + } + default: + if val, ok := attributes.Get(attrKey); ok { + attributes.PutStr(attrKey, truncateStringByLength(val.Str(), defaultMetricAttributeLength)) + } + } + } +} + +func truncateStringByLength(val string, length int) string { + if len(val) > length { + return val[:length] + } + return val +} + +func removeWhitespaces(val string) string { + return strings.ReplaceAll(val, " ", "") +} diff --git a/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer_test.go b/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer_test.go new file mode 100644 index 000000000000..96cd3afb9dc1 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/normalizer/attributesnormalizer_test.go @@ -0,0 +1,264 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package normalizer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + deprecatedsemconv "go.opentelemetry.io/collector/semconv/v1.18.0" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +func TestRenameAttributes_for_metric(t *testing.T) { + logger, _ := zap.NewDevelopment() + normalizer := NewAttributesNormalizer(logger) + + // test for metric + // Create a pcommon.Map with some attributes + attributes := pcommon.NewMap() + for originalKey, replacementKey := range attributesRenamingForMetric { + attributes.PutStr(originalKey, replacementKey+"-value") + } + + resourceAttributes := pcommon.NewMap() + // Call the process method + normalizer.renameAttributes(attributes, resourceAttributes, false) + + // Check that the original key has been removed + for originalKey := range attributesRenamingForMetric { + if _, ok := attributes.Get(originalKey); ok { + t.Errorf("originalKey was not removed") + } + } + + // Check that the new key has the correct value + for _, replacementKey := range attributesRenamingForMetric { + assertStringAttributeEqual(t, attributes, replacementKey, replacementKey+"-value") + } +} + +func TestRenameAttributes_for_trace(t *testing.T) { + logger, _ := zap.NewDevelopment() + normalizer := NewAttributesNormalizer(logger) + + // test for trace + // Create a pcommon.Map with some attributes + resourceAttributes := pcommon.NewMap() + for originalKey, replacementKey := range resourceAttributesRenamingForTrace { + resourceAttributes.PutStr(originalKey, replacementKey+"-value") + } + resourceAttributes.PutStr("host.id", "i-01ef7d37f42caa168") + + attributes := pcommon.NewMap() + // Call the process method + normalizer.renameAttributes(attributes, resourceAttributes, true) + + // Check that the original key has been removed + for originalKey := range resourceAttributesRenamingForTrace { + if _, ok := resourceAttributes.Get(originalKey); ok { + t.Errorf("originalKey was not removed") + } + } + + // Check that the new key has the correct value + for _, replacementKey := range resourceAttributesRenamingForTrace { + assertStringAttributeEqual(t, resourceAttributes, replacementKey, replacementKey+"-value") + } + + if value, ok := resourceAttributes.Get("K8s.Node"); !ok || value.AsString() != "i-01ef7d37f42caa168" { + t.Errorf("replacementKey has incorrect value: got %v, want %v", value.AsString(), "i-01ef7d37f42caa168") + } +} + +func TestCopyResourceAttributesToAttributes(t *testing.T) { + logger, _ := zap.NewDevelopment() + normalizer := NewAttributesNormalizer(logger) + + // Create a pcommon.Map for resourceAttributes with some attributes + resourceAttributes := pcommon.NewMap() + for resourceAttrKey, attrKey := range resourceToMetricAttributes { + resourceAttributes.PutStr(resourceAttrKey, attrKey+"-value") + } + resourceAttributes.PutStr("host.id", "i-01ef7d37f42caa168") + resourceAttributes.PutStr("aws.local.service", "test-app") + + // Create a pcommon.Map for attributes + attributes := pcommon.NewMap() + + // Call the process method + normalizer.copyResourceAttributesToAttributes(attributes, resourceAttributes, false) + + // Check that the attribute has been copied correctly + for _, attrKey := range resourceToMetricAttributes { + assertStringAttributeEqual(t, attributes, attrKey, attrKey+"-value") + } + + assertStringAttributeEqual(t, attributes, "K8s.Node", "i-01ef7d37f42caa168") + assertStringAttributeEqual(t, attributes, "aws.local.service", "test-app") +} + +func TestTruncateAttributes(t *testing.T) { + attributes := pcommon.NewMap() + + longValue := make([]byte, 300) + for i := 0; i < 300; i++ { + longValue[i] = 'a' + } + longStringValue := string(longValue) + for key, _ := range attributesRenamingForMetric { + attributes.PutStr(key, longStringValue) + } + + truncateAttributesByLength(attributes) + + val, _ := attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, len(val.Str()) == maxEnvironmentLength) + val, _ = attributes.Get(attr.AWSRemoteEnvironment) + assert.True(t, len(val.Str()) == maxEnvironmentLength) + val, _ = attributes.Get(attr.AWSLocalService) + assert.True(t, len(val.Str()) == maxServiceNameLength) + val, _ = attributes.Get(attr.AWSRemoteService) + assert.True(t, len(val.Str()) == maxServiceNameLength) + val, _ = attributes.Get(attr.AWSRemoteResourceIdentifier) + assert.True(t, len(val.Str()) == 300) +} + +func Test_attributesNormalizer_appendNewAttributes(t *testing.T) { + logger, _ := zap.NewDevelopment() + + completeResourceAttributes := pcommon.NewMap() + completeResourceAttributes.PutStr(semconv.AttributeTelemetrySDKName, "opentelemetry") + completeResourceAttributes.PutStr(deprecatedsemconv.AttributeTelemetryAutoVersion, "0.0.1 auto") + completeResourceAttributes.PutStr(semconv.AttributeTelemetrySDKVersion, "0.0.1 test") + completeResourceAttributes.PutStr(semconv.AttributeTelemetrySDKLanguage, "go") + + incompleteResourceAttributes := pcommon.NewMap() + incompleteResourceAttributes.PutStr(semconv.AttributeTelemetrySDKName, "opentelemetry") + incompleteResourceAttributes.PutStr(semconv.AttributeTelemetrySDKVersion, "0.0.1 test") + + tests := []struct { + name string + attributes pcommon.Map + resourceAttributes pcommon.Map + isTrace bool + expectedAttributeValue string + }{ + { + "testAppendNoAttributesToTrace", + pcommon.NewMap(), + completeResourceAttributes, + true, + "", + }, { + "testAppendAttributesToMetricWithValuesFound", + pcommon.NewMap(), + completeResourceAttributes, + false, + "opentelemetry,0.0.1auto,go,Auto", + }, + { + "testAppendAttributesToMetricWithSomeValuesMissing", + pcommon.NewMap(), + incompleteResourceAttributes, + false, + "opentelemetry,0.0.1test,-,Manual", + }, + { + + "testAppendAttributesToMetricWithAllValuesMissing", + pcommon.NewMap(), + pcommon.NewMap(), + false, + "-,-,-,Manual", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &attributesNormalizer{ + logger: logger, + } + n.normalizeTelemetryAttributes(tt.attributes, tt.resourceAttributes, tt.isTrace) + + if value, ok := tt.attributes.Get("Telemetry.SDK"); !ok { + if !tt.isTrace { + t.Errorf("attribute is not found.") + } + } else { + if tt.isTrace { + t.Errorf("unexpected attribute is found.") + } + assert.Equal(t, tt.expectedAttributeValue, value.Str()) + } + }) + } +} + +func TestRenameAttributes_AWSRemoteDbUser_for_metric(t *testing.T) { + logger, _ := zap.NewDevelopment() + normalizer := NewAttributesNormalizer(logger) + + attributes := pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteDbUser, "remoteDbUser-value") + + resourceAttributes := pcommon.NewMap() + normalizer.renameAttributes(attributes, resourceAttributes, false) + + if _, ok := attributes.Get(attr.AWSRemoteDbUser); ok { + t.Errorf("AWSRemoteDbUser was not removed") + } + + if value, ok := attributes.Get("RemoteDbUser"); !ok || value.AsString() != "remoteDbUser-value" { + t.Errorf("MetricAttributeRemoteDbUser has incorrect value: got %v, want %v", value.AsString(), "remoteDbUser-value") + } +} + +func TestTruncateAttributes_AWSRemoteDbUser(t *testing.T) { + attributes := pcommon.NewMap() + + longValue := make([]byte, 300) + for i := 0; i < 300; i++ { + longValue[i] = 'a' + } + longStringValue := string(longValue) + attributes.PutStr(attr.AWSRemoteDbUser, longStringValue) + + truncateAttributesByLength(attributes) + + val, _ := attributes.Get(attr.AWSRemoteDbUser) + assert.True(t, len(val.Str()) <= defaultMetricAttributeLength) +} + +func TestRenameAttributes_AWSRemoteResourceCfnIdentifier_for_metric(t *testing.T) { + logger, _ := zap.NewDevelopment() + normalizer := NewAttributesNormalizer(logger) + + attributes := pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteResourceCfnPrimaryIdentifier, "arn:123:abc-value") + + resourceAttributes := pcommon.NewMap() + normalizer.renameAttributes(attributes, resourceAttributes, false) + + if _, ok := attributes.Get(attr.AWSRemoteResourceCfnPrimaryIdentifier); ok { + t.Errorf("AWSRemoteResourceCfnPrimaryIdentifier was not removed") + } + + if value, ok := attributes.Get("RemoteResourceCfnPrimaryIdentifier"); !ok || value.AsString() != "arn:123:abc-value" { + t.Errorf("RemoteResourceCfnPrimaryIdentifier has incorrect value: got %v, want %v", value.AsString(), "arn:123:abc-value") + } +} + +func assertStringAttributeEqual(t *testing.T, attributes pcommon.Map, attrKey, attrVal string) { + if val, ok := attributes.Get(attrKey); ok { + if val.AsString() != attrVal { + t.Errorf("Attribute was not copied correctly: got %v, want %v", val.AsString(), attrVal) + } + } else { + t.Errorf("Attribute %s is not found", attrKey) + } +} diff --git a/processor/awsapplicationsignalsprocessor/internal/normalizer/version_info.go b/processor/awsapplicationsignalsprocessor/internal/normalizer/version_info.go new file mode 100644 index 000000000000..770c84386d45 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/normalizer/version_info.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package normalizer + +import ( + "runtime/debug" + "sync" +) + +var ( + once sync.Once + cachedVersion string +) + +func GetCollectorVersion() string { + once.Do(func() { + info, ok := debug.ReadBuildInfo() + if !ok { + cachedVersion = "UNKNOWN" + return + } + + for _, mod := range info.Deps { + if mod.Path == "go.opentelemetry.io/collector" { + cachedVersion = mod.Version + return + } + } + + cachedVersion = "UNKNOWN" + }) + + return cachedVersion +} diff --git a/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner.go b/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner.go new file mode 100644 index 000000000000..c492a5d4ec30 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner.go @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package prune + +import ( + "errors" + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" +) + +type MetricPruner struct { +} + +func (p *MetricPruner) ShouldBeDropped(attributes pcommon.Map) (bool, error) { + for _, attributeKey := range common.CWMetricAttributes { + if val, ok := attributes.Get(attributeKey); ok { + if !isAsciiPrintable(val.Str()) { + return true, errors.New("Metric attribute " + attributeKey + " must contain only ASCII characters.") + } + } + if _, ok := attributes.Get(common.MetricAttributeTelemetrySource); !ok { + return true, errors.New(fmt.Sprintf("Metric must contain %s.", common.MetricAttributeTelemetrySource)) + } + } + return false, nil +} + +func NewPruner() *MetricPruner { + return &MetricPruner{} +} + +func isAsciiPrintable(val string) bool { + nonWhitespaceFound := false + for _, c := range val { + if c < 32 || c > 126 { + return false + } else if !nonWhitespaceFound && c != 32 { + nonWhitespaceFound = true + } + } + return nonWhitespaceFound +} diff --git a/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner_test.go b/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner_test.go new file mode 100644 index 000000000000..da1ca82828e1 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/prune/metric_pruner_test.go @@ -0,0 +1,113 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package prune + +import ( + "testing" + + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" +) + +func TestMetricPrunerWithIndexableAttribute(t *testing.T) { + tests := []struct { + name string + val string + want bool + }{ + { + "testShouldDropChineseChar", + "漢", + true, + }, { + "testShouldDropSymbolChar", + "€, £, µ", + true, + }, { + "testShouldDropAllBlackSpace", + " ", + true, + }, + { + "testShouldDropAllTab", + " ", + true, + }, { + "testShouldKeepEnglishWord", + "abcdefg-", + false, + }, + } + + p := &MetricPruner{} + for _, tt := range tests { + attributes := pcommon.NewMap() + attributes.PutStr(common.MetricAttributeTelemetrySource, "UnitTest") + attributes.PutStr(common.CWMetricAttributeLocalService, tt.val) + t.Run(tt.name, func(t *testing.T) { + got, _ := p.ShouldBeDropped(attributes) + if got != tt.want { + t.Errorf("ShouldBeDropped() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetricPrunerWithNonIndexableAttribute(t *testing.T) { + tests := []struct { + name string + val string + want bool + }{ + { + "testShouldKeepChineseChar", + "漢", + false, + }, { + "testShouldKeepEnglishWord", + "abcdefg-", + false, + }, + } + + p := &MetricPruner{} + for _, tt := range tests { + attributes := pcommon.NewMap() + attributes.PutStr(common.MetricAttributeTelemetrySource, "UnitTest") + attributes.PutStr(common.AttributeEC2InstanceId, tt.val) + t.Run(tt.name, func(t *testing.T) { + got, _ := p.ShouldBeDropped(attributes) + if got != tt.want { + t.Errorf("ShouldBeDropped() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetricPrunerWithNoTelemetrySourceAttribute(t *testing.T) { + tests := []struct { + name string + val string + want bool + }{ + { + "testShouldDropValidChar", + "abc", + true, + }, + } + + p := &MetricPruner{} + for _, tt := range tests { + attributes := pcommon.NewMap() + attributes.PutStr(common.AttributeEC2InstanceId, tt.val) + t.Run(tt.name, func(t *testing.T) { + got, _ := p.ShouldBeDropped(attributes) + if got != tt.want { + t.Errorf("ShouldBeDropped() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver.go b/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver.go new file mode 100644 index 000000000000..3b858767fbc4 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver.go @@ -0,0 +1,135 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "context" + "errors" + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + appsignalsconfig "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +const ( + AttributeEnvironmentDefault = "default" + + AttributePlatformGeneric = "Generic" + AttributePlatformEC2 = "AWS::EC2" + AttributePlatformEKS = "AWS::EKS" + AttributePlatformECS = "AWS::ECS" + AttributePlatformK8S = "K8s" +) + +var GenericInheritedAttributes = map[string]string{ + semconv.AttributeDeploymentEnvironment: attr.AWSLocalEnvironment, + attr.ResourceDetectionHostName: common.AttributeHost, +} + +// DefaultInheritedAttributes is an allow-list that also renames attributes from the resource detection processor +var DefaultInheritedAttributes = map[string]string{ + semconv.AttributeDeploymentEnvironment: attr.AWSLocalEnvironment, + attr.ResourceDetectionASG: common.AttributeEC2AutoScalingGroup, + attr.ResourceDetectionHostId: common.AttributeEC2InstanceId, + attr.ResourceDetectionHostName: common.AttributeHost, +} + +type subResolver interface { + Process(attributes, resourceAttributes pcommon.Map) error + Stop(ctx context.Context) error +} + +type attributesResolver struct { + subResolvers []subResolver +} + +// create a new attributes resolver +func NewAttributesResolver(resolvers []appsignalsconfig.Resolver, logger *zap.Logger) *attributesResolver { + subResolvers := []subResolver{} + for _, resolver := range resolvers { + switch resolver.Platform { + case appsignalsconfig.PlatformEKS, appsignalsconfig.PlatformK8s: + subResolvers = append(subResolvers, getKubernetesResolver(resolver.Platform, resolver.Name, logger), newKubernetesResourceAttributesResolver(resolver.Platform, resolver.Name)) + case appsignalsconfig.PlatformEC2: + subResolvers = append(subResolvers, newResourceAttributesResolver(resolver.Platform, AttributePlatformEC2, DefaultInheritedAttributes)) + case appsignalsconfig.PlatformECS: + subResolvers = append(subResolvers, newECSResourceAttributesResolver(resolver.Platform, resolver.Name)) + default: + subResolvers = append(subResolvers, newResourceAttributesResolver(resolver.Platform, AttributePlatformGeneric, GenericInheritedAttributes)) + } + } + return &attributesResolver{ + subResolvers: subResolvers, + } +} + +// Process the attributes +func (r *attributesResolver) Process(attributes, resourceAttributes pcommon.Map, _ bool) error { + for _, subResolver := range r.subResolvers { + if err := subResolver.Process(attributes, resourceAttributes); err != nil { + return err + } + } + return nil +} + +func (r *attributesResolver) Stop(ctx context.Context) error { + var errs error + for _, subResolver := range r.subResolvers { + errs = errors.Join(errs, subResolver.Stop(ctx)) + } + return errs +} + +type resourceAttributesResolver struct { + defaultEnvPrefix string + platformType string + attributeMap map[string]string +} + +func newResourceAttributesResolver(defaultEnvPrefix, platformType string, attributeMap map[string]string) *resourceAttributesResolver { + return &resourceAttributesResolver{ + defaultEnvPrefix: defaultEnvPrefix, + platformType: platformType, + attributeMap: attributeMap, + } +} +func (h *resourceAttributesResolver) Process(attributes, resourceAttributes pcommon.Map) error { + for attrKey, mappingKey := range h.attributeMap { + if val, ok := resourceAttributes.Get(attrKey); ok { + attributes.PutStr(mappingKey, val.Str()) + } + } + attributes.PutStr(attr.AWSLocalEnvironment, getLocalEnvironment(attributes, resourceAttributes, h.defaultEnvPrefix)) + attributes.PutStr(common.AttributePlatformType, h.platformType) + return nil +} + +func getLocalEnvironment(attributes, resourceAttributes pcommon.Map, defaultEnvPrefix string) string { + if val, ok := attributes.Get(attr.AWSLocalEnvironment); ok { + return val.Str() + } + if val, found := resourceAttributes.Get(attr.AWSHostedInEnvironment); found { + return val.Str() + } + if defaultEnvPrefix == appsignalsconfig.PlatformEC2 { + if asgAttr, found := resourceAttributes.Get(attr.ResourceDetectionASG); found { + return generateLocalEnvironment(defaultEnvPrefix, asgAttr.Str()) + } + } + return generateLocalEnvironment(defaultEnvPrefix, AttributeEnvironmentDefault) +} + +func generateLocalEnvironment(platformCode, val string) string { + return fmt.Sprintf("%s:%s", platformCode, val) +} + +func (h *resourceAttributesResolver) Stop(ctx context.Context) error { + return nil +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver_test.go b/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver_test.go new file mode 100644 index 000000000000..0161c82bf2d1 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/attributesresolver_test.go @@ -0,0 +1,206 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +type MockSubResolver struct { + mock.Mock +} + +func (m *MockSubResolver) Process(attributes, resourceAttributes pcommon.Map) error { + args := m.Called(attributes, resourceAttributes) + return args.Error(0) +} + +func (m *MockSubResolver) Stop(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func TestResourceAttributesResolverWithNoConfiguredName(t *testing.T) { + tests := []struct { + name string + platformCode string + platformType string + resolver config.Resolver + }{ + { + "testOnGeneric", + config.PlatformGeneric, + AttributePlatformGeneric, + config.NewGenericResolver(""), + }, + { + "testOnEC2", + config.PlatformEC2, + AttributePlatformEC2, + config.NewEC2Resolver(""), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger, _ := zap.NewDevelopment() + attributesResolver := NewAttributesResolver([]config.Resolver{tt.resolver}, logger) + resolver := attributesResolver.subResolvers[0] + + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + + resolver.Process(attributes, resourceAttributes) + + attribute, ok := attributes.Get(common.AttributePlatformType) + assert.True(t, ok) + assert.Equal(t, tt.platformType, attribute.Str()) + + attribute, ok = attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, tt.platformCode+":default", attribute.Str()) + }) + } +} + +func TestResourceAttributesResolverWithOnEC2WithASG(t *testing.T) { + logger, _ := zap.NewDevelopment() + attributesResolver := NewAttributesResolver([]config.Resolver{config.NewEC2Resolver("")}, logger) + resolver := attributesResolver.subResolvers[0] + + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + resourceAttributes.PutStr(attr.ResourceDetectionASG, "my-asg") + + resolver.Process(attributes, resourceAttributes) + platformAttr, ok := attributes.Get(common.AttributePlatformType) + assert.True(t, ok) + assert.Equal(t, "AWS::EC2", platformAttr.Str()) + envAttr, ok := attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, "ec2:my-asg", envAttr.Str()) +} + +func TestResourceAttributesResolverWithHostname(t *testing.T) { + logger, _ := zap.NewDevelopment() + attributesResolver := NewAttributesResolver([]config.Resolver{config.NewGenericResolver("")}, logger) + resolver := attributesResolver.subResolvers[0] + + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + resourceAttributes.PutStr(attr.ResourceDetectionHostName, "hostname") + + resolver.Process(attributes, resourceAttributes) + envAttr, ok := attributes.Get(common.AttributeHost) + assert.True(t, ok) + assert.Equal(t, "hostname", envAttr.AsString()) +} + +func TestResourceAttributesResolverWithCustomEnvironment(t *testing.T) { + tests := []struct { + name string + platformCode string + resolver config.Resolver + }{ + { + "testOnGeneric", + config.PlatformGeneric, + config.NewGenericResolver(""), + }, + { + "testOnEC2", + config.PlatformEC2, + config.NewEC2Resolver(""), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger, _ := zap.NewDevelopment() + attributesResolver := NewAttributesResolver([]config.Resolver{tt.resolver}, logger) + resolver := attributesResolver.subResolvers[0] + + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + // insert default env + resourceAttributes.PutStr(attr.ResourceDetectionASG, "my-asg") + resourceAttributes.PutStr(semconv.AttributeAWSECSTaskARN, "arn:aws:ecs:us-west-1:123456789123:task/my-cluster/10838bed-421f-43ef-870a-f43feacbbb5b") + + // insert custom env + resourceAttributes.PutStr(attr.AWSHostedInEnvironment, "env1") + resolver.Process(attributes, resourceAttributes) + envAttr, ok := attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, "env1", envAttr.Str()) + + attributes = pcommon.NewMap() + resourceAttributes = pcommon.NewMap() + + resourceAttributes.PutStr(attr.AWSHostedInEnvironment, "error") + resourceAttributes.PutStr(semconv.AttributeDeploymentEnvironment, "env2") + resolver.Process(attributes, resourceAttributes) + envAttr, ok = attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, "env2", envAttr.Str()) + + attributes = pcommon.NewMap() + resourceAttributes = pcommon.NewMap() + + resourceAttributes.PutStr(semconv.AttributeDeploymentEnvironment, "env3") + resolver.Process(attributes, resourceAttributes) + envAttr, ok = attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, "env3", envAttr.Str()) + }) + } +} + +func TestAttributesResolver_Process(t *testing.T) { + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + + mockSubResolver1 := new(MockSubResolver) + mockSubResolver1.On("Process", attributes, resourceAttributes).Return(nil) + + mockSubResolver2 := new(MockSubResolver) + mockSubResolver2.On("Process", attributes, resourceAttributes).Return(errors.New("error")) + + r := &attributesResolver{ + subResolvers: []subResolver{mockSubResolver1, mockSubResolver2}, + } + + err := r.Process(attributes, resourceAttributes, true) + assert.Error(t, err) + mockSubResolver1.AssertExpectations(t) + mockSubResolver2.AssertExpectations(t) +} + +func TestAttributesResolver_Stop(t *testing.T) { + ctx := context.Background() + + mockSubResolver1 := new(MockSubResolver) + mockSubResolver1.On("Stop", ctx).Return(nil) + + mockSubResolver2 := new(MockSubResolver) + mockSubResolver2.On("Stop", ctx).Return(errors.New("error")) + + r := &attributesResolver{ + subResolvers: []subResolver{mockSubResolver1, mockSubResolver2}, + } + + err := r.Stop(ctx) + assert.Error(t, err) + mockSubResolver1.AssertExpectations(t) + mockSubResolver2.AssertExpectations(t) +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/ecs.go b/processor/awsapplicationsignalsprocessor/internal/resolver/ecs.go new file mode 100644 index 000000000000..644aa647312d --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/ecs.go @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "context" + "strings" + + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/ecsutil" +) + +type ecsResourceAttributesResolver struct { + resourceAttributesResolver + hostIn string +} + +func (e *ecsResourceAttributesResolver) Process(attributes, resourceAttributes pcommon.Map) error { + for attrKey, mappingKey := range e.attributeMap { + if val, ok := resourceAttributes.Get(attrKey); ok { + attributes.PutStr(mappingKey, val.Str()) + } + } + + clusterName, taskId := getECSResourcesFromResourceAttributes(resourceAttributes) + if clusterName == "" { + clusterName = ecsutil.GetECSUtilSingleton().Cluster + } + + attributes.PutStr(common.AttributePlatformType, e.platformType) + attributes.PutStr(attr.AWSLocalEnvironment, e.getLocalEnvironment(attributes, resourceAttributes, clusterName)) + attributes.PutStr(attr.AWSECSClusterName, clusterName) + if taskId != "" { + attributes.PutStr(attr.AWSECSTaskID, taskId) + } + return nil +} + +// getLocalEnvironment determines the environment based on the following priority: +// 1. aws.local.environment (from deployment.environment) +// 2. aws.hostedin.environment (deprecated soon) +// 3. hosted_in (user-specified) +// 4. aws.ecs.cluster.arn (auto-detected) +// 5. aws.ecs.task.arn (auto-detected) +// 6. Cluster name from CWA (auto-detected) +// 7. Hardcoded `default` +func (e *ecsResourceAttributesResolver) getLocalEnvironment(attributes pcommon.Map, resourceAttributes pcommon.Map, clusterName string) string { + if val, ok := attributes.Get(attr.AWSLocalEnvironment); ok { + return val.Str() + } + if val, found := resourceAttributes.Get(attr.AWSHostedInEnvironment); found { + return val.Str() + } + if e.hostIn != "" { + return generateLocalEnvironment(e.defaultEnvPrefix, e.hostIn) + } + if clusterName != "" { + return generateLocalEnvironment(e.defaultEnvPrefix, clusterName) + } + return generateLocalEnvironment(e.defaultEnvPrefix, AttributeEnvironmentDefault) +} + +func (e *ecsResourceAttributesResolver) Stop(ctx context.Context) error { + return nil +} + +func newECSResourceAttributesResolver(defaultEnvPrefix string, hostIn string) *ecsResourceAttributesResolver { + return &ecsResourceAttributesResolver{ + resourceAttributesResolver: resourceAttributesResolver{ + defaultEnvPrefix: defaultEnvPrefix, + platformType: AttributePlatformECS, + attributeMap: DefaultInheritedAttributes, + }, + hostIn: hostIn, + } +} + +func getECSResourcesFromResourceAttributes(resourceAttributes pcommon.Map) (clusterName, taskId string) { + if clusterAttr, ok := resourceAttributes.Get(semconv.AttributeAWSECSClusterARN); ok { + parts := strings.Split(clusterAttr.Str(), "/") + clusterName = parts[len(parts)-1] + } + if taskAttr, ok := resourceAttributes.Get(semconv.AttributeAWSECSTaskARN); ok { + parts := strings.SplitAfterN(taskAttr.Str(), ":task/", 2) + if len(parts) == 2 { + taskParts := strings.Split(parts[1], "/") + // New Task ARN format "task/cluster-name/task-id". + if len(taskParts) == 2 { + taskId = taskParts[1] + if clusterName == "" { + clusterName = taskParts[0] + } + } else if len(taskParts) == 1 { + // Legacy Task ARN format "task/task-id". + taskId = taskParts[0] + } + } + } + return +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/ecs_test.go b/processor/awsapplicationsignalsprocessor/internal/resolver/ecs_test.go new file mode 100644 index 000000000000..7f81cbe42c41 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/ecs_test.go @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + appsignalsconfig "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/ecsutil" +) + +func TestResourceAttributesResolverWithECSClusterName(t *testing.T) { + testCases := []struct { + name string + hostIn string + ecsTaskArn string + autoDetectedClusterName string + expectedClusterName string + expectedEnvironmentName string + }{ + { + name: "testECSClusterFromTaskArn", + hostIn: "", + ecsTaskArn: "arn:aws:ecs:us-west-1:123456789123:task/my-cluster/10838bed-421f-43ef-870a-f43feacbbb5b", + expectedClusterName: "my-cluster", + expectedEnvironmentName: "ecs:my-cluster", + }, + { + name: "testECSClusterFromHostIn", + hostIn: "host-in", + ecsTaskArn: "arn:aws:ecs:us-west-1:123456789123:task/my-cluster/10838bed-421f-43ef-870a-f43feacbbb5b", + expectedClusterName: "my-cluster", + expectedEnvironmentName: "ecs:host-in", + }, + { + name: "testECSClusterFromECSUtil", + hostIn: "", + ecsTaskArn: "", + autoDetectedClusterName: "my-cluster", + expectedClusterName: "my-cluster", + expectedEnvironmentName: "ecs:my-cluster", + }, + { + name: "testECSClusterDefault", + hostIn: "", + ecsTaskArn: "", + autoDetectedClusterName: "", + expectedClusterName: "", + expectedEnvironmentName: "ecs:default", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ecsutil.GetECSUtilSingleton().Cluster = tc.autoDetectedClusterName + resolver := newECSResourceAttributesResolver(appsignalsconfig.PlatformECS, tc.hostIn) + + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + resourceAttributes.PutStr(semconv.AttributeAWSECSTaskARN, tc.ecsTaskArn) + + resolver.Process(attributes, resourceAttributes) + + attribute, ok := attributes.Get(common.AttributePlatformType) + assert.True(t, ok) + assert.Equal(t, AttributePlatformECS, attribute.Str()) + + attribute, ok = attributes.Get(attr.AWSECSClusterName) + assert.True(t, ok) + assert.Equal(t, tc.expectedClusterName, attribute.Str()) + + attribute, ok = attributes.Get(attr.AWSLocalEnvironment) + assert.True(t, ok) + assert.Equal(t, tc.expectedEnvironmentName, attribute.Str()) + }) + } + ecsutil.GetECSUtilSingleton().Cluster = "" +} + +func TestGetClusterName(t *testing.T) { + resourceAttributes := pcommon.NewMap() + resourceAttributes.PutStr(semconv.AttributeAWSECSClusterARN, "arn:aws:ecs:us-west-2:123456789123:cluster/my-cluster") + clusterName, taskId := getECSResourcesFromResourceAttributes(resourceAttributes) + assert.Equal(t, "my-cluster", clusterName) + assert.Equal(t, "", taskId) + + resourceAttributes = pcommon.NewMap() + resourceAttributes.PutStr(semconv.AttributeAWSECSTaskARN, "arn:aws:ecs:us-west-1:123456789123:task/10838bedacbbb5b") + clusterName, taskId = getECSResourcesFromResourceAttributes(resourceAttributes) + assert.Equal(t, "", clusterName) + assert.Equal(t, "10838bedacbbb5b", taskId) + + resourceAttributes = pcommon.NewMap() + resourceAttributes.PutStr(semconv.AttributeAWSECSTaskARN, "arn:aws:ecs:us-west-1:123456789123:task/my-cluster/10838bedacbbb5b") + clusterName, taskId = getECSResourcesFromResourceAttributes(resourceAttributes) + assert.Equal(t, "my-cluster", clusterName) + assert.Equal(t, "10838bedacbbb5b", taskId) +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes.go b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes.go new file mode 100644 index 000000000000..0dbc812b43f9 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes.go @@ -0,0 +1,635 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "context" + "errors" + "fmt" + "math/rand" + "sync" + "time" + + mapset "github.com/deckarep/golang-set/v2" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +const ( + // Deletion delay adjustment: + // Previously, EKS resolver would instantly remove the IP to Service mapping when a pod was destroyed. + // This posed a problem because: + // 1. Metric data is aggregated and emitted every 1 minute. + // 2. If this aggregated metric data, which contains the IP of the now-destroyed pod, arrives + // at the EKS resolver after the IP records have already been deleted, the metric can't be processed correctly. + // + // To mitigate this issue, we've introduced a 2-minute deletion delay. This ensures that any + // metric data that arrives within those 2 minutes, containing the old IP, will still get mapped correctly to a service. + deletionDelay = 2 * time.Minute + + jitterKubernetesAPISeconds = 10 +) + +type kubernetesResolver struct { + logger *zap.Logger + clientset kubernetes.Interface + clusterName string + platformCode string + ipToPod *sync.Map + podToWorkloadAndNamespace *sync.Map + ipToServiceAndNamespace *sync.Map + serviceAndNamespaceToSelectors *sync.Map + workloadAndNamespaceToLabels *sync.Map + serviceToWorkload *sync.Map // computed from serviceAndNamespaceToSelectors and workloadAndNamespaceToLabels every 1 min + workloadPodCount map[string]int + safeStopCh *safeChannel // trace and metric processors share the same kubernetesResolver and might close the same channel separately +} + +// a safe channel which can be closed multiple times +type safeChannel struct { + sync.Mutex + + ch chan struct{} + closed bool +} + +func (sc *safeChannel) Close() { + sc.Lock() + defer sc.Unlock() + + if !sc.closed { + close(sc.ch) + sc.closed = true + } +} + +var ( + once sync.Once + instance *kubernetesResolver +) + +func jitterSleep(seconds int) { + jitter := time.Duration(rand.Intn(seconds)) * time.Second // nolint:gosec + time.Sleep(jitter) +} + +// Deleter represents a type that can delete a key from a map after a certain delay. +type Deleter interface { + DeleteWithDelay(m *sync.Map, key interface{}) +} + +// TimedDeleter deletes a key after a specified delay. +type TimedDeleter struct { + Delay time.Duration +} + +func (td *TimedDeleter) DeleteWithDelay(m *sync.Map, key interface{}) { + go func() { + time.Sleep(td.Delay) + m.Delete(key) + }() +} + +func (s *serviceWatcher) onAddOrUpdateService(service *corev1.Service) { + // service can also have an external IP (or ingress IP) that could be accessed + // this field can be either an IP address (in some edge case) or a hostname (see "EXTERNAL-IP" column in "k get svc" output) + // [ec2-user@ip-172-31-11-104 one-step]$ k get svc -A + // NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + // default pet-clinic-frontend ClusterIP 10.100.216.182 8080/TCP 108m + // default vets-service ClusterIP 10.100.62.167 8083/TCP 108m + // default visits-service ClusterIP 10.100.96.5 8082/TCP 108m + // ingress-nginx default-http-backend ClusterIP 10.100.11.231 80/TCP 108m + // ingress-nginx ingress-nginx LoadBalancer 10.100.154.5 aex7997ece08c435dbd2b912fd5aa5bd-5372117830.xxxxx.elb.amazonaws.com 80:32080/TCP,443:32081/TCP,9113:30410/TCP 108m + // kube-system kube-dns ClusterIP 10.100.0.10 + // + // we ignore such case for now and may need to consider it in the future + if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != corev1.ClusterIPNone { + s.ipToServiceAndNamespace.Store(service.Spec.ClusterIP, getServiceAndNamespace(service)) + } + labelSet := mapset.NewSet[string]() + for key, value := range service.Spec.Selector { + labelSet.Add(key + "=" + value) + } + if labelSet.Cardinality() > 0 { + s.serviceAndNamespaceToSelectors.Store(getServiceAndNamespace(service), labelSet) + } +} + +func (s *serviceWatcher) onDeleteService(service *corev1.Service, deleter Deleter) { + if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != corev1.ClusterIPNone { + deleter.DeleteWithDelay(s.ipToServiceAndNamespace, service.Spec.ClusterIP) + } + deleter.DeleteWithDelay(s.serviceAndNamespaceToSelectors, getServiceAndNamespace(service)) +} + +func (p *podWatcher) removeHostNetworkRecords(pod *corev1.Pod) { + for _, port := range getHostNetworkPorts(pod) { + p.deleter.DeleteWithDelay(p.ipToPod, pod.Status.HostIP+":"+port) + } +} + +func (p *podWatcher) handlePodAdd(pod *corev1.Pod) { + if pod.Spec.HostNetwork && pod.Status.HostIP != "" { + for _, port := range getHostNetworkPorts(pod) { + p.ipToPod.Store(pod.Status.HostIP+":"+port, pod.Name) + } + } + if pod.Status.PodIP != "" { + p.ipToPod.Store(pod.Status.PodIP, pod.Name) + } +} + +func (p *podWatcher) handlePodUpdate(newPod *corev1.Pod, oldPod *corev1.Pod) { + // HostNetwork is an immutable field + if newPod.Spec.HostNetwork && oldPod.Status.HostIP != newPod.Status.HostIP { + if oldPod.Status.HostIP != "" { + p.logger.Debug("deleting host ip from cache", zap.String("hostNetwork", oldPod.Status.HostIP)) + p.removeHostNetworkRecords(oldPod) + } + if newPod.Status.HostIP != "" { + for _, port := range getHostNetworkPorts(newPod) { + p.ipToPod.Store(newPod.Status.HostIP+":"+port, newPod.Name) + } + } + } + if oldPod.Status.PodIP != newPod.Status.PodIP { + if oldPod.Status.PodIP != "" { + p.logger.Debug("deleting pod ip from cache", zap.String("podNetwork", oldPod.Status.PodIP)) + p.deleter.DeleteWithDelay(p.ipToPod, oldPod.Status.PodIP) + } + if newPod.Status.PodIP != "" { + p.ipToPod.Store(newPod.Status.PodIP, newPod.Name) + } + } +} + +func (p *podWatcher) onAddOrUpdatePod(pod, oldPod *corev1.Pod) { + if oldPod == nil { + p.handlePodAdd(pod) + } else { + p.handlePodUpdate(pod, oldPod) + } + + workloadAndNamespace := getWorkloadAndNamespace(pod) + + if workloadAndNamespace != "" { + p.podToWorkloadAndNamespace.Store(pod.Name, workloadAndNamespace) + podLabels := mapset.NewSet[string]() + for key, value := range pod.ObjectMeta.Labels { + podLabels.Add(key + "=" + value) + } + if podLabels.Cardinality() > 0 { + p.workloadAndNamespaceToLabels.Store(workloadAndNamespace, podLabels) + } + if oldPod == nil { + p.workloadPodCount[workloadAndNamespace]++ + p.logger.Debug("Added pod", zap.String("pod", pod.Name), zap.String("workload", workloadAndNamespace), zap.Int("count", p.workloadPodCount[workloadAndNamespace])) + } + } +} + +func (p *podWatcher) onDeletePod(obj interface{}) { + pod := obj.(*corev1.Pod) + if pod.Spec.HostNetwork && pod.Status.HostIP != "" { + p.logger.Debug("deleting host ip from cache", zap.String("hostNetwork", pod.Status.HostIP)) + p.removeHostNetworkRecords(pod) + } + if pod.Status.PodIP != "" { + p.logger.Debug("deleting pod ip from cache", zap.String("podNetwork", pod.Status.PodIP)) + p.deleter.DeleteWithDelay(p.ipToPod, pod.Status.PodIP) + } + + if workloadKey, ok := p.podToWorkloadAndNamespace.Load(pod.Name); ok { + workloadAndNamespace := workloadKey.(string) + p.workloadPodCount[workloadAndNamespace]-- + p.logger.Debug("decrementing pod count", zap.String("workload", workloadAndNamespace), zap.Int("podCount", p.workloadPodCount[workloadAndNamespace])) + if p.workloadPodCount[workloadAndNamespace] == 0 { + p.deleter.DeleteWithDelay(p.workloadAndNamespaceToLabels, workloadAndNamespace) + } + } else { + p.logger.Error("failed to load pod workloadKey", zap.String("pod", pod.Name)) + } + p.deleter.DeleteWithDelay(p.podToWorkloadAndNamespace, pod.Name) +} + +type podWatcher struct { + ipToPod *sync.Map + podToWorkloadAndNamespace *sync.Map + workloadAndNamespaceToLabels *sync.Map + workloadPodCount map[string]int + logger *zap.Logger + informer cache.SharedIndexInformer + deleter Deleter +} + +func newPodWatcher(logger *zap.Logger, informer cache.SharedIndexInformer, deleter Deleter) *podWatcher { + return &podWatcher{ + ipToPod: &sync.Map{}, + podToWorkloadAndNamespace: &sync.Map{}, + workloadAndNamespaceToLabels: &sync.Map{}, + workloadPodCount: make(map[string]int), + logger: logger, + informer: informer, + deleter: deleter, + } +} + +func (p *podWatcher) run(stopCh chan struct{}) { + p.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + pod := obj.(*corev1.Pod) + p.logger.Debug("list and watch for pod: ADD " + pod.Name) + p.onAddOrUpdatePod(pod, nil) + }, + UpdateFunc: func(oldObj, newObj interface{}) { + pod := newObj.(*corev1.Pod) + oldPod := oldObj.(*corev1.Pod) + p.logger.Debug("list and watch for pods: UPDATE " + pod.Name) + p.onAddOrUpdatePod(pod, oldPod) + }, + DeleteFunc: func(obj interface{}) { + pod := obj.(*corev1.Pod) + p.logger.Debug("list and watch for pods: DELETE " + pod.Name) + p.onDeletePod(obj) + }, + }) + + go p.informer.Run(stopCh) + +} + +func (p *podWatcher) waitForCacheSync(stopCh chan struct{}) { + if !cache.WaitForNamedCacheSync("podWatcher", stopCh, p.informer.HasSynced) { + p.logger.Fatal("timed out waiting for kubernetes pod watcher caches to sync") + } + + p.logger.Info("podWatcher: Cache synced") +} + +type serviceWatcher struct { + ipToServiceAndNamespace *sync.Map + serviceAndNamespaceToSelectors *sync.Map + logger *zap.Logger + informer cache.SharedIndexInformer + deleter Deleter +} + +func newServiceWatcher(logger *zap.Logger, informer cache.SharedIndexInformer, deleter Deleter) *serviceWatcher { + return &serviceWatcher{ + ipToServiceAndNamespace: &sync.Map{}, + serviceAndNamespaceToSelectors: &sync.Map{}, + logger: logger, + informer: informer, + deleter: deleter, + } +} + +func (s *serviceWatcher) Run(stopCh chan struct{}) { + s.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + service := obj.(*corev1.Service) + s.logger.Debug("list and watch for services: ADD " + service.Name) + s.onAddOrUpdateService(service) + }, + UpdateFunc: func(oldObj, newObj interface{}) { + service := newObj.(*corev1.Service) + s.logger.Debug("list and watch for services: UPDATE " + service.Name) + s.onAddOrUpdateService(service) + }, + DeleteFunc: func(obj interface{}) { + service := obj.(*corev1.Service) + s.logger.Debug("list and watch for services: DELETE " + service.Name) + s.onDeleteService(service, s.deleter) + }, + }) + go s.informer.Run(stopCh) +} + +func (s *serviceWatcher) waitForCacheSync(stopCh chan struct{}) { + if !cache.WaitForNamedCacheSync("serviceWatcher", stopCh, s.informer.HasSynced) { + s.logger.Fatal("timed out waiting for kubernetes service watcher caches to sync") + } + + s.logger.Info("serviceWatcher: Cache synced") +} + +type serviceToWorkloadMapper struct { + serviceAndNamespaceToSelectors *sync.Map + workloadAndNamespaceToLabels *sync.Map + serviceToWorkload *sync.Map + logger *zap.Logger + deleter Deleter +} + +func newServiceToWorkloadMapper(serviceAndNamespaceToSelectors, workloadAndNamespaceToLabels, serviceToWorkload *sync.Map, logger *zap.Logger, deleter Deleter) *serviceToWorkloadMapper { + return &serviceToWorkloadMapper{ + serviceAndNamespaceToSelectors: serviceAndNamespaceToSelectors, + workloadAndNamespaceToLabels: workloadAndNamespaceToLabels, + serviceToWorkload: serviceToWorkload, + logger: logger, + deleter: deleter, + } +} + +func (m *serviceToWorkloadMapper) mapServiceToWorkload() { + m.logger.Debug("Map service to workload at:", zap.Time("time", time.Now())) + + m.serviceAndNamespaceToSelectors.Range(func(key, value interface{}) bool { + var workloads []string + serviceAndNamespace := key.(string) + _, serviceNamespace := extractResourceAndNamespace(serviceAndNamespace) + serviceLabels := value.(mapset.Set[string]) + + m.workloadAndNamespaceToLabels.Range(func(workloadKey, labelsValue interface{}) bool { + labels := labelsValue.(mapset.Set[string]) + workloadAndNamespace := workloadKey.(string) + _, workloadNamespace := extractResourceAndNamespace(workloadAndNamespace) + if workloadNamespace == serviceNamespace && workloadNamespace != "" && serviceLabels.IsSubset(labels) { + m.logger.Debug("Found workload for service", zap.String("service", serviceAndNamespace), zap.String("workload", workloadAndNamespace)) + workloads = append(workloads, workloadAndNamespace) + } + + return true + }) + + if len(workloads) > 1 { + m.logger.Info("Multiple workloads found for service. You will get unexpected results.", zap.String("service", serviceAndNamespace), zap.Strings("workloads", workloads)) + } else if len(workloads) == 1 { + m.serviceToWorkload.Store(serviceAndNamespace, workloads[0]) + } else { + m.logger.Debug("No workload found for service", zap.String("service", serviceAndNamespace)) + m.deleter.DeleteWithDelay(m.serviceToWorkload, serviceAndNamespace) + } + return true + }) +} + +func (m *serviceToWorkloadMapper) Start(stopCh chan struct{}) { + // do the first mapping immediately + m.mapServiceToWorkload() + m.logger.Debug("First-time map service to workload at:", zap.Time("time", time.Now())) + + go func() { + for { + select { + case <-stopCh: + return + case <-time.After(time.Minute + 30*time.Second): + m.mapServiceToWorkload() + m.logger.Debug("Map service to workload at:", zap.Time("time", time.Now())) + } + } + }() +} + +// minimizePod removes fields that could contain large objects, and retain essential +// fields needed for IP/name translation. The following fields must be kept: +// - ObjectMeta: Namespace, Name, Labels, OwnerReference +// - Spec: HostNetwork, ContainerPorts +// - Status: PodIP/s, HostIP/s +func minimizePod(obj interface{}) (interface{}, error) { + if pod, ok := obj.(*corev1.Pod); ok { + pod.Annotations = nil + pod.Finalizers = nil + pod.ManagedFields = nil + + pod.Spec.Volumes = nil + pod.Spec.InitContainers = nil + pod.Spec.EphemeralContainers = nil + pod.Spec.ImagePullSecrets = nil + pod.Spec.HostAliases = nil + pod.Spec.SchedulingGates = nil + pod.Spec.ResourceClaims = nil + pod.Spec.Tolerations = nil + pod.Spec.Affinity = nil + + pod.Status.InitContainerStatuses = nil + pod.Status.ContainerStatuses = nil + pod.Status.EphemeralContainerStatuses = nil + + for i := 0; i < len(pod.Spec.Containers); i++ { + c := &pod.Spec.Containers[i] + c.Image = "" + c.Command = nil + c.Args = nil + c.EnvFrom = nil + c.Env = nil + c.Resources = corev1.ResourceRequirements{} + c.VolumeMounts = nil + c.VolumeDevices = nil + c.SecurityContext = nil + } + } + return obj, nil +} + +// minimizeService removes fields that could contain large objects, and retain essential +// fields needed for IP/name translation. The following fields must be kept: +// - ObjectMeta: Namespace, Name +// - Spec: Selectors, ClusterIP +func minimizeService(obj interface{}) (interface{}, error) { + if svc, ok := obj.(*corev1.Service); ok { + svc.Annotations = nil + svc.Finalizers = nil + svc.ManagedFields = nil + + svc.Spec.LoadBalancerSourceRanges = nil + svc.Spec.SessionAffinityConfig = nil + svc.Spec.IPFamilies = nil + svc.Spec.IPFamilyPolicy = nil + svc.Spec.InternalTrafficPolicy = nil + svc.Spec.InternalTrafficPolicy = nil + + svc.Status.Conditions = nil + } + return obj, nil +} + +func getKubernetesResolver(platformCode, clusterName string, logger *zap.Logger) subResolver { + once.Do(func() { + config, err := clientcmd.BuildConfigFromFlags("", "") + if err != nil { + logger.Fatal("Failed to create config", zap.Error(err)) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + logger.Fatal("Failed to create kubernetes client", zap.Error(err)) + } + + // jitter calls to the kubernetes api + jitterSleep(jitterKubernetesAPISeconds) + + sharedInformerFactory := informers.NewSharedInformerFactory(clientset, 0) + podInformer := sharedInformerFactory.Core().V1().Pods().Informer() + err = podInformer.SetTransform(minimizePod) + if err != nil { + logger.Error("failed to minimize Pod objects", zap.Error(err)) + } + serviceInformer := sharedInformerFactory.Core().V1().Services().Informer() + err = serviceInformer.SetTransform(minimizeService) + if err != nil { + logger.Error("failed to minimize Service objects", zap.Error(err)) + } + + timedDeleter := &TimedDeleter{Delay: deletionDelay} + poWatcher := newPodWatcher(logger, podInformer, timedDeleter) + svcWatcher := newServiceWatcher(logger, serviceInformer, timedDeleter) + + safeStopCh := &safeChannel{ch: make(chan struct{}), closed: false} + // initialize the pod and service watchers for the cluster + poWatcher.run(safeStopCh.ch) + svcWatcher.Run(safeStopCh.ch) + // wait for caches to sync (for once) so that clients knows about the pods and services in the cluster + poWatcher.waitForCacheSync(safeStopCh.ch) + svcWatcher.waitForCacheSync(safeStopCh.ch) + + serviceToWorkload := &sync.Map{} + svcToWorkloadMapper := newServiceToWorkloadMapper(svcWatcher.serviceAndNamespaceToSelectors, poWatcher.workloadAndNamespaceToLabels, serviceToWorkload, logger, timedDeleter) + svcToWorkloadMapper.Start(safeStopCh.ch) + + instance = &kubernetesResolver{ + logger: logger, + clientset: clientset, + clusterName: clusterName, + platformCode: platformCode, + ipToServiceAndNamespace: svcWatcher.ipToServiceAndNamespace, + serviceAndNamespaceToSelectors: svcWatcher.serviceAndNamespaceToSelectors, + ipToPod: poWatcher.ipToPod, + podToWorkloadAndNamespace: poWatcher.podToWorkloadAndNamespace, + workloadAndNamespaceToLabels: poWatcher.workloadAndNamespaceToLabels, + serviceToWorkload: serviceToWorkload, + workloadPodCount: poWatcher.workloadPodCount, + safeStopCh: safeStopCh, + } + }) + + return instance +} + +func (e *kubernetesResolver) Stop(_ context.Context) error { + e.safeStopCh.Close() + return nil +} + +// add a method to kubernetesResolver +func (e *kubernetesResolver) getWorkloadAndNamespaceByIP(ip string) (string, string, error) { + var workload, namespace string + if podKey, ok := e.ipToPod.Load(ip); ok { + pod := podKey.(string) + if workloadKey, ok := e.podToWorkloadAndNamespace.Load(pod); ok { + workload, namespace = extractResourceAndNamespace(workloadKey.(string)) + return workload, namespace, nil + } + } + + if serviceKey, ok := e.ipToServiceAndNamespace.Load(ip); ok { + serviceAndNamespace := serviceKey.(string) + if workloadKey, ok := e.serviceToWorkload.Load(serviceAndNamespace); ok { + workload, namespace = extractResourceAndNamespace(workloadKey.(string)) + return workload, namespace, nil + } + } + + return "", "", errors.New("no kubernetes workload found for ip: " + ip) +} + +func (e *kubernetesResolver) Process(attributes, resourceAttributes pcommon.Map) error { + var namespace string + if value, ok := attributes.Get(attr.AWSRemoteService); ok { + valueStr := value.AsString() + ipStr := "" + if ip, _, ok := extractIPPort(valueStr); ok { + if workload, ns, err := e.getWorkloadAndNamespaceByIP(valueStr); err == nil { + attributes.PutStr(attr.AWSRemoteService, workload) + namespace = ns + } else { + ipStr = ip + } + } else if isIP(valueStr) { + ipStr = valueStr + } + + if ipStr != "" { + if workload, ns, err := e.getWorkloadAndNamespaceByIP(ipStr); err == nil { + attributes.PutStr(attr.AWSRemoteService, workload) + namespace = ns + } else { + e.logger.Debug("failed to Process ip", zap.String("ip", ipStr), zap.Error(err)) + } + } + } + + if _, ok := attributes.Get(attr.AWSRemoteEnvironment); !ok { + if namespace != "" { + attributes.PutStr(attr.AWSRemoteEnvironment, fmt.Sprintf("%s:%s/%s", e.platformCode, e.clusterName, namespace)) + } + } + + return nil +} + +type kubernetesResourceAttributesResolver struct { + platformCode string + clusterName string + attributeMap map[string]string +} + +func newKubernetesResourceAttributesResolver(platformCode, clusterName string) *kubernetesResourceAttributesResolver { + return &kubernetesResourceAttributesResolver{ + platformCode: platformCode, + clusterName: clusterName, + attributeMap: DefaultInheritedAttributes, + } +} +func (h *kubernetesResourceAttributesResolver) Process(attributes, resourceAttributes pcommon.Map) error { + for attrKey, mappingKey := range h.attributeMap { + if val, ok := resourceAttributes.Get(attrKey); ok { + attributes.PutStr(mappingKey, val.AsString()) + } + } + if h.platformCode == config.PlatformEKS { + attributes.PutStr(common.AttributePlatformType, AttributePlatformEKS) + attributes.PutStr(common.AttributeEKSClusterName, h.clusterName) + } else { + attributes.PutStr(common.AttributePlatformType, AttributePlatformK8S) + attributes.PutStr(common.AttributeK8SClusterName, h.clusterName) + } + var namespace string + if nsAttr, ok := resourceAttributes.Get(semconv.AttributeK8SNamespaceName); ok { + namespace = nsAttr.Str() + } else { + namespace = "UnknownNamespace" + } + + if val, ok := attributes.Get(attr.AWSLocalEnvironment); !ok { + env := generateLocalEnvironment(h.platformCode, h.clusterName+"/"+namespace) + attributes.PutStr(attr.AWSLocalEnvironment, env) + } else { + attributes.PutStr(attr.AWSLocalEnvironment, val.Str()) + } + + attributes.PutStr(common.AttributeK8SNamespace, namespace) + //The application log group in Container Insights is a fixed pattern: + // "/aws/containerinsights/{Cluster_Name}/application" + // See https://github.com/aws/amazon-cloudwatch-agent-operator/blob/fe144bb02d7b1930715aa3ea32e57a5ff13406aa/helm/templates/fluent-bit-configmap.yaml#L82 + logGroupName := "/aws/containerinsights/" + h.clusterName + "/application" + resourceAttributes.PutStr(semconv.AttributeAWSLogGroupNames, logGroupName) + + return nil +} + +func (h *kubernetesResourceAttributesResolver) Stop(ctx context.Context) error { + return nil +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_test.go b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_test.go new file mode 100644 index 000000000000..f8e909afe890 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_test.go @@ -0,0 +1,1287 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "context" + "fmt" + "strings" + "sync" + "testing" + "time" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.22.0" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +// MockDeleter deletes a key immediately, useful for testing. +type MockDeleter struct{} + +func (md *MockDeleter) DeleteWithDelay(m *sync.Map, key interface{}) { + m.Delete(key) +} + +var mockDeleter = &MockDeleter{} + +// TestAttachNamespace function +func TestAttachNamespace(t *testing.T) { + result := attachNamespace("testResource", "testNamespace") + if result != "testResource@testNamespace" { + t.Errorf("attachNamespace was incorrect, got: %s, want: %s.", result, "testResource@testNamespace") + } +} + +// TestGetServiceAndNamespace function +func TestGetServiceAndNamespace(t *testing.T) { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testService", + Namespace: "testNamespace", + }, + } + result := getServiceAndNamespace(service) + if result != "testService@testNamespace" { + t.Errorf("getServiceAndNamespace was incorrect, got: %s, want: %s.", result, "testService@testNamespace") + } +} + +// TestExtractResourceAndNamespace function +func TestExtractResourceAndNamespace(t *testing.T) { + // Test normal case + name, namespace := extractResourceAndNamespace("testService@testNamespace") + if name != "testService" || namespace != "testNamespace" { + t.Errorf("extractResourceAndNamespace was incorrect, got: %s and %s, want: %s and %s.", name, namespace, "testService", "testNamespace") + } + + // Test invalid case + name, namespace = extractResourceAndNamespace("invalid") + if name != "" || namespace != "" { + t.Errorf("extractResourceAndNamespace was incorrect, got: %s and %s, want: %s and %s.", name, namespace, "", "") + } +} + +func TestExtractWorkloadNameFromRS(t *testing.T) { + testCases := []struct { + name string + replicaSetName string + want string + shouldErr bool + }{ + { + name: "Valid ReplicaSet Name", + replicaSetName: "my-deployment-5859ffc7ff", + want: "my-deployment", + shouldErr: false, + }, + { + name: "Invalid ReplicaSet Name - No Hyphen", + replicaSetName: "mydeployment5859ffc7ff", + want: "", + shouldErr: true, + }, + { + name: "Invalid ReplicaSet Name - Less Than 10 Suffix Characters", + replicaSetName: "my-deployment-bc2", + want: "", + shouldErr: true, + }, + { + name: "Invalid ReplicaSet Name - More Than 10 Suffix Characters", + replicaSetName: "my-deployment-5859ffc7ffx", + want: "", + shouldErr: true, + }, + { + name: "Invalid ReplicaSet Name - Invalid Characters in Suffix", + replicaSetName: "my-deployment-aeiou12345", + want: "", + shouldErr: true, + }, + { + name: "Invalid ReplicaSet Name - Empty String", + replicaSetName: "", + want: "", + shouldErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := extractWorkloadNameFromRS(tc.replicaSetName) + + if (err != nil) != tc.shouldErr { + t.Errorf("extractWorkloadNameFromRS() error = %v, wantErr %v", err, tc.shouldErr) + return + } + + if got != tc.want { + t.Errorf("extractWorkloadNameFromRS() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestExtractWorkloadNameFromPodName(t *testing.T) { + testCases := []struct { + name string + podName string + want string + shouldErr bool + }{ + { + name: "Valid Pod Name", + podName: "my-replicaset-bc24f", + want: "my-replicaset", + shouldErr: false, + }, + { + name: "Invalid Pod Name - No Hyphen", + podName: "myreplicasetbc24f", + want: "", + shouldErr: true, + }, + { + name: "Invalid Pod Name - Less Than 5 Suffix Characters", + podName: "my-replicaset-bc2", + want: "", + shouldErr: true, + }, + { + name: "Invalid Pod Name - More Than 5 Suffix Characters", + podName: "my-replicaset-bc24f5", + want: "", + shouldErr: true, + }, + { + name: "Invalid Pod Name - Empty String", + podName: "", + want: "", + shouldErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := extractWorkloadNameFromPodName(tc.podName) + + if (err != nil) != tc.shouldErr { + t.Errorf("extractWorkloadNameFromPodName() error = %v, wantErr %v", err, tc.shouldErr) + return + } + + if got != tc.want { + t.Errorf("extractWorkloadNameFromPodName() = %v, want %v", got, tc.want) + } + }) + } +} + +// TestGetWorkloadAndNamespace function +func TestGetWorkloadAndNamespace(t *testing.T) { + // Test ReplicaSet case + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-5d68bc5f49", + }, + }, + }, + } + result := getWorkloadAndNamespace(pod) + if result != "testDeployment@testNamespace" { + t.Errorf("getDeploymentAndNamespace was incorrect, got: %s, want: %s.", result, "testDeployment@testNamespace") + } + + // Test StatefulSet case + pod.ObjectMeta.OwnerReferences[0].Kind = "StatefulSet" + pod.ObjectMeta.OwnerReferences[0].Name = "testStatefulSet" + result = getWorkloadAndNamespace(pod) + if result != "testStatefulSet@testNamespace" { + t.Errorf("getWorkloadAndNamespace was incorrect, got: %s, want: %s.", result, "testStatefulSet@testNamespace") + } + + // Test Other case + pod.ObjectMeta.OwnerReferences[0].Kind = "Other" + pod.ObjectMeta.OwnerReferences[0].Name = "testOther" + result = getWorkloadAndNamespace(pod) + if result != "" { + t.Errorf("getWorkloadAndNamespace was incorrect, got: %s, want: %s.", result, "") + } + + // Test no OwnerReferences case + pod.ObjectMeta.OwnerReferences = nil + result = getWorkloadAndNamespace(pod) + if result != "" { + t.Errorf("getWorkloadAndNamespace was incorrect, got: %s, want: %s.", result, "") + } +} + +func TestServiceToWorkloadMapper_MapServiceToWorkload(t *testing.T) { + logger, _ := zap.NewDevelopment() + + serviceAndNamespaceToSelectors := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + serviceToWorkload := &sync.Map{} + + serviceAndNamespaceToSelectors.Store("service1@namespace1", mapset.NewSet("label1=value1", "label2=value2")) + workloadAndNamespaceToLabels.Store("deployment1@namespace1", mapset.NewSet("label1=value1", "label2=value2", "label3=value3")) + + mapper := newServiceToWorkloadMapper(serviceAndNamespaceToSelectors, workloadAndNamespaceToLabels, serviceToWorkload, logger, mockDeleter) + mapper.mapServiceToWorkload() + + if _, ok := serviceToWorkload.Load("service1@namespace1"); !ok { + t.Errorf("Expected service1@namespace1 to be mapped to a workload, but it was not") + } +} + +func TestServiceToWorkloadMapper_MapServiceToWorkload_NoWorkload(t *testing.T) { + logger, _ := zap.NewDevelopment() + + serviceAndNamespaceToSelectors := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + serviceToWorkload := &sync.Map{} + + // Add a service with no matching workload + serviceAndNamespace := "service@namespace" + serviceAndNamespaceToSelectors.Store(serviceAndNamespace, mapset.NewSet("label1=value1")) + serviceToWorkload.Store(serviceAndNamespace, "workload@namespace") + + mapper := newServiceToWorkloadMapper(serviceAndNamespaceToSelectors, workloadAndNamespaceToLabels, serviceToWorkload, logger, mockDeleter) + mapper.mapServiceToWorkload() + + // Check that the service was deleted from serviceToWorkload + if _, ok := serviceToWorkload.Load(serviceAndNamespace); ok { + t.Errorf("Service was not deleted from serviceToWorkload") + } +} + +func TestServiceToWorkloadMapper_MapServiceToWorkload_MultipleWorkloads(t *testing.T) { + logger, _ := zap.NewDevelopment() + + serviceAndNamespaceToSelectors := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + serviceToWorkload := &sync.Map{} + + serviceAndNamespace := "service@namespace" + serviceAndNamespaceToSelectors.Store(serviceAndNamespace, mapset.NewSet("label1=value1", "label2=value2")) + + // Add two workloads with matching labels to the service + workloadAndNamespaceToLabels.Store("workload1@namespace", mapset.NewSet("label1=value1", "label2=value2", "label3=value3")) + workloadAndNamespaceToLabels.Store("workload2@namespace", mapset.NewSet("label1=value1", "label2=value2", "label4=value4")) + + mapper := newServiceToWorkloadMapper(serviceAndNamespaceToSelectors, workloadAndNamespaceToLabels, serviceToWorkload, logger, mockDeleter) + mapper.mapServiceToWorkload() + + // Check that the service does not map to any workload + if _, ok := serviceToWorkload.Load(serviceAndNamespace); ok { + t.Errorf("Unexpected mapping of service to multiple workloads") + } +} + +func TestMapServiceToWorkload_StopsWhenSignaled(t *testing.T) { + logger, _ := zap.NewDevelopment() + + serviceAndNamespaceToSelectors := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + serviceToWorkload := &sync.Map{} + + stopchan := make(chan struct{}) + + // Signal the stopchan to stop after 100 milliseconds + time.AfterFunc(100*time.Millisecond, func() { + close(stopchan) + }) + + mapper := newServiceToWorkloadMapper(serviceAndNamespaceToSelectors, workloadAndNamespaceToLabels, serviceToWorkload, logger, mockDeleter) + + start := time.Now() + mapper.Start(stopchan) + duration := time.Since(start) + + // Check that the function stopped in a reasonable time after the stop signal + if duration > 200*time.Millisecond { + t.Errorf("mapServiceToWorkload did not stop in a reasonable time after the stop signal, duration: %v", duration) + } +} + +func TestOnAddOrUpdateService(t *testing.T) { + // Create a fake service + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myservice", + Namespace: "mynamespace", + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "1.2.3.4", + Selector: map[string]string{ + "app": "myapp", + }, + }, + } + + // Create the maps + ipToServiceAndNamespace := &sync.Map{} + serviceAndNamespaceToSelectors := &sync.Map{} + + // Call the function + svcWatcher := newServiceWatcherForTesting(ipToServiceAndNamespace, serviceAndNamespaceToSelectors) + svcWatcher.onAddOrUpdateService(service) + + // Check that the maps contain the expected entries + if _, ok := ipToServiceAndNamespace.Load("1.2.3.4"); !ok { + t.Errorf("ipToServiceAndNamespace does not contain the service IP") + } + if _, ok := serviceAndNamespaceToSelectors.Load("myservice@mynamespace"); !ok { + t.Errorf("serviceAndNamespaceToSelectors does not contain the service") + } +} + +func TestOnDeleteService(t *testing.T) { + // Create a fake service + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myservice", + Namespace: "mynamespace", + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "1.2.3.4", + Selector: map[string]string{ + "app": "myapp", + }, + }, + } + + // Create the maps and add the service to them + ipToServiceAndNamespace := &sync.Map{} + ipToServiceAndNamespace.Store("1.2.3.4", "myservice@mynamespace") + serviceAndNamespaceToSelectors := &sync.Map{} + serviceAndNamespaceToSelectors.Store("myservice@mynamespace", mapset.NewSet("app=myapp")) + + // Call the function + svcWatcher := newServiceWatcherForTesting(ipToServiceAndNamespace, serviceAndNamespaceToSelectors) + svcWatcher.onDeleteService(service, mockDeleter) + + // Check that the maps do not contain the service + if _, ok := ipToServiceAndNamespace.Load("1.2.3.4"); ok { + t.Errorf("ipToServiceAndNamespace still contains the service IP") + } + if _, ok := serviceAndNamespaceToSelectors.Load("myservice@mynamespace"); ok { + t.Errorf("serviceAndNamespaceToSelectors still contains the service") + } +} + +func TestOnAddOrUpdatePod(t *testing.T) { + t.Run("pod with both PodIP and HostIP", func(t *testing.T) { + ipToPod := &sync.Map{} + podToWorkloadAndNamespace := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + workloadPodCount := map[string]int{} + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-598b89cd8d", + }, + }, + }, + Status: corev1.PodStatus{ + PodIP: "1.2.3.4", + HostIP: "5.6.7.8", + }, + } + + poWatcher := newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels, workloadPodCount) + poWatcher.onAddOrUpdatePod(pod, nil) + + // Test the mappings in ipToPod + if podName, _ := ipToPod.Load("1.2.3.4"); podName != "testPod" { + t.Errorf("ipToPod was incorrect, got: %s, want: %s.", podName, "testPod") + } + + // Test the mapping in podToWorkloadAndNamespace + if depAndNamespace, _ := podToWorkloadAndNamespace.Load("testPod"); depAndNamespace != "testDeployment@testNamespace" { + t.Errorf("podToWorkloadAndNamespace was incorrect, got: %s, want: %s.", depAndNamespace, "testDeployment@testNamespace") + } + + // Test the count in workloadPodCount + if count := workloadPodCount["testDeployment@testNamespace"]; count != 1 { + t.Errorf("workloadPodCount was incorrect, got: %d, want: %d.", count, 1) + } + }) + + t.Run("pod with only HostIP", func(t *testing.T) { + ipToPod := &sync.Map{} + podToWorkloadAndNamespace := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + workloadPodCount := map[string]int{} + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-7b74958fb8", + }, + }, + }, + Status: corev1.PodStatus{ + HostIP: "5.6.7.8", + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + HostPort: int32(8080), + }, + }, + }, + }, + }, + } + + poWatcher := newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels, workloadPodCount) + poWatcher.onAddOrUpdatePod(pod, nil) + + // Test the mappings in ipToPod + if podName, _ := ipToPod.Load("5.6.7.8:8080"); podName != "testPod" { + t.Errorf("ipToPod was incorrect, got: %s, want: %s.", podName, "testPod") + } + + // Test the mapping in podToWorkloadAndNamespace + if depAndNamespace, _ := podToWorkloadAndNamespace.Load("testPod"); depAndNamespace != "testDeployment@testNamespace" { + t.Errorf("podToWorkloadAndNamespace was incorrect, got: %s, want: %s.", depAndNamespace, "testDeployment@testNamespace") + } + + // Test the count in workloadPodCount + if count := workloadPodCount["testDeployment@testNamespace"]; count != 1 { + t.Errorf("workloadPodCount was incorrect, got: %d, want: %d.", count, 1) + } + }) + + t.Run("pod updated with different set of labels", func(t *testing.T) { + ipToPod := &sync.Map{} + podToWorkloadAndNamespace := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-5d68bc5f49", + }, + }, + }, + Status: corev1.PodStatus{ + HostIP: "5.6.7.8", + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + {HostPort: 8080}, + }, + }, + }, + }, + } + + // add the pod + poWatcher := newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels, map[string]int{}) + poWatcher.onAddOrUpdatePod(pod, nil) + + // Test the mappings in ipToPod + if podName, ok := ipToPod.Load("5.6.7.8:8080"); !ok && podName != "testPod" { + t.Errorf("ipToPod[%s] was incorrect, got: %s, want: %s.", "5.6.7.8:8080", podName, "testPod") + } + + // Test the mapping in workloadAndNamespaceToLabels + labels, _ := workloadAndNamespaceToLabels.Load("testDeployment@testNamespace") + expectedLabels := []string{"label1=value1", "label2=value2"} + for _, label := range expectedLabels { + if !labels.(mapset.Set[string]).Contains(label) { + t.Errorf("deploymentAndNamespaceToLabels was incorrect, got: %v, want: %s.", labels, label) + } + } + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-5d68bc5f49", + }, + }, + }, + Status: corev1.PodStatus{ + PodIP: "1.2.3.4", + HostIP: "5.6.7.8", + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + {HostPort: 8080}, + }, + }, + }, + }, + } + + // add the pod + poWatcher.onAddOrUpdatePod(pod2, pod) + + // Test the mappings in ipToPod + if podName, ok := ipToPod.Load("5.6.7.8:8080"); !ok && podName != "testPod" { + t.Errorf("ipToPod[%s] was incorrect, got: %s, want: %s.", "5.6.7.8:8080", podName, "testPod") + } + + if podName, ok := ipToPod.Load("1.2.3.4"); !ok && podName != "testPod" { + t.Errorf("ipToPod[%s] was incorrect, got: %s, want: %s.", "1.2.3.4", podName, "testPod") + } + // Test the mapping in workloadAndNamespaceToLabels + labels, _ = workloadAndNamespaceToLabels.Load("testDeployment@testNamespace") + expectedLabels = []string{"label1=value1", "label2=value2", "label3=value3"} + for _, label := range expectedLabels { + if !labels.(mapset.Set[string]).Contains(label) { + t.Errorf("workloadAndNamespaceToLabels was incorrect, got: %v, want: %s.", labels, label) + } + } + }) +} + +func TestOnDeletePod(t *testing.T) { + t.Run("pod with both PodIP and HostIP", func(t *testing.T) { + ipToPod := &sync.Map{} + podToWorkloadAndNamespace := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + workloadPodCount := map[string]int{} + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-xyz", + }, + }, + }, + Status: corev1.PodStatus{ + PodIP: "1.2.3.4", + HostIP: "5.6.7.8", + }, + } + + // Assume the pod has already been added + ipToPod.Store(pod.Status.PodIP, pod.Name) + ipToPod.Store(pod.Status.HostIP, pod.Name) + podToWorkloadAndNamespace.Store(pod.Name, "testDeployment@testNamespace") + workloadAndNamespaceToLabels.Store("testDeployment@testNamespace", "testLabels") + workloadPodCount["testDeployment@testNamespace"] = 1 + + poWatcher := newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels, workloadPodCount) + poWatcher.onDeletePod(pod) + + // Test if the entries in ipToPod and podToWorkloadAndNamespace have been deleted + if _, ok := ipToPod.Load("1.2.3.4"); ok { + t.Errorf("ipToPod deletion was incorrect, key: %s still exists", "1.2.3.4") + } + + if _, ok := podToWorkloadAndNamespace.Load("testPod"); ok { + t.Errorf("podToWorkloadAndNamespace deletion was incorrect, key: %s still exists", "testPod") + } + + // Test if the count in workloadPodCount has been decremented and the entry in workloadAndNamespaceToLabels has been deleted + if count := workloadPodCount["testDeployment@testNamespace"]; count != 0 { + t.Errorf("workloadPodCount was incorrect, got: %d, want: %d.", count, 0) + } + + if _, ok := workloadAndNamespaceToLabels.Load("testDeployment@testNamespace"); ok { + t.Errorf("workloadAndNamespaceToLabels deletion was incorrect, key: %s still exists", "testDeployment@testNamespace") + } + }) + + t.Run("pod with only HostIP and some network ports", func(t *testing.T) { + ipToPod := &sync.Map{} + podToWorkloadAndNamespace := &sync.Map{} + workloadAndNamespaceToLabels := &sync.Map{} + workloadPodCount := map[string]int{} + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPod", + Namespace: "testNamespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testDeployment-xyz", + }, + }, + }, + Status: corev1.PodStatus{ + HostIP: "5.6.7.8", + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + HostPort: int32(8080), + }, + }, + }, + }, + }, + } + + // Assume the pod has already been added + ipToPod.Store(pod.Status.HostIP, pod.Name) + ipToPod.Store(pod.Status.HostIP+":8080", pod.Name) + podToWorkloadAndNamespace.Store(pod.Name, "testDeployment@testNamespace") + workloadAndNamespaceToLabels.Store("testDeployment@testNamespace", "testLabels") + workloadPodCount["testDeployment@testNamespace"] = 1 + + poWatcher := newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels, workloadPodCount) + poWatcher.onDeletePod(pod) + + // Test if the entries in ipToPod and podToWorkloadAndNamespace have been deleted + if _, ok := ipToPod.Load("5.6.7.8:8080"); ok { + t.Errorf("ipToPod deletion was incorrect, key: %s still exists", "5.6.7.8:8080") + } + + if _, ok := podToWorkloadAndNamespace.Load("testPod"); ok { + t.Errorf("podToDeploymentAndNamespace deletion was incorrect, key: %s still exists", "testPod") + } + + // Test if the count in workloadPodCount has been decremented and the entry in workloadAndNamespaceToLabels has been deleted + if count := workloadPodCount["testDeployment@testNamespace"]; count != 0 { + t.Errorf("workloadPodCount was incorrect, got: %d, want: %d.", count, 0) + } + + if _, ok := workloadAndNamespaceToLabels.Load("testDeployment@testNamespace"); ok { + t.Errorf("workloadAndNamespaceToLabels deletion was incorrect, key: %s still exists", "testDeployment@testNamespace") + } + }) +} + +func TestEksResolver(t *testing.T) { + logger, _ := zap.NewProduction() + ctx := context.Background() + + t.Run("Test getWorkloadAndNamespaceByIP", func(t *testing.T) { + resolver := &kubernetesResolver{ + logger: logger, + clusterName: "test", + ipToPod: &sync.Map{}, + podToWorkloadAndNamespace: &sync.Map{}, + ipToServiceAndNamespace: &sync.Map{}, + serviceToWorkload: &sync.Map{}, + } + + ip := "1.2.3.4" + pod := "testPod" + workloadAndNamespace := "testDeployment@testNamespace" + + // Pre-fill the resolver maps + resolver.ipToPod.Store(ip, pod) + resolver.podToWorkloadAndNamespace.Store(pod, workloadAndNamespace) + + // Test existing IP + workload, namespace, err := resolver.getWorkloadAndNamespaceByIP(ip) + if err != nil || workload != "testDeployment" || namespace != "testNamespace" { + t.Errorf("Expected testDeployment@testNamespace, got %s@%s, error: %v", workload, namespace, err) + } + + // Test non-existing IP + _, _, err = resolver.getWorkloadAndNamespaceByIP("5.6.7.8") + if err == nil || !strings.Contains(err.Error(), "no kubernetes workload found for ip: 5.6.7.8") { + t.Errorf("Expected error, got %v", err) + } + + // Test ip in ipToServiceAndNamespace but not in ipToPod + newIP := "2.3.4.5" + serviceAndNamespace := "testService@testNamespace" + resolver.ipToServiceAndNamespace.Store(newIP, serviceAndNamespace) + resolver.serviceToWorkload.Store(serviceAndNamespace, workloadAndNamespace) + workload, namespace, err = resolver.getWorkloadAndNamespaceByIP(newIP) + if err != nil || workload != "testDeployment" || namespace != "testNamespace" { + t.Errorf("Expected testDeployment@testNamespace, got %s@%s, error: %v", workload, namespace, err) + } + }) + + t.Run("Test Stop", func(t *testing.T) { + resolver := &kubernetesResolver{ + logger: logger, + safeStopCh: &safeChannel{ch: make(chan struct{}), closed: false}, + } + + err := resolver.Stop(ctx) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !resolver.safeStopCh.closed { + t.Errorf("Expected channel to be closed") + } + + // Test closing again + err = resolver.Stop(ctx) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("Test Process", func(t *testing.T) { + // helper function to get string values from the attributes + getStrAttr := func(attributes pcommon.Map, key string, t *testing.T) string { + if value, ok := attributes.Get(key); ok { + return value.AsString() + } else { + t.Errorf("Failed to get value for key: %s", key) + return "" + } + } + + logger, _ := zap.NewProduction() + resolver := &kubernetesResolver{ + logger: logger, + clusterName: "test", + platformCode: config.PlatformEKS, + ipToPod: &sync.Map{}, + podToWorkloadAndNamespace: &sync.Map{}, + ipToServiceAndNamespace: &sync.Map{}, + serviceToWorkload: &sync.Map{}, + } + + // Test case 1: "aws.remote.service" contains IP:Port + attributes := pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteService, "192.0.2.1:8080") + resourceAttributes := pcommon.NewMap() + resolver.ipToPod.Store("192.0.2.1:8080", "test-pod") + resolver.podToWorkloadAndNamespace.Store("test-pod", "test-deployment@test-namespace") + err := resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + assert.Equal(t, "test-deployment", getStrAttr(attributes, attr.AWSRemoteService, t)) + assert.Equal(t, "eks:test/test-namespace", getStrAttr(attributes, attr.AWSRemoteEnvironment, t)) + + // Test case 2: "aws.remote.service" contains only IP + attributes = pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteService, "192.0.2.2") + resourceAttributes = pcommon.NewMap() + resolver.ipToPod.Store("192.0.2.2", "test-pod-2") + resolver.podToWorkloadAndNamespace.Store("test-pod-2", "test-deployment-2@test-namespace-2") + err = resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + assert.Equal(t, "test-deployment-2", getStrAttr(attributes, attr.AWSRemoteService, t)) + assert.Equal(t, "eks:test/test-namespace-2", getStrAttr(attributes, attr.AWSRemoteEnvironment, t)) + + // Test case 3: "aws.remote.service" contains non-ip string + attributes = pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteService, "not-an-ip") + resourceAttributes = pcommon.NewMap() + err = resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + assert.Equal(t, "not-an-ip", getStrAttr(attributes, attr.AWSRemoteService, t)) + + // Test case 4: Process with valid IP but getWorkloadAndNamespaceByIP returns error + attributes = pcommon.NewMap() + attributes.PutStr(attr.AWSRemoteService, "192.168.1.2") + resourceAttributes = pcommon.NewMap() + err = resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + assert.Equal(t, "192.168.1.2", getStrAttr(attributes, attr.AWSRemoteService, t)) + }) +} + +func TestK8sResourceAttributesResolverOnEKS(t *testing.T) { + // helper function to get string values from the attributes + getStrAttr := func(attributes pcommon.Map, key string, t *testing.T) string { + if value, ok := attributes.Get(key); ok { + return value.AsString() + } else { + t.Errorf("Failed to get value for key: %s", key) + return "" + } + } + + resolver := newKubernetesResourceAttributesResolver(config.PlatformEKS, "test-cluster") + + resourceAttributesBase := map[string]string{ + "cloud.provider": "aws", + "k8s.namespace.name": "test-namespace-3", + "host.id": "instance-id", + "host.name": "hostname", + "ec2.tag.aws:autoscaling:groupName": "asg", + } + + tests := []struct { + name string + resourceAttributesOverwrite map[string]string + expectedAttributes map[string]string + }{ + { + "testDefault", + map[string]string{}, + + map[string]string{ + attr.AWSLocalEnvironment: "eks:test-cluster/test-namespace-3", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeEKSClusterName: "test-cluster", + common.AttributeEC2InstanceId: "instance-id", + common.AttributeHost: "hostname", + common.AttributeEC2AutoScalingGroup: "asg", + }, + }, + { + "testOverwrite", + map[string]string{ + semconv.AttributeDeploymentEnvironment: "custom-env", + }, + map[string]string{ + attr.AWSLocalEnvironment: "custom-env", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeEKSClusterName: "test-cluster", + common.AttributeEC2InstanceId: "instance-id", + common.AttributeHost: "hostname", + common.AttributeEC2AutoScalingGroup: "asg", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + for key, val := range resourceAttributesBase { + resourceAttributes.PutStr(key, val) + } + for key, val := range tt.resourceAttributesOverwrite { + resourceAttributes.PutStr(key, val) + } + err := resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + + for key, val := range tt.expectedAttributes { + assert.Equal(t, val, getStrAttr(attributes, key, t), fmt.Sprintf("expected %s for key %s", val, key)) + } + assert.Equal(t, "/aws/containerinsights/test-cluster/application", getStrAttr(resourceAttributes, semconv.AttributeAWSLogGroupNames, t)) + }) + } +} + +func TestK8sResourceAttributesResolverOnK8S(t *testing.T) { + // helper function to get string values from the attributes + getStrAttr := func(attributes pcommon.Map, key string, t *testing.T) string { + if value, ok := attributes.Get(key); ok { + return value.AsString() + } else { + t.Errorf("Failed to get value for key: %s", key) + return "" + } + } + + resolver := newKubernetesResourceAttributesResolver(config.PlatformK8s, "test-cluster") + + resourceAttributesBase := map[string]string{ + "cloud.provider": "aws", + "k8s.namespace.name": "test-namespace-3", + "host.id": "instance-id", + "host.name": "hostname", + "ec2.tag.aws:autoscaling:groupName": "asg", + } + + tests := []struct { + name string + resourceAttributesOverwrite map[string]string + expectedAttributes map[string]string + }{ + { + "testDefaultOnK8s", + map[string]string{}, + + map[string]string{ + attr.AWSLocalEnvironment: "k8s:test-cluster/test-namespace-3", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeK8SClusterName: "test-cluster", + common.AttributeEC2InstanceId: "instance-id", + common.AttributeHost: "hostname", + common.AttributeEC2AutoScalingGroup: "asg", + }, + }, + { + "testOverwriteOnK8s", + map[string]string{ + semconv.AttributeDeploymentEnvironment: "custom-env", + }, + map[string]string{ + attr.AWSLocalEnvironment: "custom-env", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeK8SClusterName: "test-cluster", + common.AttributeEC2InstanceId: "instance-id", + common.AttributeHost: "hostname", + common.AttributeEC2AutoScalingGroup: "asg", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + for key, val := range resourceAttributesBase { + resourceAttributes.PutStr(key, val) + } + for key, val := range tt.resourceAttributesOverwrite { + resourceAttributes.PutStr(key, val) + } + err := resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + + for key, val := range tt.expectedAttributes { + assert.Equal(t, val, getStrAttr(attributes, key, t), fmt.Sprintf("expected %s for key %s", val, key)) + } + assert.Equal(t, "/aws/containerinsights/test-cluster/application", getStrAttr(resourceAttributes, semconv.AttributeAWSLogGroupNames, t)) + }) + } +} + +func TestK8sResourceAttributesResolverOnK8SOnPrem(t *testing.T) { + // helper function to get string values from the attributes + getStrAttr := func(attributes pcommon.Map, key string, t *testing.T) string { + if value, ok := attributes.Get(key); ok { + return value.AsString() + } else { + t.Errorf("Failed to get value for key: %s", key) + return "" + } + } + + resolver := newKubernetesResourceAttributesResolver(config.PlatformK8s, "test-cluster") + + resourceAttributesBase := map[string]string{ + "cloud.provider": "aws", + "k8s.namespace.name": "test-namespace-3", + "host.name": "hostname", + } + + tests := []struct { + name string + resourceAttributesOverwrite map[string]string + expectedAttributes map[string]string + }{ + { + "testDefault", + map[string]string{}, + + map[string]string{ + attr.AWSLocalEnvironment: "k8s:test-cluster/test-namespace-3", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeK8SClusterName: "test-cluster", + common.AttributeHost: "hostname", + }, + }, + { + "testOverwrite", + map[string]string{ + semconv.AttributeDeploymentEnvironment: "custom-env", + }, + map[string]string{ + attr.AWSLocalEnvironment: "custom-env", + common.AttributeK8SNamespace: "test-namespace-3", + common.AttributeK8SClusterName: "test-cluster", + common.AttributeHost: "hostname", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + attributes := pcommon.NewMap() + resourceAttributes := pcommon.NewMap() + for key, val := range resourceAttributesBase { + resourceAttributes.PutStr(key, val) + } + for key, val := range tt.resourceAttributesOverwrite { + resourceAttributes.PutStr(key, val) + } + err := resolver.Process(attributes, resourceAttributes) + assert.NoError(t, err) + + for key, val := range tt.expectedAttributes { + assert.Equal(t, val, getStrAttr(attributes, key, t), fmt.Sprintf("expected %s for key %s", val, key)) + } + assert.Equal(t, "/aws/containerinsights/test-cluster/application", getStrAttr(resourceAttributes, semconv.AttributeAWSLogGroupNames, t)) + + // EC2 related fields that should not exist for on-prem + _, exists := attributes.Get(common.AttributeEC2AutoScalingGroup) + assert.False(t, exists) + + _, exists = attributes.Get(common.AttributeEC2InstanceId) + assert.False(t, exists) + }) + } +} + +func TestExtractIPPort(t *testing.T) { + // Test valid IP:Port + ip, port, ok := extractIPPort("192.0.2.0:8080") + assert.Equal(t, "192.0.2.0", ip) + assert.Equal(t, "8080", port) + assert.True(t, ok) + + // Test invalid IP:Port + ip, port, ok = extractIPPort("192.0.2:8080") + assert.Equal(t, "", ip) + assert.Equal(t, "", port) + assert.False(t, ok) + + // Test IP only + ip, port, ok = extractIPPort("192.0.2.0") + assert.Equal(t, "", ip) + assert.Equal(t, "", port) + assert.False(t, ok) +} + +func TestFilterPodIPFields(t *testing.T) { + meta := metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: map[string]string{ + "name": "app", + }, + } + pod := &corev1.Pod{ + ObjectMeta: meta, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + {}, + }, + }, + Status: corev1.PodStatus{}, + } + newPod, err := minimizePod(pod) + assert.Nil(t, err) + assert.Empty(t, getHostNetworkPorts(newPod.(*corev1.Pod))) + + podStatus := corev1.PodStatus{ + PodIP: "192.168.0.12", + HostIPs: []corev1.HostIP{ + { + IP: "132.168.3.12", + }, + }, + } + pod = &corev1.Pod{ + ObjectMeta: meta, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + {HostPort: 8080}, + }, + }, + }, + }, + Status: podStatus, + } + newPod, err = minimizePod(pod) + assert.Nil(t, err) + assert.Equal(t, "app", newPod.(*corev1.Pod).Labels["name"]) + assert.Equal(t, []string{"8080"}, getHostNetworkPorts(newPod.(*corev1.Pod))) + assert.Equal(t, podStatus, newPod.(*corev1.Pod).Status) + + pod = &corev1.Pod{ + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + {HostPort: 8080}, + {HostPort: 8081}, + }, + }, + }, + }, + Status: podStatus, + } + newPod, err = minimizePod(pod) + assert.Nil(t, err) + assert.Equal(t, []string{"8080", "8081"}, getHostNetworkPorts(newPod.(*corev1.Pod))) + assert.Equal(t, podStatus, newPod.(*corev1.Pod).Status) +} + +func TestFilterServiceIPFields(t *testing.T) { + meta := metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + } + svc := &corev1.Service{ + ObjectMeta: meta, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "name": "app", + }, + ClusterIP: "10.0.12.4", + }, + } + newSvc, err := minimizeService(svc) + assert.Nil(t, err) + assert.Equal(t, "10.0.12.4", newSvc.(*corev1.Service).Spec.ClusterIP) + assert.Equal(t, "app", newSvc.(*corev1.Service).Spec.Selector["name"]) +} + +func TestHandlePodUpdate(t *testing.T) { + testCases := []struct { + name string + oldPod *corev1.Pod + newPod *corev1.Pod + initialIPToPod map[string]string + expectedIPToPod map[string]string + }{ + { + name: "Old and New Pod Do Not Use Host Network, Different Pod IPs", + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + }, + Status: corev1.PodStatus{ + PodIP: "10.0.0.3", + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + }, + newPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + }, + Status: corev1.PodStatus{ + PodIP: "10.0.0.4", + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + }, + initialIPToPod: map[string]string{ + "10.0.0.3": "mypod", + }, + expectedIPToPod: map[string]string{ + "10.0.0.4": "mypod", + }, + }, + { + name: "Old Pod Has Empty PodIP, New Pod Does Not Use Host Network, Non-Empty Pod IP", + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + }, + Status: corev1.PodStatus{ + PodIP: "", + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + }, + newPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + }, + Status: corev1.PodStatus{ + PodIP: "10.0.0.5", + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + }, + initialIPToPod: map[string]string{}, + expectedIPToPod: map[string]string{ + "10.0.0.5": "mypod", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ipToPod := &sync.Map{} + // Initialize ipToPod map + for k, v := range tc.initialIPToPod { + ipToPod.Store(k, v) + } + poWatcher := newPodWatcherForTesting(ipToPod, nil, nil, map[string]int{}) + poWatcher.handlePodUpdate(tc.newPod, tc.oldPod) + + // Now validate that ipToPod map has been updated correctly + for key, expectedValue := range tc.expectedIPToPod { + val, ok := ipToPod.Load(key) + if !ok || val.(string) != expectedValue { + t.Errorf("Expected record for %v to be %v, got %v", key, expectedValue, val) + } + } + // Validate that old keys have been removed + for key := range tc.initialIPToPod { + if _, ok := tc.expectedIPToPod[key]; !ok { + if _, ok := ipToPod.Load(key); ok { + t.Errorf("Expected record for %v to be removed, but it was not", key) + } + } + } + }) + } +} + +func newServiceWatcherForTesting(ipToServiceAndNamespace, serviceAndNamespaceToSelectors *sync.Map) *serviceWatcher { + logger, _ := zap.NewDevelopment() + return &serviceWatcher{ipToServiceAndNamespace, serviceAndNamespaceToSelectors, logger, nil, nil} +} + +func newPodWatcherForTesting(ipToPod, podToWorkloadAndNamespace, workloadAndNamespaceToLabels *sync.Map, workloadPodCount map[string]int) *podWatcher { + logger, _ := zap.NewDevelopment() + return &podWatcher{ + ipToPod: ipToPod, + podToWorkloadAndNamespace: podToWorkloadAndNamespace, + workloadAndNamespaceToLabels: workloadAndNamespaceToLabels, + workloadPodCount: workloadPodCount, + logger: logger, + informer: nil, + deleter: mockDeleter, + } +} diff --git a/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_utils.go b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_utils.go new file mode 100644 index 000000000000..32befd4c7903 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/internal/resolver/kubernetes_utils.go @@ -0,0 +1,142 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package resolver + +import ( + "errors" + "fmt" + "net" + "regexp" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +const ( + // kubeAllowedStringAlphaNums holds the characters allowed in replicaset names from as parent deployment + // https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/rand/rand.go#L121 + kubeAllowedStringAlphaNums = "bcdfghjklmnpqrstvwxz2456789" +) + +var ( + // ReplicaSet name = Deployment name + "-" + up to 10 alphanumeric characters string, if the ReplicaSet was created through a deployment + // The suffix string of the ReplicaSet name is an int32 number (0 to 4,294,967,295) that is cast to a string and then + // mapped to an alphanumeric value with only the following characters allowed: "bcdfghjklmnpqrstvwxz2456789". + // The suffix string length is therefore nondeterministic. The regex accepts a suffix of length 6-10 to account for + // ReplicaSets not managed by deployments that may have similar names. + // Suffix Generation: https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/controller_utils.go#L1201 + // Alphanumeric Mapping: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/rand/rand.go#L121) + replicaSetWithDeploymentNamePattern = fmt.Sprintf(`^(.+)-[%s]{6,10}$`, kubeAllowedStringAlphaNums) + deploymentFromReplicaSetPattern = regexp.MustCompile(replicaSetWithDeploymentNamePattern) + // if a pod is launched directly by a replicaSet (with a given name by users), its name has the following pattern: + // Pod name = ReplicaSet name + 5 alphanumeric characters long string + podWithReplicaSetNamePattern = fmt.Sprintf(`^(.+)-[%s]{5}$`, kubeAllowedStringAlphaNums) + replicaSetFromPodPattern = regexp.MustCompile(podWithReplicaSetNamePattern) +) + +func attachNamespace(resourceName, namespace string) string { + // character "@" is not allowed in kubernetes resource names: https://unofficial-kubernetes.readthedocs.io/en/latest/concepts/overview/working-with-objects/names/ + return resourceName + "@" + namespace +} + +func getServiceAndNamespace(service *corev1.Service) string { + return attachNamespace(service.Name, service.Namespace) +} + +func extractResourceAndNamespace(serviceOrWorkloadAndNamespace string) (string, string) { + // extract service name and namespace from serviceAndNamespace + parts := strings.Split(serviceOrWorkloadAndNamespace, "@") + if len(parts) != 2 { + return "", "" + } + return parts[0], parts[1] +} + +func extractWorkloadNameFromRS(replicaSetName string) (string, error) { + match := deploymentFromReplicaSetPattern.FindStringSubmatch(replicaSetName) + if match != nil { + return match[1], nil + } + + return "", errors.New("failed to extract workload name from replicatSet name: " + replicaSetName) +} + +func extractWorkloadNameFromPodName(podName string) (string, error) { + match := replicaSetFromPodPattern.FindStringSubmatch(podName) + if match != nil { + return match[1], nil + } + + return "", errors.New("failed to extract workload name from pod name: " + podName) +} + +func getWorkloadAndNamespace(pod *corev1.Pod) string { + var workloadAndNamespace string + if pod.ObjectMeta.OwnerReferences != nil { + for _, ownerRef := range pod.ObjectMeta.OwnerReferences { + if workloadAndNamespace != "" { + break + } + + if ownerRef.Kind == "ReplicaSet" { + if workloadName, err := extractWorkloadNameFromRS(ownerRef.Name); err == nil { + // when the replicaSet is created by a deployment, use deployment name + workloadAndNamespace = attachNamespace(workloadName, pod.Namespace) + } else if workloadName, err := extractWorkloadNameFromPodName(pod.Name); err == nil { + // when the replicaSet is not created by a deployment, use replicaSet name directly + workloadAndNamespace = attachNamespace(workloadName, pod.Namespace) + } + } else if ownerRef.Kind == "StatefulSet" { + workloadAndNamespace = attachNamespace(ownerRef.Name, pod.Namespace) + } else if ownerRef.Kind == "DaemonSet" { + workloadAndNamespace = attachNamespace(ownerRef.Name, pod.Namespace) + } + } + } + + return workloadAndNamespace +} + +const IP_PORT_PATTERN = `^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$` + +var ipPortRegex = regexp.MustCompile(IP_PORT_PATTERN) + +func extractIPPort(ipPort string) (string, string, bool) { + match := ipPortRegex.MatchString(ipPort) + + if !match { + return "", "", false + } + + result := ipPortRegex.FindStringSubmatch(ipPort) + if len(result) != 3 { + return "", "", false + } + + ip := result[1] + port := result[2] + + return ip, port, true +} + +func getHostNetworkPorts(pod *corev1.Pod) []string { + var ports []string + if !pod.Spec.HostNetwork { + return ports + } + for _, container := range pod.Spec.Containers { + for _, port := range container.Ports { + if port.HostPort != 0 { + ports = append(ports, strconv.Itoa(int(port.HostPort))) + } + } + } + return ports +} + +func isIP(ipString string) bool { + ip := net.ParseIP(ipString) + return ip != nil +} diff --git a/processor/awsapplicationsignalsprocessor/processor.go b/processor/awsapplicationsignalsprocessor/processor.go new file mode 100644 index 000000000000..30a83a779f40 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/processor.go @@ -0,0 +1,336 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package awsapplicationsignalsprocessor + +import ( + "context" + "unicode" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + appsignalsconfig "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/cardinalitycontrol" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/normalizer" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/prune" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/resolver" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/rules" +) + +const ( + failedToProcessAttribute = "failed to process attributes" + failedToProcessAttributeWithLimiter = "failed to process attributes with limiter, keep the data" +) + +var metricCaser = cases.Title(language.English) + +// this is used to Process some attributes (like IP addresses) to a generic form to reduce high cardinality +type attributesMutator interface { + Process(attributes, resourceAttributes pcommon.Map, isTrace bool) error +} + +type allowListMutator interface { + ShouldBeDropped(attributes pcommon.Map) (bool, error) +} + +type stopper interface { + Stop(context.Context) error +} + +type awsapplicationsignalsprocessor struct { + logger *zap.Logger + config *appsignalsconfig.Config + replaceActions *rules.ReplaceActions + allowlistMutators []allowListMutator + metricMutators []attributesMutator + traceMutators []attributesMutator + limiter cardinalitycontrol.Limiter + stoppers []stopper +} + +func (ap *awsapplicationsignalsprocessor) StartMetrics(ctx context.Context, _ component.Host) error { + attributesResolver := resolver.NewAttributesResolver(ap.config.Resolvers, ap.logger) + ap.stoppers = []stopper{attributesResolver} + attributesNormalizer := normalizer.NewAttributesNormalizer(ap.logger) + ap.metricMutators = []attributesMutator{attributesResolver, attributesNormalizer} + + limiterConfig := ap.config.Limiter + if limiterConfig == nil { + limiterConfig = appsignalsconfig.NewDefaultLimiterConfig() + } + if limiterConfig.ParentContext == nil { + limiterConfig.ParentContext = ctx + } + + if !limiterConfig.Disabled { + ap.limiter = cardinalitycontrol.NewMetricsLimiter(limiterConfig, ap.logger) + } else { + ap.logger.Info("metrics limiter is disabled.") + } + + ap.replaceActions = rules.NewReplacer(ap.config.Rules, !limiterConfig.Disabled) + + pruner := prune.NewPruner() + keeper := rules.NewKeeper(ap.config.Rules, !limiterConfig.Disabled) + dropper := rules.NewDropper(ap.config.Rules) + ap.allowlistMutators = []allowListMutator{pruner, keeper, dropper} + + return nil +} + +func (ap *awsapplicationsignalsprocessor) StartTraces(_ context.Context, _ component.Host) error { + attributesResolver := resolver.NewAttributesResolver(ap.config.Resolvers, ap.logger) + attributesNormalizer := normalizer.NewAttributesNormalizer(ap.logger) + customReplacer := rules.NewReplacer(ap.config.Rules, false) + + ap.stoppers = append(ap.stoppers, attributesResolver) + ap.traceMutators = append(ap.traceMutators, attributesResolver, attributesNormalizer, customReplacer) + return nil +} + +func (ap *awsapplicationsignalsprocessor) Shutdown(ctx context.Context) error { + for _, stopper := range ap.stoppers { + err := stopper.Stop(ctx) + if err != nil { + ap.logger.Error("failed to stop", zap.Error(err)) + } + } + return nil +} + +func (ap *awsapplicationsignalsprocessor) processTraces(_ context.Context, td ptrace.Traces) (ptrace.Traces, error) { + rss := td.ResourceSpans() + for i := 0; i < rss.Len(); i++ { + rs := rss.At(i) + ilss := rs.ScopeSpans() + resourceAttributes := rs.Resource().Attributes() + for j := 0; j < ilss.Len(); j++ { + ils := ilss.At(j) + spans := ils.Spans() + for k := 0; k < spans.Len(); k++ { + span := spans.At(k) + for _, Mutator := range ap.traceMutators { + err := Mutator.Process(span.Attributes(), resourceAttributes, true) + if err != nil { + ap.logger.Debug("failed to Process span", zap.Error(err)) + } + } + } + } + } + return td, nil +} + +func (ap *awsapplicationsignalsprocessor) processMetrics(ctx context.Context, md pmetric.Metrics) (pmetric.Metrics, error) { + rms := md.ResourceMetrics() + for i := 0; i < rms.Len(); i++ { + rs := rms.At(i) + ilms := rs.ScopeMetrics() + resourceAttributes := rs.Resource().Attributes() + for j := 0; j < ilms.Len(); j++ { + ils := ilms.At(j) + metrics := ils.Metrics() + for k := 0; k < metrics.Len(); k++ { + m := metrics.At(k) + // Check if the first letter of the metric name is not capitalized + if len(m.Name()) > 0 && !unicode.IsUpper(rune(m.Name()[0])) { + m.SetName(metricCaser.String(m.Name())) // Ensure metric name is in sentence case + } + ap.processMetricAttributes(ctx, m, resourceAttributes) + } + } + } + return md, nil +} + +// Attributes are provided for each log and trace, but not at the metric level +// Need to process attributes for every data point within a metric. +func (ap *awsapplicationsignalsprocessor) processMetricAttributes(_ context.Context, m pmetric.Metric, resourceAttribes pcommon.Map) { + // This is a lot of repeated code, but since there is no single parent superclass + // between metric data types, we can't use polymorphism. + switch m.Type() { + case pmetric.MetricTypeGauge: + dps := m.Gauge().DataPoints() + for i := 0; i < dps.Len(); i++ { + for _, mutator := range ap.metricMutators { + err := mutator.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + } + dps.RemoveIf(func(d pmetric.NumberDataPoint) bool { + for _, mutator := range ap.allowlistMutators { + shouldBeDropped, err := mutator.ShouldBeDropped(d.Attributes()) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + if shouldBeDropped { + return true + } + } + return false + }) + for i := 0; i < dps.Len(); i++ { + err := ap.replaceActions.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + if ap.limiter != nil { + for i := 0; i < dps.Len(); i++ { + if _, err := ap.limiter.Admit(m.Name(), dps.At(i).Attributes(), resourceAttribes); err != nil { + ap.logger.Debug(failedToProcessAttributeWithLimiter, zap.Error(err)) + } + } + } + case pmetric.MetricTypeSum: + dps := m.Sum().DataPoints() + for i := 0; i < dps.Len(); i++ { + for _, mutator := range ap.metricMutators { + err := mutator.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + } + dps.RemoveIf(func(d pmetric.NumberDataPoint) bool { + for _, mutator := range ap.allowlistMutators { + shouldBeDropped, err := mutator.ShouldBeDropped(d.Attributes()) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + if shouldBeDropped { + return true + } + } + return false + }) + for i := 0; i < dps.Len(); i++ { + err := ap.replaceActions.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + if ap.limiter != nil { + for i := 0; i < dps.Len(); i++ { + if _, err := ap.limiter.Admit(m.Name(), dps.At(i).Attributes(), resourceAttribes); err != nil { + ap.logger.Debug(failedToProcessAttributeWithLimiter, zap.Error(err)) + } + } + } + case pmetric.MetricTypeHistogram: + dps := m.Histogram().DataPoints() + for i := 0; i < dps.Len(); i++ { + for _, mutator := range ap.metricMutators { + err := mutator.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + } + dps.RemoveIf(func(d pmetric.HistogramDataPoint) bool { + for _, mutator := range ap.allowlistMutators { + shouldBeDropped, err := mutator.ShouldBeDropped(d.Attributes()) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + if shouldBeDropped { + return true + } + } + return false + }) + for i := 0; i < dps.Len(); i++ { + err := ap.replaceActions.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + if ap.limiter != nil { + for i := 0; i < dps.Len(); i++ { + if _, err := ap.limiter.Admit(m.Name(), dps.At(i).Attributes(), resourceAttribes); err != nil { + ap.logger.Debug(failedToProcessAttributeWithLimiter, zap.Error(err)) + } + } + } + case pmetric.MetricTypeExponentialHistogram: + dps := m.ExponentialHistogram().DataPoints() + for i := 0; i < dps.Len(); i++ { + for _, mutator := range ap.metricMutators { + err := mutator.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + } + dps.RemoveIf(func(d pmetric.ExponentialHistogramDataPoint) bool { + for _, mutator := range ap.allowlistMutators { + shouldBeDropped, err := mutator.ShouldBeDropped(d.Attributes()) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + if shouldBeDropped { + return true + } + } + return false + }) + for i := 0; i < dps.Len(); i++ { + err := ap.replaceActions.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + if ap.limiter != nil { + for i := 0; i < dps.Len(); i++ { + if _, err := ap.limiter.Admit(m.Name(), dps.At(i).Attributes(), resourceAttribes); err != nil { + ap.logger.Debug(failedToProcessAttributeWithLimiter, zap.Error(err)) + } + } + } + case pmetric.MetricTypeSummary: + dps := m.Summary().DataPoints() + for i := 0; i < dps.Len(); i++ { + for _, mutator := range ap.metricMutators { + err := mutator.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + } + dps.RemoveIf(func(d pmetric.SummaryDataPoint) bool { + for _, mutator := range ap.allowlistMutators { + shouldBeDropped, err := mutator.ShouldBeDropped(d.Attributes()) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + if shouldBeDropped { + return true + } + } + return false + }) + for i := 0; i < dps.Len(); i++ { + err := ap.replaceActions.Process(dps.At(i).Attributes(), resourceAttribes, false) + if err != nil { + ap.logger.Debug(failedToProcessAttribute, zap.Error(err)) + } + } + if ap.limiter != nil { + for i := 0; i < dps.Len(); i++ { + if _, err := ap.limiter.Admit(m.Name(), dps.At(i).Attributes(), resourceAttribes); err != nil { + ap.logger.Debug(failedToProcessAttributeWithLimiter, zap.Error(err)) + } + } + } + default: + ap.logger.Debug("Ignore unknown metric type", zap.String("type", m.Type().String())) + } +} diff --git a/processor/awsapplicationsignalsprocessor/processor_test.go b/processor/awsapplicationsignalsprocessor/processor_test.go new file mode 100644 index 000000000000..4b01206960bb --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/processor_test.go @@ -0,0 +1,271 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package awsapplicationsignalsprocessor + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/config" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/rules" +) + +var testRules = []rules.Rule{ + { + Selectors: []rules.Selector{ + { + Dimension: "dim_action", + Match: "reserved", + }, + { + Dimension: "dim_val", + Match: "test1", + }, + }, + Replacements: []rules.Replacement{ + { + TargetDimension: "dim_val", + Value: "test2", + }, + }, + Action: "replace", + }, + { + Selectors: []rules.Selector{ + { + Dimension: "dim_action", + Match: "reserved", + }, + }, + Action: "keep", + }, + { + Selectors: []rules.Selector{ + { + Dimension: "dim_drop", + Match: "hc", + }, + }, + Action: "drop", + }, +} + +func TestProcessMetrics(t *testing.T) { + logger, _ := zap.NewDevelopment() + ap := &awsapplicationsignalsprocessor{ + logger: logger, + config: &config.Config{ + Resolvers: []config.Resolver{config.NewGenericResolver("")}, + Rules: testRules, + }, + } + + ctx := context.Background() + ap.StartMetrics(ctx, nil) + + keepMetrics := generateMetrics(map[string]string{ + "dim_action": "reserved", + "dim_val": "test", + "dim_op": "keep", + "Telemetry.Source": "UnitTest", + }) + ap.processMetrics(ctx, keepMetrics) + assert.Equal(t, "reserved", getDimensionValue(t, keepMetrics, "dim_action")) + assert.Equal(t, "test", getDimensionValue(t, keepMetrics, "dim_val")) + + replaceMetrics := generateMetrics(map[string]string{ + "dim_action": "reserved", + "dim_val": "test1", + "Telemetry.Source": "UnitTest", + }) + ap.processMetrics(ctx, replaceMetrics) + assert.Equal(t, "reserved", getDimensionValue(t, replaceMetrics, "dim_action")) + assert.Equal(t, "test2", getDimensionValue(t, replaceMetrics, "dim_val")) + + dropMetricsByDrop := generateMetrics(map[string]string{ + "dim_action": "reserved", + "dim_drop": "hc", + "Telemetry.Source": "UnitTest", + }) + ap.processMetrics(ctx, dropMetricsByDrop) + assert.True(t, isMetricNil(dropMetricsByDrop)) + + dropMetricsByKeep := generateMetrics(map[string]string{ + "dim_op": "drop", + "Telemetry.Source": "UnitTest", + }) + ap.processMetrics(ctx, dropMetricsByKeep) + assert.True(t, isMetricNil(dropMetricsByKeep)) +} + +func TestProcessMetricsLowercase(t *testing.T) { + logger, _ := zap.NewDevelopment() + ap := &awsapplicationsignalsprocessor{ + logger: logger, + config: &config.Config{ + Resolvers: []config.Resolver{config.NewGenericResolver("")}, + Rules: testRules, + }, + } + + ctx := context.Background() + ap.StartMetrics(ctx, nil) + + lowercaseMetrics := pmetric.NewMetrics() + errorMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + errorMetric.SetName("error") + latencyMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + latencyMetric.SetName("latency") + faultMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + faultMetric.SetName("fault") + + ap.processMetrics(ctx, lowercaseMetrics) + assert.Equal(t, "Error", lowercaseMetrics.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) + assert.Equal(t, "Latency", lowercaseMetrics.ResourceMetrics().At(1).ScopeMetrics().At(0).Metrics().At(0).Name()) + assert.Equal(t, "Fault", lowercaseMetrics.ResourceMetrics().At(2).ScopeMetrics().At(0).Metrics().At(0).Name()) +} + +func TestProcessTraces(t *testing.T) { + logger, _ := zap.NewDevelopment() + ap := &awsapplicationsignalsprocessor{ + logger: logger, + config: &config.Config{ + Resolvers: []config.Resolver{config.NewGenericResolver("")}, + Rules: testRules, + }, + } + + ctx := context.Background() + ap.StartTraces(ctx, nil) + + traces := ptrace.NewTraces() + span := traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() + span.Attributes().PutStr("dim_action", "reserved") + span.Attributes().PutStr("dim_val", "test1") + + ap.processTraces(ctx, traces) + + actualSpan := traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) + actualVal, _ := actualSpan.Attributes().Get("dim_val") + assert.Equal(t, "test2", actualVal.AsString()) +} + +func generateMetrics(dimensions map[string]string) pmetric.Metrics { + md := pmetric.NewMetrics() + + m := md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + gauge := m.SetEmptyGauge().DataPoints().AppendEmpty() + gauge.SetIntValue(10) + + m = md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + sum := m.SetEmptySum().DataPoints().AppendEmpty() + sum.SetIntValue(10) + + m = md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + expoHistogram := m.SetEmptyExponentialHistogram().DataPoints().AppendEmpty() + expoHistogram.SetSum(10) + + m = md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + summary := m.SetEmptySummary().DataPoints().AppendEmpty() + summary.SetSum(10) + + m = md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + histogram := m.SetEmptyHistogram().DataPoints().AppendEmpty() + histogram.SetSum(10) + + for k, v := range dimensions { + gauge.Attributes().PutStr(k, v) + sum.Attributes().PutStr(k, v) + expoHistogram.Attributes().PutStr(k, v) + summary.Attributes().PutStr(k, v) + histogram.Attributes().PutStr(k, v) + } + + return md +} + +func getDimensionValue(t *testing.T, m pmetric.Metrics, dimensionName string) string { + var agreedValue string + + gauge := m.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Gauge() + if val, ok := gauge.DataPoints().At(0).Attributes().Get(dimensionName); !ok { + t.Errorf("no dimension value is found with key %s\n", dimensionName) + } else { + agreedValue = val.AsString() + } + + sum := m.ResourceMetrics().At(1).ScopeMetrics().At(0).Metrics().At(0).Sum() + if val, ok := sum.DataPoints().At(0).Attributes().Get(dimensionName); !ok { + t.Errorf("no dimension value is found with key %s\n", dimensionName) + } else { + newVal := val.AsString() + if agreedValue != newVal { + t.Errorf("inconsistent dimension value, agreed value is %s, new %s\n", agreedValue, newVal) + } + } + + expoHistogram := m.ResourceMetrics().At(2).ScopeMetrics().At(0).Metrics().At(0).ExponentialHistogram() + if val, ok := expoHistogram.DataPoints().At(0).Attributes().Get(dimensionName); !ok { + t.Errorf("no dimension value is found with key %s\n", dimensionName) + } else { + newVal := val.AsString() + if agreedValue != newVal { + t.Errorf("inconsistent dimension value, agreed value is %s, new %s\n", agreedValue, newVal) + } + } + + summary := m.ResourceMetrics().At(3).ScopeMetrics().At(0).Metrics().At(0).Summary() + if val, ok := summary.DataPoints().At(0).Attributes().Get(dimensionName); !ok { + t.Errorf("no dimension value is found with key %s\n", dimensionName) + } else { + newVal := val.AsString() + if agreedValue != newVal { + t.Errorf("inconsistent dimension value, agreed value is %s, new %s\n", agreedValue, newVal) + } + } + + histogram := m.ResourceMetrics().At(4).ScopeMetrics().At(0).Metrics().At(0).Histogram() + if val, ok := histogram.DataPoints().At(0).Attributes().Get(dimensionName); !ok { + t.Errorf("no dimension value is found with key %s\n", dimensionName) + } else { + newVal := val.AsString() + if agreedValue != newVal { + t.Errorf("inconsistent dimension value, agreed value is %s, new %s\n", agreedValue, newVal) + } + } + return agreedValue +} + +func isMetricNil(m pmetric.Metrics) bool { + gauge := m.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Gauge().DataPoints() + if gauge.Len() > 0 { + return false + } + + sum := m.ResourceMetrics().At(1).ScopeMetrics().At(0).Metrics().At(0).Sum().DataPoints() + if sum.Len() > 0 { + return false + } + + expoHistogram := m.ResourceMetrics().At(2).ScopeMetrics().At(0).Metrics().At(0).ExponentialHistogram().DataPoints() + if expoHistogram.Len() > 0 { + return false + } + + summary := m.ResourceMetrics().At(3).ScopeMetrics().At(0).Metrics().At(0).Summary().DataPoints() + if summary.Len() > 0 { + return false + } + + histogram := m.ResourceMetrics().At(4).ScopeMetrics().At(0).Metrics().At(0).Histogram().DataPoints() + if histogram.Len() > 0 { + return false + } + return true +} diff --git a/processor/awsapplicationsignalsprocessor/rules/common.go b/processor/awsapplicationsignalsprocessor/rules/common.go new file mode 100644 index 000000000000..af85c47d01ee --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/common.go @@ -0,0 +1,122 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "errors" + + "github.com/gobwas/glob" + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +type AllowListAction string + +const ( + AllowListActionKeep AllowListAction = "keep" + AllowListActionDrop AllowListAction = "drop" + AllowListActionReplace AllowListAction = "replace" +) + +type Selector struct { + Dimension string `mapstructure:"dimension"` + Match string `mapstructure:"match"` +} + +type Replacement struct { + TargetDimension string `mapstructure:"target_dimension"` + Value string `mapstructure:"value"` +} + +type Rule struct { + Selectors []Selector `mapstructure:"selectors"` + Replacements []Replacement `mapstructure:"replacements,omitempty"` + Action AllowListAction `mapstructure:"action"` + RuleName string `mapstructure:"rule_name,omitempty"` +} + +type SelectorMatcherItem struct { + Key string + Matcher glob.Glob +} + +type ActionItem struct { + SelectorMatchers []SelectorMatcherItem + Replacements []Replacement `mapstructure:",omitempty"` +} + +var traceKeyMap = map[string]string{ + common.CWMetricAttributeLocalService: attributes.AWSLocalService, + common.CWMetricAttributeEnvironment: attributes.AWSLocalEnvironment, + common.CWMetricAttributeLocalOperation: attributes.AWSLocalOperation, + common.CWMetricAttributeRemoteService: attributes.AWSRemoteService, + common.CWMetricAttributeRemoteEnvironment: attributes.AWSRemoteEnvironment, + common.CWMetricAttributeRemoteOperation: attributes.AWSRemoteOperation, + common.CWMetricAttributeRemoteResourceIdentifier: attributes.AWSRemoteResourceIdentifier, + common.CWMetricAttributeRemoteResourceType: attributes.AWSRemoteResourceType, +} + +func GetAllowListAction(action string) (AllowListAction, error) { + switch action { + case "drop": + return AllowListActionDrop, nil + case "keep": + return AllowListActionKeep, nil + case "replace": + return AllowListActionReplace, nil + } + return "", errors.New("invalid action in rule") +} + +func convertToManagedAttributeKey(attributeKey string, isTrace bool) string { + val, ok := traceKeyMap[attributeKey] + if ok && isTrace { + return val + } + return attributeKey +} + +func matchesSelectors(attributes pcommon.Map, selectorMatchers []SelectorMatcherItem, isTrace bool) bool { + for _, item := range selectorMatchers { + exactKey := convertToManagedAttributeKey(item.Key, isTrace) + value, ok := attributes.Get(exactKey) + if !ok { + return false + } + if !item.Matcher.Match(value.AsString()) { + return false + } + } + return true +} + +func generateSelectorMatchers(selectors []Selector) []SelectorMatcherItem { + var selectorMatchers []SelectorMatcherItem + for _, selector := range selectors { + selectorMatcherItem := SelectorMatcherItem{ + selector.Dimension, + glob.MustCompile(selector.Match), + } + selectorMatchers = append(selectorMatchers, selectorMatcherItem) + } + return selectorMatchers +} + +func generateActionDetails(rules []Rule, action AllowListAction) []ActionItem { + var actionItems []ActionItem + for _, rule := range rules { + if rule.Action == action { + var selectorMatchers = generateSelectorMatchers(rule.Selectors) + actionItem := ActionItem{ + selectorMatchers, + rule.Replacements, + } + actionItems = append(actionItems, actionItem) + } + } + + return actionItems +} diff --git a/processor/awsapplicationsignalsprocessor/rules/common_test.go b/processor/awsapplicationsignalsprocessor/rules/common_test.go new file mode 100644 index 000000000000..c1761f7a867f --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/common_test.go @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" + attr "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/internal/attributes" +) + +func generateTestAttributes(service string, operation string, remoteService string, remoteOperation string, + isTrace bool) pcommon.Map { + return generateAttributesWithEnv(service, operation, "", remoteService, remoteOperation, "", isTrace) +} + +func generateAttributesWithEnv(service string, operation string, environment string, + remoteService string, remoteOperation string, remoteEnvironment string, + isTrace bool) pcommon.Map { + attributes := pcommon.NewMap() + if isTrace { + attributes.PutStr(attr.AWSLocalService, service) + attributes.PutStr(attr.AWSLocalOperation, operation) + if environment != "" { + attributes.PutStr(attr.AWSLocalEnvironment, environment) + } + attributes.PutStr(attr.AWSRemoteService, remoteService) + attributes.PutStr(attr.AWSRemoteOperation, remoteOperation) + if remoteEnvironment != "" { + attributes.PutStr(attr.AWSRemoteEnvironment, remoteEnvironment) + } + } else { + attributes.PutStr(common.CWMetricAttributeLocalService, service) + attributes.PutStr(common.CWMetricAttributeLocalOperation, operation) + if environment != "" { + attributes.PutStr(common.CWMetricAttributeEnvironment, environment) + } + attributes.PutStr(common.CWMetricAttributeRemoteService, remoteService) + attributes.PutStr(common.CWMetricAttributeRemoteOperation, remoteOperation) + if remoteEnvironment != "" { + attributes.PutStr(common.CWMetricAttributeRemoteEnvironment, remoteEnvironment) + } + } + return attributes +} diff --git a/processor/awsapplicationsignalsprocessor/rules/dropper.go b/processor/awsapplicationsignalsprocessor/rules/dropper.go new file mode 100644 index 000000000000..d04b1946ea3a --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/dropper.go @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import "go.opentelemetry.io/collector/pdata/pcommon" + +type DropActions struct { + Actions []ActionItem +} + +func NewDropper(rules []Rule) *DropActions { + return &DropActions{ + Actions: generateActionDetails(rules, AllowListActionDrop), + } +} + +func (d *DropActions) ShouldBeDropped(attributes pcommon.Map) (bool, error) { + // nothing will be dropped if no rule is defined + if d.Actions == nil || len(d.Actions) == 0 { + return false, nil + } + for _, element := range d.Actions { + isMatched := matchesSelectors(attributes, element.SelectorMatchers, false) + if isMatched { + // drop the datapoint as one of drop rules is matched + return true, nil + } + } + return false, nil +} diff --git a/processor/awsapplicationsignalsprocessor/rules/dropper_test.go b/processor/awsapplicationsignalsprocessor/rules/dropper_test.go new file mode 100644 index 000000000000..f3244f9aea6f --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/dropper_test.go @@ -0,0 +1,180 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +type TestCaseForDropper struct { + name string + input pcommon.Map + output bool +} + +func TestDropperProcessor(t *testing.T) { + config := []Rule{ + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "PUT *", + }, + { + Dimension: "RemoteService", + Match: "customer-test", + }, + }, + Action: "keep", + }, + { + Selectors: []Selector{ + { + Dimension: "RemoteService", + Match: "customer-*", + }, + { + Dimension: "RemoteOperation", + Match: "GET /Owners/*", + }, + }, + Action: "drop", + }, + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "PUT /*/pet/*", + }, + { + Dimension: "RemoteService", + Match: "visit-*-service", + }, + }, + Action: "drop", + }, + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "ListPetsByCustomer", + }, + { + TargetDimension: "ResourceTarget", + Value: " ", + }, + }, + Action: "replace", + }, + } + + testDropper := NewDropper(config) + assert.Equal(t, 2, len(testDropper.Actions)) + + testCases := []TestCaseForDropper{ + { + name: "commonTest01ShouldBeKept", + input: generateTestAttributes("customer-test", "GET /user/123", "visit-service", "GET /visit/12345", false), + output: false, + }, + { + name: "commonTest02ShouldBeDropped", + input: generateTestAttributes("common-test", "GET /user/123", "customer-service", "GET /Owners/12345", false), + output: true, + }, + { + name: "commonTest03ShouldBeDropped", + input: generateTestAttributes("common-test", "PUT /test/pet/123", "visit-test-service", "GET /visit/12345", false), + output: true, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testDropper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} + +func TestDropperProcessorWithNilConfig(t *testing.T) { + testDropper := NewDropper(nil) + isTrace := false + + testCases := []TestCaseForDropper{ + { + name: "nilTest01ShouldBeKept", + input: generateTestAttributes("customer-test", "GET /user/123", "visit-service", "GET /visit/12345", isTrace), + output: false, + }, + { + name: "nilTest02ShouldBeDropped", + input: generateTestAttributes("common-test", "GET /user/123", "customer-service", "GET /Owners/12345", isTrace), + output: false, + }, + { + name: "nilTest03ShouldBeDropped", + input: generateTestAttributes("common-test", "PUT /test/pet/123", "visit-test-service", "GET /visit/12345", isTrace), + output: false, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testDropper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} + +func TestDropperProcessorWithEmptyConfig(t *testing.T) { + var config []Rule + + testDropper := NewDropper(config) + isTrace := false + + testCases := []TestCaseForDropper{ + { + name: "emptyTest01ShouldBeKept", + input: generateTestAttributes("customer-test", "GET /user/123", "visit-service", "GET /visit/12345", isTrace), + output: false, + }, + { + name: "emptyTest02ShouldBeDropped", + input: generateTestAttributes("common-test", "GET /user/123", "customer-service", "GET /Owners/12345", isTrace), + output: false, + }, + { + name: "emptyTest03ShouldBeDropped", + input: generateTestAttributes("common-test", "PUT /test/pet/123", "visit-test-service", "GET /visit/12345", isTrace), + output: false, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testDropper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/rules/keeper.go b/processor/awsapplicationsignalsprocessor/rules/keeper.go new file mode 100644 index 000000000000..fa386001eda4 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/keeper.go @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" +) + +type KeepActions struct { + Actions []ActionItem + markDataPointAsReserved bool +} + +func NewKeeper(rules []Rule, markDataPointAsReserved bool) *KeepActions { + return &KeepActions{ + Actions: generateActionDetails(rules, AllowListActionKeep), + markDataPointAsReserved: markDataPointAsReserved, + } +} + +func (k *KeepActions) ShouldBeDropped(attributes pcommon.Map) (bool, error) { + // nothing will be dropped if no keep rule is defined + if k.Actions == nil || len(k.Actions) == 0 { + return false, nil + } + for _, element := range k.Actions { + isMatched := matchesSelectors(attributes, element.SelectorMatchers, false) + if k.markDataPointAsReserved { + attributes.PutBool(common.AttributeTmpReserved, true) + } + if isMatched { + // keep the datapoint as one of the keep rules is matched + return false, nil + } + } + return true, nil +} diff --git a/processor/awsapplicationsignalsprocessor/rules/keeper_test.go b/processor/awsapplicationsignalsprocessor/rules/keeper_test.go new file mode 100644 index 000000000000..258fef3426be --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/keeper_test.go @@ -0,0 +1,177 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +type TestCaseForKeeper struct { + name string + input pcommon.Map + output bool +} + +func TestKeeperProcessor(t *testing.T) { + config := []Rule{ + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "PUT *", + }, + { + Dimension: "RemoteService", + Match: "customer-test", + }, + }, + Action: "keep", + }, + { + Selectors: []Selector{ + { + Dimension: "RemoteService", + Match: "UnknownRemoteService", + }, + { + Dimension: "RemoteOperation", + Match: "GetShardIterator", + }, + }, + Action: "drop", + }, + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "ListPetsByCustomer", + }, + { + TargetDimension: "ResourceTarget", + Value: " ", + }, + }, + Action: "replace", + }, + } + + testKeeper := NewKeeper(config, false) + assert.Equal(t, 1, len(testKeeper.Actions)) + + isTrace := false + + testCases := []TestCaseForKeeper{ + { + name: "commonTest01ShouldBeKept", + input: generateTestAttributes("visit-test", "PUT owners", "customer-test", "PUT owners", isTrace), + output: false, + }, + { + name: "commonTest02ShouldBeDropped", + input: generateTestAttributes("visit-test", "PUT owners", "vet-test", "PUT owners", isTrace), + output: true, + }, + { + name: "commonTest03ShouldBeDropped", + input: generateTestAttributes("vet-test", "GET owners", "customer-test", "PUT owners", isTrace), + output: true, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testKeeper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} + +func TestKeeperProcessorWithNilConfig(t *testing.T) { + testKeeper := NewKeeper(nil, false) + isTrace := false + + testCases := []TestCaseForKeeper{ + { + name: "nilTest01ShouldBeKept", + input: generateTestAttributes("visit-test", "PUT owners", "customer-test", "PUT owners", isTrace), + output: false, + }, + { + name: "nilTest02ShouldBeKept", + input: generateTestAttributes("visit-test", "PUT owners", "vet-test", "PUT owners", isTrace), + output: false, + }, + { + name: "nilTest03ShouldBeKept", + input: generateTestAttributes("vet-test", "PUT owners", "visit-test", "PUT owners", isTrace), + output: false, + }, + { + name: "nilTest04ShouldBeKept", + input: generateTestAttributes("customer-test", "PUT owners", "visit-test", "PUT owners", isTrace), + output: false, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testKeeper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} + +func TestKeeperProcessorWithEmptyConfig(t *testing.T) { + + config := []Rule{} + + testKeeper := NewKeeper(config, false) + isTrace := false + + testCases := []TestCaseForKeeper{ + { + name: "emptyTest01ShouldBeKept", + input: generateTestAttributes("visit-test", "PUT owners", "customer-test", "PUT owners", isTrace), + output: false, + }, + { + name: "emptyTest02ShouldBeKept", + input: generateTestAttributes("visit-test", "PUT owners", "vet-test", "PUT owners", isTrace), + output: false, + }, + { + name: "emptyTest03ShouldBeKept", + input: generateTestAttributes("vet-test", "PUT owners", "visit-test", "PUT owners", isTrace), + output: false, + }, + { + name: "emptyTest04ShouldBeKept", + input: generateTestAttributes("customer-test", "PUT owners", "visit-test", "PUT owners", isTrace), + output: false, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + result, err := testKeeper.ShouldBeDropped(tt.input) + assert.NoError(t, err) + assert.Equal(t, tt.output, result) + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/rules/replacer.go b/processor/awsapplicationsignalsprocessor/rules/replacer.go new file mode 100644 index 000000000000..b67dd77fcab2 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/replacer.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/amazon-contributing/opentelemetry-collector-contrib/processor/awsapplicationsignalsprocessor/common" +) + +type ReplaceActions struct { + Actions []ActionItem + markDataPointAsReserved bool +} + +func NewReplacer(rules []Rule, markDataPointAsReserved bool) *ReplaceActions { + return &ReplaceActions{ + Actions: generateActionDetails(rules, AllowListActionReplace), + markDataPointAsReserved: markDataPointAsReserved, + } +} + +func (r *ReplaceActions) Process(attributes, _ pcommon.Map, isTrace bool) error { + // do nothing when there is no replace rule defined + if r.Actions == nil || len(r.Actions) == 0 { + return nil + } + // If there are more than one rule are matched, the last one will be executed(Later one has higher priority) + actions := r.Actions + finalRules := make(map[string]string) + for i := len(actions) - 1; i >= 0; i = i - 1 { + element := actions[i] + isMatched := matchesSelectors(attributes, element.SelectorMatchers, isTrace) + if !isMatched { + continue + } + for _, replacement := range element.Replacements { + targetDimension := replacement.TargetDimension + + attr := convertToManagedAttributeKey(targetDimension, isTrace) + // every replacement in one specific dimension only will be performed once + if _, visited := finalRules[attr]; !visited { + finalRules[attr] = replacement.Value + } + } + } + + for key, value := range finalRules { + attributes.PutStr(key, value) + } + + if len(finalRules) > 0 && r.markDataPointAsReserved { + attributes.PutBool(common.AttributeTmpReserved, true) + } + return nil +} diff --git a/processor/awsapplicationsignalsprocessor/rules/replacer_test.go b/processor/awsapplicationsignalsprocessor/rules/replacer_test.go new file mode 100644 index 000000000000..d77719455064 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/rules/replacer_test.go @@ -0,0 +1,354 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +type TestCaseForReplacer struct { + name string + input pcommon.Map + output pcommon.Map + isTrace bool +} + +func TestReplacerProcess(t *testing.T) { + + config := []Rule{ + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "PUT *", + }, + { + Dimension: "RemoteService", + Match: "customer-test", + }, + }, + Action: "keep", + }, + { + Selectors: []Selector{ + { + Dimension: "RemoteService", + Match: "UnknownRemoteService", + }, + { + Dimension: "RemoteOperation", + Match: "GetShardIterator", + }, + }, + Action: "drop", + }, + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "ListPetsByCustomer", + }, + { + TargetDimension: "Operation", + Value: "PUT/GET", + }, + }, + Action: "replace", + }, + } + + testReplacer := NewReplacer(config, false) + assert.Equal(t, 1, len(testReplacer.Actions)) + + testCases := []TestCaseForReplacer{ + { + name: "test01TraceMatch", + input: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + output: generateTestAttributes("replace-test", "PUT/GET", "customer-test", + "ListPetsByCustomer", true), + isTrace: true, + }, + { + name: "test02TraceNotMatch", + input: generateTestAttributes("replace-test", "PUT /api/customer/owners/12345", "customer-test", + "GET", true), + output: generateTestAttributes("replace-test", "PUT /api/customer/owners/12345", "customer-test", + "GET", true), + isTrace: true, + }, + { + name: "test03MetricMatch", + input: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + output: generateTestAttributes("replace-test", "PUT/GET", "customer-test", + "ListPetsByCustomer", false), + isTrace: false, + }, + { + name: "test04MetricNotMatch", + input: generateTestAttributes("replace-test", "PUT /api/customer/owners/12345", "customer-test", + "GET", false), + output: generateTestAttributes("replace-test", "PUT /api/customer/owners/12345", "customer-test", + "GET", false), + isTrace: false, + }, + } + + testMapPlaceHolder := pcommon.NewMap() + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + assert.NoError(t, testReplacer.Process(tt.input, testMapPlaceHolder, tt.isTrace)) + assert.Equal(t, tt.output, tt.input) + }) + } +} + +func TestAddManagedDimensionKey(t *testing.T) { + config := []Rule{ + { + Selectors: []Selector{ + { + Dimension: "Service", + Match: "app", + }, + { + Dimension: "RemoteService", + Match: "remote-app", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteEnvironment", + Value: "test", + }, + }, + Action: "replace", + }, + } + + testReplacer := NewReplacer(config, false) + assert.Equal(t, 1, len(testReplacer.Actions)) + + testCases := []TestCaseForReplacer{ + { + name: "testAddMissingRemoteEnvironmentInMetric", + input: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "", false), + output: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "test", false), + isTrace: false, + }, + { + name: "testAddMissingRemoteEnvironmentInTrace", + input: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "", true), + output: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "test", true), + isTrace: true, + }, + { + name: "testReplaceRemoteEnvironmentInMetric", + input: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "error", false), + output: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "test", false), + isTrace: false, + }, + { + name: "testReplaceRemoteEnvironmentInTrace", + input: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "error", true), + output: generateAttributesWithEnv("app", "PUT /api/customer/owners/12345", "test", + "remote-app", "GET", "test", true), + isTrace: true, + }, + } + + testMapPlaceHolder := pcommon.NewMap() + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + assert.NoError(t, testReplacer.Process(tt.input, testMapPlaceHolder, tt.isTrace)) + assert.Equal(t, tt.output, tt.input) + }) + } +} + +func TestReplacerProcessWithPriority(t *testing.T) { + + config := []Rule{ + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "* /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "*", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "ListPetsByCustomer", + }, + { + TargetDimension: "Operation", + Value: "PUT/GET", + }, + }, + Action: "replace", + }, + { + Selectors: []Selector{ + { + Dimension: "Operation", + Match: "PUT /api/visits/*", + }, + { + Dimension: "RemoteOperation", + Match: "PUT *", + }, + }, + Replacements: []Replacement{ + { + TargetDimension: "RemoteOperation", + Value: "PUT visits", + }, + { + TargetDimension: "Operation", + Value: "PUT", + }, + }, + Action: "replace", + }, + } + + testReplacer := NewReplacer(config, false) + testMapPlaceHolder := pcommon.NewMap() + + testCases := []TestCaseForReplacer{ + { + name: "test01TraceMatchPreviousOne", + input: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + output: generateTestAttributes("replace-test", "PUT/GET", "customer-test", + "ListPetsByCustomer", true), + isTrace: true, + }, + { + name: "test02TraceBothMatch", + input: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "PUT /api/owners/123456", true), + output: generateTestAttributes("replace-test", "PUT", "customer-test", + "PUT visits", true), + isTrace: true, + }, + { + name: "test03MetricMatchPreviousOne", + input: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + output: generateTestAttributes("replace-test", "PUT/GET", "customer-test", + "ListPetsByCustomer", false), + isTrace: false, + }, + { + name: "test04MetricBothMatch", + input: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "PUT owners", false), + output: generateTestAttributes("replace-test", "PUT", "customer-test", + "PUT visits", false), + isTrace: false, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + assert.NoError(t, testReplacer.Process(tt.input, testMapPlaceHolder, tt.isTrace)) + assert.Equal(t, tt.output, tt.input) + }) + } +} + +func TestReplacerProcessWithNilConfig(t *testing.T) { + + testReplacer := NewReplacer(nil, false) + testMapPlaceHolder := pcommon.NewMap() + + testCases := []TestCaseForReplacer{ + { + name: "test01Trace", + input: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + output: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + isTrace: true, + }, + { + name: "test02Metric", + input: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + output: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + isTrace: false, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + assert.NoError(t, testReplacer.Process(tt.input, testMapPlaceHolder, tt.isTrace)) + assert.Equal(t, tt.output, tt.input) + }) + } +} + +func TestReplacerProcessWithEmptyConfig(t *testing.T) { + + config := []Rule{} + + testReplacer := NewReplacer(config, false) + testMapPlaceHolder := pcommon.NewMap() + + testCases := []TestCaseForReplacer{ + { + name: "test01Trace", + input: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + output: generateTestAttributes("replace-test", "PUT /api/visits/test/123456", "customer-test", + "GET", true), + isTrace: true, + }, + { + name: "test02Metric", + input: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + output: generateTestAttributes("replace-test", "PUT /api/visits/owners/12345", "customer-test", + "GET", false), + isTrace: false, + }, + } + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + assert.NoError(t, testReplacer.Process(tt.input, testMapPlaceHolder, tt.isTrace)) + assert.Equal(t, tt.output, tt.input) + }) + } +} diff --git a/processor/awsapplicationsignalsprocessor/testdata/config_eks.yaml b/processor/awsapplicationsignalsprocessor/testdata/config_eks.yaml new file mode 100644 index 000000000000..16b4c026f8b5 --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/testdata/config_eks.yaml @@ -0,0 +1,31 @@ +awsapplicationsignals: + resolvers: + - platform: eks + name: test + rules: + - selectors: + - dimension: Operation + match: "* /api/visits/*" + - dimension: RemoteOperation + match: "*" + action: keep + rule_name: "keep01" + + - selectors: + - dimension: RemoteService + match: "UnknownRemoteService" + - dimension: RemoteOperation + match: "GetShardIterator" + action: drop + + - selectors: + - dimension: Operation + match: "* /api/visits/*" + - dimension: RemoteOperation + match: "*" + replacements: + - target_dimension: RemoteOperation + value: ListPetsByCustomer + - target_dimension: ResourceTarget + value: ' ' + action: replace \ No newline at end of file diff --git a/processor/awsapplicationsignalsprocessor/testdata/config_generic.yaml b/processor/awsapplicationsignalsprocessor/testdata/config_generic.yaml new file mode 100644 index 000000000000..ba3a82d532ac --- /dev/null +++ b/processor/awsapplicationsignalsprocessor/testdata/config_generic.yaml @@ -0,0 +1,30 @@ +awsapplicationsignals: + resolvers: + - platform: generic + rules: + - selectors: + - dimension: Operation + match: "* /api/visits/*" + - dimension: RemoteOperation + match: "*" + action: keep + rule_name: "keep01" + + - selectors: + - dimension: RemoteService + match: "UnknownRemoteService" + - dimension: RemoteOperation + match: "GetShardIterator" + action: drop + + - selectors: + - dimension: Operation + match: "* /api/visits/*" + - dimension: RemoteOperation + match: "*" + replacements: + - target_dimension: RemoteOperation + value: ListPetsByCustomer + - target_dimension: ResourceTarget + value: ' ' + action: replace \ No newline at end of file