From 981e9d37d1914cb0f52cf242dad8a9fbb8791bd1 Mon Sep 17 00:00:00 2001 From: Mayank Pande Date: Mon, 6 Apr 2026 11:44:16 +0530 Subject: [PATCH 1/6] deps: update logparser with structured log level detection (#235) Updates github.com/nudgebee/logparser to include single-pass structured log parsing that extracts level from JSON/logfmt fields instead of unreliable text scanning (nudgebee/logparser#18). Fixes false positive log level classification where INFO logs containing "error"/"fatal" in message content were misclassified as ERROR/CRITICAL. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b122fc9..7a8f500 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jpillora/backoff v1.0.0 github.com/mdlayher/taskstats v0.0.0-20230712191918-387b3d561d14 - github.com/nudgebee/logparser v0.0.0-20260313095946-b22168b95d9c + github.com/nudgebee/logparser v0.0.0-20260406040008-c408d0a5c69c github.com/opencontainers/runtime-spec v1.1.0 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 diff --git a/go.sum b/go.sum index 991435d..18b9413 100644 --- a/go.sum +++ b/go.sum @@ -325,6 +325,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nudgebee/logparser v0.0.0-20260313095946-b22168b95d9c h1:C0Jx8+61ndlWeKrS25W7hFJW/qpxUpG96tBQdjffd5I= github.com/nudgebee/logparser v0.0.0-20260313095946-b22168b95d9c/go.mod h1:oFnM9D6YEjZzb1jy0kJ/NUkTbkZA+BgNYjpLO4n4szA= +github.com/nudgebee/logparser v0.0.0-20260406040008-c408d0a5c69c h1:drsGFCiBXp5OZm92HE0iVx8U4XhpzAxL5vVKubBGl+I= +github.com/nudgebee/logparser v0.0.0-20260406040008-c408d0a5c69c/go.mod h1:oFnM9D6YEjZzb1jy0kJ/NUkTbkZA+BgNYjpLO4n4szA= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= From 5e82fa5e3a587db67e000d6c106a031504bd9dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:51:08 +0530 Subject: [PATCH 2/6] chore(deps): bump google.golang.org/grpc from 1.76.0 to 1.79.3 (#229) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.76.0 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.76.0...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 18 +++++++++--------- go.sum | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 7a8f500..3094580 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( go.opentelemetry.io/otel/trace v1.40.0 golang.org/x/arch v0.4.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/net v0.46.0 + golang.org/x/net v0.48.0 golang.org/x/sys v0.40.0 golang.org/x/time v0.14.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 @@ -187,15 +187,15 @@ require ( go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 18b9413..2bf2129 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= @@ -58,8 +58,8 @@ github.com/cilium/workerpool v1.2.0 h1:Wc2iOPTvCgWKQXeq4L5tnx4QFEI+z5q1+bSpSS0cn github.com/cilium/workerpool v1.2.0/go.mod h1:GOYJhwlnIjR+jWSDNBb5kw47G1H/XA9X4WOBpgr4pQU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= @@ -112,8 +112,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= @@ -500,18 +500,18 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -542,13 +542,13 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -560,8 +560,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn 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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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= @@ -575,17 +575,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 8797cdb4a3e1d0f574df06963745e561cb248030 Mon Sep 17 00:00:00 2001 From: Mayank Pande Date: Tue, 7 Apr 2026 15:35:51 +0530 Subject: [PATCH 3/6] fix: prevent OOM crash from L7 protocol misidentification (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ClickHouse eBPF detector matched non-ClickHouse TCP payloads using a 3-byte heuristic (0x01, 0x00, 0x01), causing ch-go to decode garbage varint lengths and attempt 228TB allocations — a fatal OOM that bypasses defer/recover. 3-layer defense: Layer 0 — eBPF detection hardening: Remove non-port-gated is_clickhouse_query() call. ClickHouse native protocol detection now only on ports 9000/8123. Layer 1 — Bounded parsing: Wrap ch-go proto.NewReader with io.LimitReader(payload size). Varint lengths exceeding actual payload now return io.EOF instead of OOM. Layer 2 — Userspace validation: Add structural checks before invoking external libraries: - ClickHouse: validate query code byte + query ID length - Postgres: reject unknown frame types (Q/B/P/C only) - Zookeeper: validate opcode in known range Layer 3 — Protocol reclassification: Track consecutive parse failures per connection. After 3 failures, override the cached protocol to stop further misidentified parsing. Logs a warning for observability. --- containers/container.go | 47 ++++++++++++++++++++++++++++++++++++- ebpftracer/ebpf/l7/l7.c | 2 -- ebpftracer/l7/clickhouse.go | 26 +++++++++++++++++++- ebpftracer/l7/postgres.go | 9 +++++++ ebpftracer/l7/zookeeper.go | 14 +++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/containers/container.go b/containers/container.go index af4ea03..d7f26fe 100644 --- a/containers/container.go +++ b/containers/container.go @@ -102,6 +102,9 @@ type ActiveConnection struct { http2Parser *l7.Http2Parser postgresParser *l7.PostgresParser mysqlParser *l7.MysqlParser + + parseFailCount int + protocolOverride l7.Protocol // non-zero = override eBPF-detected protocol } type ListenDetails struct { @@ -1088,8 +1091,16 @@ func (c *Container) onL7RequestWithResult(pid uint32, fd uint64, timestamp uint6 trace = c.tracer.NewTrace(conn.DestinationKey.ActualDestinationIfKnown(), conn.srcWorkload, destWorkload, actualDestWorkload) } + // Protocol reclassification: if previous parse attempts failed repeatedly, + // the connection was likely misidentified by eBPF heuristics. Use the + // override to skip further parsing for this connection. + protocol := r.Protocol + if conn.protocolOverride != 0 { + protocol = conn.protocolOverride + } + // Process L7 requests and update metrics - switch r.Protocol { + switch protocol { case l7.ProtocolDNS: status := r.Status.DNS() if status == "" { @@ -1282,6 +1293,11 @@ func (c *Container) onL7RequestWithResult(pid uint32, fd uint64, timestamp uint6 conn.postgresParser = l7.NewPostgresParser() } query := conn.postgresParser.Parse(r.Payload) + if query == "" && r.Method != l7.MethodStatementClose { + c.trackParseFail(conn, pid, fd, r.Protocol) + } else { + conn.parseFailCount = 0 + } if trace != nil { trace.PostgresQuery(query, r.Status.Error(), r.Duration) } @@ -1331,6 +1347,11 @@ func (c *Container) onL7RequestWithResult(pid uint32, fd uint64, timestamp uint6 // Update stats for Clickhouse c.l7Stats.observe(r.Protocol, r.Status.String(), "", "", r.Duration, conn.DestinationKey, conn.srcWorkload, r, "") query := l7.ParseClickhouse(r.Payload) + if query == "" { + c.trackParseFail(conn, pid, fd, r.Protocol) + } else { + conn.parseFailCount = 0 + } if trace != nil { trace.ClickhouseQuery(query, r.Status.Error(), r.Duration) } @@ -1338,6 +1359,11 @@ func (c *Container) onL7RequestWithResult(pid uint32, fd uint64, timestamp uint6 // Update stats for Zookeeper c.l7Stats.observe(r.Protocol, r.Status.Zookeeper(), "", "", r.Duration, conn.DestinationKey, conn.srcWorkload, r, "") op, arg := l7.ParseZookeeper(r.Payload) + if op == "" { + c.trackParseFail(conn, pid, fd, r.Protocol) + } else { + conn.parseFailCount = 0 + } if trace != nil { trace.ZookeeperRequest(op, arg, r.Status, r.Duration) } @@ -1351,6 +1377,25 @@ func (c *Container) onL7RequestWithResult(pid uint32, fd uint64, timestamp uint6 return nil, L7RequestProcessed } +const ( + parseFailThreshold = 3 + protocolReclassified = l7.Protocol(0xFF) // sentinel: no protocol matches, hits default case +) + +// trackParseFail tracks consecutive parse failures on a connection. After +// parseFailThreshold failures, the connection's protocol is overridden to +// skip further parsing. This handles eBPF protocol misidentification where +// weak heuristics (e.g., 3-byte ClickHouse check) tag a non-matching +// connection permanently. +func (c *Container) trackParseFail(conn *ActiveConnection, pid uint32, fd uint64, proto l7.Protocol) { + conn.parseFailCount++ + if conn.parseFailCount == parseFailThreshold { + conn.protocolOverride = protocolReclassified + klog.Warningf("reclassified connection pid=%d fd=%d from %s to unknown after %d consecutive parse failures", + pid, fd, proto, conn.parseFailCount) + } +} + // processHTTP2WithoutConnection handles HTTP/2 events when TCP connection tracking failed. // This is common for Go TLS connections where goroutines switch threads between // TCP connect and TLS write, causing fd_by_pid_tgid lookup to fail in eBPF. diff --git a/ebpftracer/ebpf/l7/l7.c b/ebpftracer/ebpf/l7/l7.c index c6a2169..9c473c4 100644 --- a/ebpftracer/ebpf/l7/l7.c +++ b/ebpftracer/ebpf/l7/l7.c @@ -408,8 +408,6 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size, req->protocol = PROTOCOL_RABBITMQ; } else if (is_amqp_method_frame(payload, size)) { req->protocol = PROTOCOL_RABBITMQ; - } else if (is_clickhouse_query(payload, size)) { - req->protocol = PROTOCOL_CLICKHOUSE; } else if (is_zk_request(payload, total_size)) { req->protocol = PROTOCOL_ZOOKEEPER; } else if (is_kafka_request(payload, size, &req->request_id)) { diff --git a/ebpftracer/l7/clickhouse.go b/ebpftracer/l7/clickhouse.go index a31a25f..626efba 100644 --- a/ebpftracer/l7/clickhouse.go +++ b/ebpftracer/l7/clickhouse.go @@ -2,10 +2,13 @@ package l7 import ( "bytes" + "io" "github.com/ClickHouse/ch-go/proto" ) +const clickhouseClientCodeQuery = 1 + func ParseClickhouse(payload []byte) (result string) { // Recover from panics caused by malformed/incomplete packets defer func() { @@ -14,7 +17,28 @@ func ParseClickhouse(payload []byte) (result string) { } }() - r := proto.NewReader(bytes.NewReader(payload)) + // Layer 2: Structural validation before invoking ch-go. + // Reject misidentified payloads early, before the library can + // decode garbage varint lengths and attempt unbounded allocations. + if len(payload) < 3 { + return "" + } + if payload[0] != clickhouseClientCodeQuery { + return "" + } + // Query ID length is varint-encoded. Single-byte varints (0-127) cover + // all practical query IDs. Multi-byte varints (>127) in this position + // indicate garbage data from a misidentified connection. + if payload[1] > 127 { + return "" + } + + // Layer 1: Bound the reader to the actual payload size. + // ch-go's proto.Reader decodes varint string lengths and pre-allocates + // that many bytes. With corrupted data, the varint can decode to TB-scale + // values, causing a fatal OOM that bypasses defer/recover. + // LimitReader caps reads at len(payload), turning the OOM into io.EOF. + r := proto.NewReader(io.LimitReader(bytes.NewReader(payload), int64(len(payload)))) var err error if _, err = r.Byte(); err != nil { return "" diff --git a/ebpftracer/l7/postgres.go b/ebpftracer/l7/postgres.go index 148a120..5d23c25 100644 --- a/ebpftracer/l7/postgres.go +++ b/ebpftracer/l7/postgres.go @@ -26,6 +26,15 @@ func (p *PostgresParser) Parse(payload []byte) string { return "" } cmd := payload[0] + // Reject unknown frame types early — Postgres detection in eBPF is weak + // (single-byte check), so misidentified connections can reach here. + // Accept all valid frontend message types per the Postgres wire protocol. + switch cmd { + case PostgresFrameQuery, PostgresFrameBind, PostgresFrameParse, PostgresFrameClose, + 'E', 'D', 'S', 'H', 'X': // Execute, Describe, Sync, Flush, Terminate + default: + return "" + } switch cmd { case PostgresFrameQuery: var query string diff --git a/ebpftracer/l7/zookeeper.go b/ebpftracer/l7/zookeeper.go index 71a04d5..0f40d5a 100644 --- a/ebpftracer/l7/zookeeper.go +++ b/ebpftracer/l7/zookeeper.go @@ -108,11 +108,25 @@ func zkReadString(r io.Reader) string { return string(res[:n]) } +func isValidZkOp(op int32) bool { + switch op { + case zkOpCreate, zkOpDelete, zkOpExists, zkOpGetData, zkOpSetData, + zkOpGetAcl, zkOpSetAcl, zkOpGetChildren, zkOpSync, zkOpPing, + zkOpGetChildren2, zkOpCheck, zkOpMulti, zkOpReconfig, + zkOpCreateContainer, zkOpCreateTTL, zkOpClose, zkOpSetAuth, zkOpSetWatches: + return true + } + return false +} + func ParseZookeeper(payload []byte) (string, string) { r := bytes.NewReader(payload) h := zkRequestHeader{} if err := binary.Read(r, binary.BigEndian, &h); err != nil { return "", "" } + if !isValidZkOp(h.OpType) { + return "", "" + } return zkParse(r, h.OpType) } From 9b11a75df98f455275821dc33bbe8e0659397ebd Mon Sep 17 00:00:00 2001 From: Mayank Pande Date: Mon, 13 Apr 2026 12:54:36 +0530 Subject: [PATCH 4/6] fix: HTTP2 nil pointer panic and broken GOMEMLIMIT under hostPID (#240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: HTTP2 nil pointer panic and broken GOMEMLIMIT under hostPID Two fixes found during production health check across 3 clusters: 1. HTTP2 nil pointer dereference (ebpftracer/l7/http2.go): When maxActiveRequests limit is hit, the break exits the switch but decoder.Write() still runs with a stale emit func from a previous call, dereferencing a nil request. Set a no-op emit func before breaking so HPACK dynamic table stays in sync without the panic. This was causing 100K+ recovered panics per 6h across KP and DEV, wasting CPU and inflating page cache by ~150Mi per affected pod. 2. GOMEMLIMIT never set under hostPID (main.go): readCgroupMemoryLimit() reads /sys/fs/cgroup/memory.max which doesn't exist under the agent's init.scope cgroup (hostPID places PID 1 at the host root). The pod's actual 1Gi limit lives at /sys/fs/cgroup/kubepods.slice/.../cri-containerd-xxx.scope/memory.max. Fix: read /proc/self/cgroup to resolve the container's own cgroup path. Without this, GOMEMLIMIT was dead code on all clusters — GC didn't trigger during ELF parsing spikes, letting RSS hit 1Gi and causing OOM kills. * fix: handle cgroup v1 under hostPID in readCgroupMemoryLimit Parse /proc/self/cgroup for both v2 (hierarchy-id "0") and v1 ("memory" controller) to resolve the container's own cgroup path. Previously only v2 was handled via /proc/self/cgroup; v1 clusters with hostPID would still fall through to the root path which reports the host limit, not the pod limit. --- ebpftracer/l7/http2.go | 5 ++++- main.go | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ebpftracer/l7/http2.go b/ebpftracer/l7/http2.go index 7c484d5..13df64e 100644 --- a/ebpftracer/l7/http2.go +++ b/ebpftracer/l7/http2.go @@ -263,7 +263,10 @@ func (p *Http2Parser) decodeHeaderBlock( req := p.activeRequests[streamId] if req == nil { if len(p.activeRequests) >= maxActiveRequests { - // Too many active streams; skip to prevent unbounded growth + // Too many active streams; set no-op emit so decoder.Write still + // processes the HPACK block (keeps dynamic table in sync) without + // dereferencing a nil request. + decoder.SetEmitFunc(func(hf hpack.HeaderField) {}) break } req = &Http2Request{ diff --git a/main.go b/main.go index b200bfc..dfbe008 100644 --- a/main.go +++ b/main.go @@ -297,8 +297,38 @@ func (o *RateLimitedLogOutput) Write(data []byte) (int, error) { } // readCgroupMemoryLimit reads the memory limit from cgroup v2 or v1. +// The agent runs with hostPID, so /proc/1/cgroup points to the host root (init.scope) +// which has no memory limit. We use /proc/self/cgroup to find the container's own +// cgroup path where kubelet enforces the pod memory limit. func readCgroupMemoryLimit() (int64, error) { - // Try cgroup v2 first + // Parse /proc/self/cgroup to find the container's own cgroup path. + // cgroupv2: "0::/kubepods.slice/.../cri-containerd-xxx.scope" + // cgroupv1: "N:memory:/kubepods/burstable/pod-xxx/container-yyy" + if data, err := os.ReadFile("/proc/self/cgroup"); err == nil { + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + parts := strings.SplitN(line, ":", 3) + if len(parts) < 3 { + continue + } + var cgPath string + if parts[0] == "0" { // cgroup v2 + cgPath = "/sys/fs/cgroup" + parts[2] + "/memory.max" + } else if strings.Contains(parts[1], "memory") { // cgroup v1 + cgPath = "/sys/fs/cgroup/memory" + parts[2] + "/memory.limit_in_bytes" + } + if cgPath != "" { + if mem, err := os.ReadFile(cgPath); err == nil { + s := strings.TrimSpace(string(mem)) + if s != "max" { + if limit, err := strconv.ParseInt(s, 10, 64); err == nil && limit > 0 && limit < 1<<62 { + return limit, nil + } + } + } + } + } + } + // Fallback: try cgroup v2 at root (non-hostPID containers) if data, err := os.ReadFile("/sys/fs/cgroup/memory.max"); err == nil { s := strings.TrimSpace(string(data)) if s != "max" { @@ -307,11 +337,10 @@ func readCgroupMemoryLimit() (int64, error) { } } } - // Try cgroup v1 + // Fallback: try cgroup v1 at root (non-hostPID containers) if data, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes"); err == nil { s := strings.TrimSpace(string(data)) if limit, err := strconv.ParseInt(s, 10, 64); err == nil { - // cgroup v1 reports a very large number when unlimited if limit < 1<<62 { return limit, nil } From 750bfdd67e0fff6bda40660fdec995a3b0530bf1 Mon Sep 17 00:00:00 2001 From: Mayank Pande Date: Mon, 13 Apr 2026 15:33:53 +0530 Subject: [PATCH 5/6] Merge pull request #241 from nudgebee/fix/aggregate-ephemeral-workloads fix: aggregate ephemeral workload names to prevent series cardinality explosion --- common/ip_resolver.go | 28 +++++ common/ip_resolver_test.go | 229 +++++++++++++++++++++++++++++++++---- flags/flags.go | 2 + 3 files changed, 238 insertions(+), 21 deletions(-) diff --git a/common/ip_resolver.go b/common/ip_resolver.go index 3568001..a09e05a 100644 --- a/common/ip_resolver.go +++ b/common/ip_resolver.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/coroot/coroot-node-agent/flags" lrucache "github.com/hashicorp/golang-lru/v2" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -829,6 +830,27 @@ func (resolver *K8sIPResolver) getControllerOfOwner(owner *metav1.OwnerReference return getControllerOwnerRef(info.OwnerReferences), nil } +// workloadIdentityLabels defines the priority order for finding a stable +// workload identity from pod labels when no higher-level controller exists. +var workloadIdentityLabels = []string{ + "app.kubernetes.io/name", + "app", + "app.kubernetes.io/component", + "component", +} + +// resolveEphemeralWorkloadName returns a stable workload name for bare pods +// and standalone Jobs by checking standard Kubernetes labels. Falls back to +// "-ephemeral" to prevent cardinality explosion from unique pod names. +func resolveEphemeralWorkloadName(labels map[string]string, namespace string) string { + for _, key := range workloadIdentityLabels { + if val, ok := labels[key]; ok && val != "" { + return val + } + } + return namespace + "-ephemeral" +} + func (resolver *K8sIPResolver) resolvePodDescriptor(pod *MinimalPod) Workload { existing, ok := resolver.snapshot.PodDescriptors.Load(pod.UID) if ok { @@ -866,6 +888,12 @@ func (resolver *K8sIPResolver) resolvePodDescriptor(pod *MinimalPod) Workload { } } + // Aggregate ephemeral workloads (bare pods and standalone Jobs) to prevent + // cardinality explosion from unique pod/job names (e.g. Airflow task pods). + if *flags.AggregateEphemeralWorkloads && (kind == "pod" || kind == "Job") { + name = resolveEphemeralWorkloadName(pod.Labels, namespace) + } + instanceMeta, ok := resolver.instanceMetaMap.Load(pod.NodeName) region := "" zone := "" diff --git a/common/ip_resolver_test.go b/common/ip_resolver_test.go index 026be10..8872182 100644 --- a/common/ip_resolver_test.go +++ b/common/ip_resolver_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/coroot/coroot-node-agent/flags" "github.com/google/uuid" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" @@ -25,6 +26,7 @@ type podDescriptor struct { Phase v1.PodPhase UID types.UID Controller *workloadResourceDescriptor + Labels map[string]string } type nodeDescriptor struct { @@ -122,6 +124,7 @@ func generatePod(pod podDescriptor) runtime.Object { Name: pod.Name, Namespace: pod.Namespace, UID: pod.UID, + Labels: pod.Labels, }, Status: v1.PodStatus{ PodIP: pod.IP, @@ -397,7 +400,7 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.2": { @@ -413,7 +416,7 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -429,9 +432,9 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, - {"pod2", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID(uuid.New().String()), nil}, - {"pod3", "namespaceA", "1.1.1.3", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, + {"pod2", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, + {"pod3", "namespaceA", "1.1.1.3", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -468,7 +471,7 @@ func TestResolving(t *testing.T) { { shouldWait: true, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -524,7 +527,7 @@ func TestResolving(t *testing.T) { { shouldWait: true, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, newNodes: []nodeDescriptor{ {"Node1", "1.1.1.0", types.UID(uuid.NewString())}, @@ -549,7 +552,7 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -568,7 +571,7 @@ func TestResolving(t *testing.T) { { shouldWait: true, modifiedPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { // the resolver shouldn't delete old not-reused entries @@ -590,7 +593,7 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID("1"), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID("1"), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -609,14 +612,14 @@ func TestResolving(t *testing.T) { { shouldWait: false, modifiedPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID("1"), nil}, + {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID("1"), nil, nil}, }, expectedResolves: map[string]Workload{}, }, { shouldWait: true, newPods: []podDescriptor{ - {"pod2", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod2", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -638,7 +641,7 @@ func TestResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID("1"), nil}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID("1"), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.1": { @@ -657,7 +660,7 @@ func TestResolving(t *testing.T) { { shouldWait: false, modifiedPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID("1"), nil}, + {"pod1", "namespaceA", "1.1.1.2", v1.PodRunning, types.UID("1"), nil, nil}, }, expectedResolves: map[string]Workload{}, }, @@ -766,7 +769,7 @@ func TestResolving(t *testing.T) { { shouldWait: true, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.0", v1.PodRunning, types.UID(uuid.New().String()), nil}, + {"pod1", "namespaceA", "1.1.1.0", v1.PodRunning, types.UID(uuid.New().String()), nil, nil}, }, expectedResolves: map[string]Workload{ "1.1.1.0": { @@ -793,7 +796,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testDeployment}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testDeployment, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testDeployment}, expectedResolves: map[string]Workload{ @@ -810,7 +813,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testReplicaSet}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testReplicaSet, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testReplicaSet}, expectedResolves: map[string]Workload{ @@ -827,7 +830,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testDaemonSet}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testDaemonSet, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testDaemonSet}, expectedResolves: map[string]Workload{ @@ -844,7 +847,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testStatefulSet}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testStatefulSet, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testStatefulSet}, expectedResolves: map[string]Workload{ @@ -861,7 +864,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testJob}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testJob, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testJob}, expectedResolves: map[string]Workload{ @@ -878,7 +881,7 @@ func TestControllersResolving(t *testing.T) { initialState: testStep{ shouldWait: false, newPods: []podDescriptor{ - {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testCronjob}, + {"pod1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testCronjob, nil}, }, newWorkloadResource: []workloadResourceDescriptor{testCronjob}, expectedResolves: map[string]Workload{ @@ -897,3 +900,187 @@ func TestControllersResolving(t *testing.T) { }) } } + +func TestResolveEphemeralWorkloadName(t *testing.T) { + tests := []struct { + name string + labels map[string]string + namespace string + expected string + }{ + { + name: "app.kubernetes.io/name label", + labels: map[string]string{"app.kubernetes.io/name": "airflow-worker"}, + namespace: "airflow", + expected: "airflow-worker", + }, + { + name: "app label", + labels: map[string]string{"app": "my-batch-job"}, + namespace: "data", + expected: "my-batch-job", + }, + { + name: "component label", + labels: map[string]string{"component": "dag-runner"}, + namespace: "airflow", + expected: "dag-runner", + }, + { + name: "app.kubernetes.io/name takes priority over app", + labels: map[string]string{"app.kubernetes.io/name": "preferred", "app": "fallback"}, + namespace: "ns", + expected: "preferred", + }, + { + name: "no matching labels falls back to namespace-ephemeral", + labels: map[string]string{"dag_id": "etl_pipeline", "run_id": "abc123"}, + namespace: "airflow", + expected: "airflow-ephemeral", + }, + { + name: "nil labels falls back to namespace-ephemeral", + labels: nil, + namespace: "default", + expected: "default-ephemeral", + }, + { + name: "empty label value is skipped", + labels: map[string]string{"app": "", "component": "worker"}, + namespace: "airflow", + expected: "worker", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveEphemeralWorkloadName(tt.labels, tt.namespace) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestEphemeralWorkloadAggregation(t *testing.T) { + orig := flags.AggregateEphemeralWorkloads + enabled := true + flags.AggregateEphemeralWorkloads = &enabled + t.Cleanup(func() { flags.AggregateEphemeralWorkloads = orig }) + + var tests = []testScenario{ + { + description: "bare pod with app label aggregates to label value", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"abbvie-val-m24-108-spdm-7ugt2ge6", "airflow", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), nil, map[string]string{"app": "airflow-task"}}, + }, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: "airflow-task", + Namespace: "airflow", + Kind: "pod", + }, + }, + }, + }, + { + description: "bare pod without standard labels aggregates to namespace-ephemeral", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"abbvie-val-m24-108-spdm-7ugt2ge6", "airflow", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), nil, map[string]string{"dag_id": "etl"}}, + }, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: "airflow-ephemeral", + Namespace: "airflow", + Kind: "pod", + }, + }, + }, + }, + { + description: "standalone Job aggregates to label value", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"job-pod-1", "airflow", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testJob, map[string]string{"app.kubernetes.io/name": "dag-executor"}}, + }, + newWorkloadResource: []workloadResourceDescriptor{testJob}, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: "dag-executor", + Namespace: "airflow", + Kind: "Job", + }, + }, + }, + }, + { + description: "CronJob-owned pod is NOT aggregated", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"cronjob-pod-1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testCronjob, map[string]string{"app": "should-not-use"}}, + }, + newWorkloadResource: []workloadResourceDescriptor{testCronjob}, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: testCronjob.Name, + Namespace: testCronjob.Namespace, + Kind: testCronjob.Kind, + }, + }, + }, + }, + { + description: "Deployment-owned pod is NOT aggregated", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"deploy-pod-1", "namespaceA", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), &testDeployment, map[string]string{"app": "should-not-use"}}, + }, + newWorkloadResource: []workloadResourceDescriptor{testDeployment}, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: testDeployment.Name, + Namespace: testDeployment.Namespace, + Kind: testDeployment.Kind, + }, + }, + }, + }, + { + description: "multiple ephemeral pods collapse to same workload name", + initialState: testStep{ + shouldWait: false, + newPods: []podDescriptor{ + {"task-pod-aaa", "airflow", "1.1.1.1", v1.PodRunning, types.UID(uuid.NewString()), nil, nil}, + {"task-pod-bbb", "airflow", "1.1.1.2", v1.PodRunning, types.UID(uuid.NewString()), nil, nil}, + {"task-pod-ccc", "airflow", "1.1.1.3", v1.PodRunning, types.UID(uuid.NewString()), nil, nil}, + }, + expectedResolves: map[string]Workload{ + "1.1.1.1": { + Name: "airflow-ephemeral", + Namespace: "airflow", + Kind: "pod", + }, + "1.1.1.2": { + Name: "airflow-ephemeral", + Namespace: "airflow", + Kind: "pod", + }, + "1.1.1.3": { + Name: "airflow-ephemeral", + Namespace: "airflow", + Kind: "pod", + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + runTest(t, test) + }) + } +} diff --git a/flags/flags.go b/flags/flags.go index 9a256ca..c7dd6d7 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -69,6 +69,8 @@ var ( HttpPathNormalizationRules = kingpin.Flag("http-path-normalization-rules", "Custom HTTP path normalization rules in format 'pattern1:replacement1,pattern2:replacement2'").Envar("HTTP_PATH_NORMALIZATION_RULES").String() + AggregateEphemeralWorkloads = kingpin.Flag("aggregate-ephemeral-workloads", "Aggregate metrics for bare pods and standalone Jobs using standard labels to reduce series cardinality").Default("true").Envar("AGGREGATE_EPHEMERAL_WORKLOADS").Bool() + agentVersion = kingpin.Flag("version", "Print version and exit").Default("false").Bool() Version = "unknown" ) From e7eadcf66b747c2c212d9184d08daf77b761c195 Mon Sep 17 00:00:00 2001 From: Mayank Pande Date: Mon, 20 Apr 2026 11:28:41 +0530 Subject: [PATCH 6/6] fix: suppress noisy uprobe symbol-not-found errors (#242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: suppress noisy uprobe "symbol not found" errors for non-matching binaries Binaries like calico-node match the nodejs detection regex but lack uv__io_poll. Similarly, some libpthread.so builds lack pthread_cond_timedwait. These are expected failures, not actionable errors — suppress them like other benign attachment failures. * fix: narrow suppression to "symbol ... not found" errors only Generic "not found" suffix could mask unrelated errors. Match the specific error format from elf.go GetSymbol instead. --- ebpftracer/nodejs.go | 3 +++ ebpftracer/python.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ebpftracer/nodejs.go b/ebpftracer/nodejs.go index 78cef98..3007958 100644 --- a/ebpftracer/nodejs.go +++ b/ebpftracer/nodejs.go @@ -19,6 +19,9 @@ func (t *Tracer) AttachNodejsProbes(pid uint32, exe string) []link.Link { return } } + if strings.Contains(err.Error(), "symbol") && strings.HasSuffix(err.Error(), "not found") { + return + } klog.ErrorfDepth(1, "pid=%d lib=%s: %s: %s", pid, libPath, msg, err) return } diff --git a/ebpftracer/python.go b/ebpftracer/python.go index f6565fe..90c4d12 100644 --- a/ebpftracer/python.go +++ b/ebpftracer/python.go @@ -25,6 +25,9 @@ func (t *Tracer) AttachPythonThreadLockProbes(pid uint32) []link.Link { return } } + if strings.Contains(err.Error(), "symbol") && strings.HasSuffix(err.Error(), "not found") { + return + } klog.ErrorfDepth(1, "pid=%d lib=%s: %s: %s", pid, libPath, msg, err) return }