diff --git a/docs/rules.md b/docs/rules.md index 8248fac5d..6524cc785 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -379,7 +379,86 @@ grpc.Dial(addr, func(v ...grpc.DialOption) []grpc.DialOption { - All packages referenced in `append_args` must be in the target module's `go.mod`. - Ellipsis calls without `variadic_type` are skipped with a logged warning. -### 5. Directive Rule +### 5. Struct Literal Wrapping Rule + +This rule wraps struct literal expressions (`SomeType{}`) at instantiation sites with instrumentation code. It is the struct equivalent of the Call Wrapping Rule. + +**Use Cases:** + +- Wrapping `&http.Transport{}` with an instrumented transport. +- Injecting middleware when `&http.Server{}` is created. +- Instrumenting configuration struct creation without modifying the consuming code. + +**Fields:** + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `struct_literal` | string | Yes | Qualified struct name: `package/path.StructName` | +| `match` | string | No | Match mode: `value-only`, `pointer-only`, or `any` (default: `any`) | +| `template` | string | Yes | Wrapper template with `{{ . }}` placeholder for the original literal | +| `imports` | map[string]string | No | Additional imports needed for injected code | + +**Match Modes:** + +- `value-only`: Only matches value literals like `Config{}`. Does not match `&Config{}`. +- `pointer-only`: Only matches pointer literals like `&Config{}`. Does not match `Config{}`. +- `any`: Matches both value and pointer literals. + +**Template System:** + +The `template` field uses Go's `text/template` package. The `{{ . }}` placeholder is replaced with the original struct literal expression (including the `&` prefix for pointer matches). + +**Examples:** + +#### Example 1: Wrapping HTTP Server Creation + +```yaml +wrap_http_server: + target: myapp/server + struct_literal: net/http.Server + match: pointer-only + template: | + func(s *http.Server) *http.Server { + s.Handler = otelhttp.NewHandler(s.Handler) + return s + }({{ . }}) + imports: + otelhttp: "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +``` + +This transforms `&http.Server{Addr: ":8080", Handler: mux}` into: + +```go +func(s *http.Server) *http.Server { + s.Handler = otelhttp.NewHandler(s.Handler) + return s +}(&http.Server{Addr: ":8080", Handler: mux}) +``` + +#### Example 2: Value-Only Wrapping + +```yaml +wrap_config: + target: main + struct_literal: main.Config + match: value-only + template: "WrapConfig({{ . }})" +``` + +This transforms `Config{Host: "localhost", Port: 8080}` into `WrapConfig(Config{Host: "localhost", Port: 8080})`. + +Pointer usages like `&Config{}` are not affected when `match` is `value-only`. + +**Important Notes:** + +- The `{{ . }}` placeholder represents the original struct literal expression. +- The template must produce a valid Go expression. +- When using `match: any`, be careful with templates that take the value's address — wrapping in an IIFE may prevent taking the address without first assigning to a variable. +- The `struct_literal` field uses the qualified format `package/path.StructName`. For same-package structs, use `packagename.StructName`. + +--- + +### 6. Directive Rule This rule instruments functions annotated with a magic comment (a "directive") by prepending templated Go code into their bodies. The template is rendered once per annotated function, and the resulting statements are inserted at the top of the function body. @@ -440,7 +519,7 @@ func foo() { - Functions without the directive comment are not affected. - Multiple functions in the same file can carry the directive; each gets the template applied independently with its own `{{FuncName}}`. -### 6. File Addition Rule +### 7. File Addition Rule This rule adds a new Go source file to the target package. @@ -480,7 +559,7 @@ add_file_with_extra_imports: log: "log" # Add extra import to the copied file ``` -### 7. Named Declaration Rule +### 8. Named Declaration Rule This rule targets a named package-level symbol (variable, constant, function, or type) and replaces its initializer with a new expression. It is the primary mechanism for overriding default values in third-party packages without modifying their source — for example, replacing a default HTTP transport with an instrumented one to enable distributed tracing. diff --git a/pkg/instrumentation/grpc/server/go.mod b/pkg/instrumentation/grpc/server/go.mod index 4c0864074..ed2a2fdc3 100644 --- a/pkg/instrumentation/grpc/server/go.mod +++ b/pkg/instrumentation/grpc/server/go.mod @@ -39,7 +39,7 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -49,8 +49,8 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/net v0.52.0 // indirect diff --git a/pkg/instrumentation/grpc/server/go.sum b/pkg/instrumentation/grpc/server/go.sum index ac7216fac..1707e1fb8 100644 --- a/pkg/instrumentation/grpc/server/go.sum +++ b/pkg/instrumentation/grpc/server/go.sum @@ -59,8 +59,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= @@ -79,16 +79,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= diff --git a/pkg/instrumentation/nethttp/client/go.mod b/pkg/instrumentation/nethttp/client/go.mod index 8e12094ef..76dbbeb20 100644 --- a/pkg/instrumentation/nethttp/client/go.mod +++ b/pkg/instrumentation/nethttp/client/go.mod @@ -37,7 +37,7 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -47,9 +47,9 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/net v0.52.0 // indirect diff --git a/pkg/instrumentation/nethttp/client/go.sum b/pkg/instrumentation/nethttp/client/go.sum index ac7216fac..1707e1fb8 100644 --- a/pkg/instrumentation/nethttp/client/go.sum +++ b/pkg/instrumentation/nethttp/client/go.sum @@ -59,8 +59,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= @@ -79,16 +79,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= diff --git a/pkg/instrumentation/nethttp/server/go.mod b/pkg/instrumentation/nethttp/server/go.mod index 572a3089f..239d577ee 100644 --- a/pkg/instrumentation/nethttp/server/go.mod +++ b/pkg/instrumentation/nethttp/server/go.mod @@ -40,7 +40,7 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -50,9 +50,9 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/net v0.52.0 // indirect diff --git a/pkg/instrumentation/nethttp/server/go.sum b/pkg/instrumentation/nethttp/server/go.sum index ac7216fac..1707e1fb8 100644 --- a/pkg/instrumentation/nethttp/server/go.sum +++ b/pkg/instrumentation/nethttp/server/go.sum @@ -59,8 +59,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= @@ -79,16 +79,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= diff --git a/pkg/instrumentation/redis/v9/go.mod b/pkg/instrumentation/redis/v9/go.mod index ba9621513..4b45366df 100644 --- a/pkg/instrumentation/redis/v9/go.mod +++ b/pkg/instrumentation/redis/v9/go.mod @@ -35,7 +35,7 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -45,9 +45,9 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect golang.org/x/net v0.52.0 // indirect diff --git a/pkg/instrumentation/redis/v9/go.sum b/pkg/instrumentation/redis/v9/go.sum index 69991cab9..b93e2640d 100644 --- a/pkg/instrumentation/redis/v9/go.sum +++ b/pkg/instrumentation/redis/v9/go.sum @@ -67,8 +67,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= @@ -87,16 +87,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= diff --git a/tool/internal/instrument/apply_struct_literal.go b/tool/internal/instrument/apply_struct_literal.go new file mode 100644 index 000000000..9de539197 --- /dev/null +++ b/tool/internal/instrument/apply_struct_literal.go @@ -0,0 +1,136 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package instrument + +import ( + "context" + "strings" + + "github.com/dave/dst" + "github.com/dave/dst/dstutil" + + "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/ex" + "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/rule" +) + +// applyStructLiteralRule transforms struct literals by wrapping them with +// code according to the provided template. +func (ip *InstrumentPhase) applyStructLiteralRule( + ctx context.Context, + r *rule.InstStructLiteralRule, + root *dst.File, +) error { + importAliases := collectImportAliases(root) + + // Determine expected import path and struct name + parts := strings.Split(r.StructLiteral, ".") + const minParts = 2 + if len(parts) < minParts { + return ex.Newf("invalid struct_literal %q, expected pkg.StructName", r.StructLiteral) + } + structName := parts[len(parts)-1] + importPath := strings.Join(parts[:len(parts)-1], ".") + + tmpl, err := newCallTemplate(r.Template) + if err != nil { + return ex.Wrapf(err, "invalid template in struct_literal rule") + } + + modified := false + + dstutil.Apply(root, func(cursor *dstutil.Cursor) bool { + node := cursor.Node() + + var compLit *dst.CompositeLit + var pointer bool + var targetNode dst.Expr + + // Case 1: pointer match &Struct{} + if unary, isUnary := node.(*dst.UnaryExpr); isUnary && unary.Op.String() == "&" { + if cl, isCl := unary.X.(*dst.CompositeLit); isCl { + compLit = cl + pointer = true + targetNode = unary + } + } else if cl, isCl := node.(*dst.CompositeLit); isCl { + // Check if it's inside a pointer & + if parentUnary, isParentUnary := cursor.Parent().(*dst.UnaryExpr); isParentUnary && + parentUnary.Op.String() == "&" && + parentUnary.X == cl { + return true // Ignore, it is handled at the UnaryExpr level + } + // Case 2: value match Struct{} + compLit = cl + pointer = false + targetNode = cl + } + + if compLit == nil { + return true + } + + // check type + if !matchesStructType(compLit.Type, importPath, structName, importAliases) { + return true + } + + // check match mode + if r.Match == "value-only" && pointer { + return true + } + if r.Match == "pointer-only" && !pointer { + return true + } + + // wrap + wrappedExpr, tmplErr := tmpl.compileExpression(targetNode) + if tmplErr != nil { + ip.Warn("Failed to compile template for struct literal", "error", tmplErr) + return true + } + + // clone and replace + cloned := dst.Clone(wrappedExpr) + cursor.Replace(cloned) + modified = true + + return true // continue visiting children of the wrapped expression if any (e.g. nested literals) + }, nil) + + if modified { + if addErr := ip.addRuleImports(ctx, root, r.Imports, r.Name); addErr != nil { + return addErr + } + ip.Info("Apply struct_literal rule", "rule", r) + } + + return nil +} + +func matchesStructType(expr dst.Expr, expectedPath, expectedName string, aliases map[string]string) bool { + // Case 1: Same-package struct (e.g., Config{} when struct_literal is "main.Config") + if ident, ok := expr.(*dst.Ident); ok { + return ident.Name == expectedName && ident.Path == "" + } + // Case 2: Imported struct (e.g., http.Server{} when struct_literal is "net/http.Server") + sel, ok := expr.(*dst.SelectorExpr) + if !ok { + return false + } + if sel.Sel.Name != expectedName { + return false + } + ident, ok := sel.X.(*dst.Ident) + if !ok { + return false + } + + pkgPath := ident.Path + if pkgPath != "" { + return pkgPath == expectedPath + } + + resolvedPath, ok := aliases[ident.Name] + return ok && resolvedPath == expectedPath +} diff --git a/tool/internal/instrument/apply_struct_literal_test.go b/tool/internal/instrument/apply_struct_literal_test.go new file mode 100644 index 000000000..e87abb373 --- /dev/null +++ b/tool/internal/instrument/apply_struct_literal_test.go @@ -0,0 +1,196 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package instrument + +import ( + "context" + "go/token" + "io" + "log/slog" + "strings" + "testing" + + "github.com/dave/dst/decorator" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/rule" +) + +func TestApplyStructLiteralRule(t *testing.T) { + tests := []struct { + name string + source string + ruleYaml string + expected string + }{ + { + name: "pointer match", + source: `package main + +import "net/http" + +func main() { + s := &http.Server{Addr: ":8080"} + _ = s +} +`, + ruleYaml: ` +struct_literal: "net/http.Server" +target: "main" +match: pointer-only +template: | + func(s *http.Server) *http.Server { + return s + }({{ . }}) +`, + expected: `package main + +import "net/http" + +func main() { + s := func(s *http.Server) *http.Server { + return s + }(&http.Server{Addr: ":8080"}) + _ = s +} +`, + }, + { + name: "value match", + source: `package main + +import "net/http" + +func main() { + s := http.Server{Addr: ":8080"} + _ = s +} +`, + ruleYaml: ` +struct_literal: "net/http.Server" +target: "main" +match: value-only +template: | + func(s http.Server) http.Server { + return s + }({{ . }}) +`, + expected: `package main + +import "net/http" + +func main() { + s := func(s http.Server) http.Server { + return s + }(http.Server{Addr: ":8080"}) + _ = s +} +`, + }, + { + name: "pointer mismatch (expects value)", + source: `package main + +import "net/http" + +func main() { + s := &http.Server{Addr: ":8080"} + _ = s +} +`, + ruleYaml: ` +struct_literal: "net/http.Server" +target: "main" +match: value-only +template: "wrapped({{ . }})" +`, + expected: `package main + +import "net/http" + +func main() { + s := &http.Server{Addr: ":8080"} + _ = s +} +`, + }, + { + name: "any match", + source: `package main + +import "net/http" + +func main() { + s := &http.Server{Addr: ":8080"} + v := http.Server{Addr: ":9090"} +} +`, + ruleYaml: ` +struct_literal: "net/http.Server" +target: "main" +match: any +template: "wrapped({{ . }})" +`, + expected: `package main + +import "net/http" + +func main() { + s := wrapped(&http.Server{Addr: ":8080"}) + v := wrapped(http.Server{Addr: ":9090"}) +} +`, + }, + { + name: "alias import", + source: `package main + +import myhttp "net/http" + +func main() { + s := &myhttp.Server{Addr: ":8080"} + _ = s +} +`, + ruleYaml: ` +struct_literal: "net/http.Server" +target: "main" +template: "wrapped({{ . }})" +`, + expected: `package main + +import myhttp "net/http" + +func main() { + s := wrapped(&myhttp.Server{Addr: ":8080"}) + _ = s +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip := &InstrumentPhase{ + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + r, err := rule.NewInstStructLiteralRule([]byte(tt.ruleYaml), "test-rule") + require.NoError(t, err) + + fset := token.NewFileSet() + file, err := decorator.ParseFile(fset, "test.go", tt.source, 0) + require.NoError(t, err) + + err = ip.applyStructLiteralRule(context.Background(), r, file) + require.NoError(t, err) + + var buf strings.Builder + err = decorator.Fprint(&buf, file) + require.NoError(t, err) + + assert.Equal(t, tt.expected, buf.String()) + }) + } +} diff --git a/tool/internal/instrument/instrument.go b/tool/internal/instrument/instrument.go index 4d3d2a3c6..b00680d0a 100644 --- a/tool/internal/instrument/instrument.go +++ b/tool/internal/instrument/instrument.go @@ -18,6 +18,7 @@ func groupRules(workDir string, rset *rule.InstRuleSet) map[string][]rule.InstRu file2rules := make(map[string][]rule.InstRule) addRulesToMap(rset.FuncRules, file2rules, rset.CgoFileMap, workDir) addRulesToMap(rset.StructRules, file2rules, rset.CgoFileMap, workDir) + addRulesToMap(rset.StructLiteralRules, file2rules, rset.CgoFileMap, workDir) addRulesToMap(rset.RawRules, file2rules, rset.CgoFileMap, workDir) addRulesToMap(rset.CallRules, file2rules, rset.CgoFileMap, workDir) addRulesToMap(rset.DirectiveRules, file2rules, rset.CgoFileMap, workDir) @@ -56,6 +57,8 @@ func (ip *InstrumentPhase) applyOneRule(ctx context.Context, r rule.InstRule, ro return true, ip.applyRawRule(ctx, rt, root) case *rule.InstCallRule: return false, ip.applyCallRule(ctx, rt, root) + case *rule.InstStructLiteralRule: + return false, ip.applyStructLiteralRule(ctx, rt, root) case *rule.InstDirectiveRule: return true, ip.applyDirectiveRule(ctx, rt, root) default: diff --git a/tool/internal/instrument/instrument_test.go b/tool/internal/instrument/instrument_test.go index cf32514d5..3ddae8022 100644 --- a/tool/internal/instrument/instrument_test.go +++ b/tool/internal/instrument/instrument_test.go @@ -104,15 +104,16 @@ func loadRulesYAML(t *testing.T, testName, sourceFile string) *rule.InstRuleSet yaml.Unmarshal(data, &rawRules) ruleSet := &rule.InstRuleSet{ - PackageName: mainPackage, - ModulePath: mainPackage, - FuncRules: make(map[string][]*rule.InstFuncRule), - StructRules: make(map[string][]*rule.InstStructRule), - RawRules: make(map[string][]*rule.InstRawRule), - CallRules: make(map[string][]*rule.InstCallRule), - DirectiveRules: make(map[string][]*rule.InstDirectiveRule), - DeclRules: make(map[string][]*rule.InstDeclRule), - FileRules: make([]*rule.InstFileRule, 0), + PackageName: mainPackage, + ModulePath: mainPackage, + FuncRules: make(map[string][]*rule.InstFuncRule), + StructRules: make(map[string][]*rule.InstStructRule), + StructLiteralRules: make(map[string][]*rule.InstStructLiteralRule), + RawRules: make(map[string][]*rule.InstRawRule), + CallRules: make(map[string][]*rule.InstCallRule), + DirectiveRules: make(map[string][]*rule.InstDirectiveRule), + DeclRules: make(map[string][]*rule.InstDeclRule), + FileRules: make([]*rule.InstFileRule, 0), } // Sort rule names to ensure deterministic order in tests @@ -128,6 +129,9 @@ func loadRulesYAML(t *testing.T, testName, sourceFile string) *rule.InstRuleSet ruleData, _ := yaml.Marshal(props) switch { + case props["struct_literal"] != nil: + r, _ := rule.NewInstStructLiteralRule(ruleData, name) + ruleSet.StructLiteralRules[sourceFile] = append(ruleSet.StructLiteralRules[sourceFile], r) case props["struct"] != nil: r, _ := rule.NewInstStructRule(ruleData, name) ruleSet.StructRules[sourceFile] = append(ruleSet.StructRules[sourceFile], r) diff --git a/tool/internal/instrument/testdata/golden/struct-literal-only/rules.yml b/tool/internal/instrument/testdata/golden/struct-literal-only/rules.yml new file mode 100644 index 000000000..19b4103b3 --- /dev/null +++ b/tool/internal/instrument/testdata/golden/struct-literal-only/rules.yml @@ -0,0 +1,5 @@ +wrap_config: + target: main + struct_literal: main.Config + match: value-only + template: "WrapConfig({{ . }})" diff --git a/tool/internal/instrument/testdata/golden/struct-literal-only/source.go b/tool/internal/instrument/testdata/golden/struct-literal-only/source.go new file mode 100644 index 000000000..8d27a01ce --- /dev/null +++ b/tool/internal/instrument/testdata/golden/struct-literal-only/source.go @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "fmt" + +type Config struct { + Host string + Port int +} + +func WrapConfig(c Config) Config { + fmt.Println("wrapped config") + return c +} + +func main() { + cfg := Config{Host: "localhost", Port: 8080} + _ = cfg +} diff --git a/tool/internal/instrument/testdata/golden/struct-literal-only/struct_literal_only.main.go.golden b/tool/internal/instrument/testdata/golden/struct-literal-only/struct_literal_only.main.go.golden new file mode 100644 index 000000000..e235604bc --- /dev/null +++ b/tool/internal/instrument/testdata/golden/struct-literal-only/struct_literal_only.main.go.golden @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "fmt" + +type Config struct { + Host string + Port int +} + +func WrapConfig(c Config) Config { + fmt.Println("wrapped config") + return c +} + +func main() { + cfg := WrapConfig(Config{Host: "localhost", Port: 8080}) + _ = cfg +} diff --git a/tool/internal/rule/base.go b/tool/internal/rule/base.go index 3a3e638fc..3eff27e72 100644 --- a/tool/internal/rule/base.go +++ b/tool/internal/rule/base.go @@ -44,30 +44,32 @@ func (ibr *InstBaseRule) GetVersion() string { return ibr.Version } // This structure is essential for the instrumentation process, as it allows the // tool to efficiently locate and apply the correct rules to the source code. type InstRuleSet struct { - PackageName string `json:"package_name"` - ModulePath string `json:"module_path"` - CgoFileMap map[string]string `json:"cgo_file_map,omitempty"` // go -> cgo - RawRules map[string][]*InstRawRule `json:"raw_rules"` - FuncRules map[string][]*InstFuncRule `json:"func_rules"` - StructRules map[string][]*InstStructRule `json:"struct_rules"` - CallRules map[string][]*InstCallRule `json:"call_rules"` - DirectiveRules map[string][]*InstDirectiveRule `json:"directive_rules"` - DeclRules map[string][]*InstDeclRule `json:"decl_rules"` - FileRules []*InstFileRule `json:"file_rules"` + PackageName string `json:"package_name"` + ModulePath string `json:"module_path"` + CgoFileMap map[string]string `json:"cgo_file_map,omitempty"` // go -> cgo + RawRules map[string][]*InstRawRule `json:"raw_rules"` + FuncRules map[string][]*InstFuncRule `json:"func_rules"` + StructRules map[string][]*InstStructRule `json:"struct_rules"` + StructLiteralRules map[string][]*InstStructLiteralRule `json:"struct_literal_rules"` + CallRules map[string][]*InstCallRule `json:"call_rules"` + DirectiveRules map[string][]*InstDirectiveRule `json:"directive_rules"` + DeclRules map[string][]*InstDeclRule `json:"decl_rules"` + FileRules []*InstFileRule `json:"file_rules"` } func NewInstRuleSet(importPath string) *InstRuleSet { return &InstRuleSet{ - PackageName: "", - ModulePath: importPath, - CgoFileMap: make(map[string]string), - RawRules: make(map[string][]*InstRawRule), - FuncRules: make(map[string][]*InstFuncRule), - StructRules: make(map[string][]*InstStructRule), - CallRules: make(map[string][]*InstCallRule), - DirectiveRules: make(map[string][]*InstDirectiveRule), - DeclRules: make(map[string][]*InstDeclRule), - FileRules: make([]*InstFileRule, 0), + PackageName: "", + ModulePath: importPath, + CgoFileMap: make(map[string]string), + RawRules: make(map[string][]*InstRawRule), + FuncRules: make(map[string][]*InstFuncRule), + StructRules: make(map[string][]*InstStructRule), + StructLiteralRules: make(map[string][]*InstStructLiteralRule), + CallRules: make(map[string][]*InstCallRule), + DirectiveRules: make(map[string][]*InstDirectiveRule), + DeclRules: make(map[string][]*InstDeclRule), + FileRules: make([]*InstFileRule, 0), } } @@ -76,6 +78,7 @@ func (irs *InstRuleSet) String() string { fmt.Sprintf("raw=%v", irs.RawRules), fmt.Sprintf("func=%v", irs.FuncRules), fmt.Sprintf("struct=%v", irs.StructRules), + fmt.Sprintf("struct_literal=%v", irs.StructLiteralRules), fmt.Sprintf("call=%v", irs.CallRules), fmt.Sprintf("directive=%v", irs.DirectiveRules), fmt.Sprintf("decl=%v", irs.DeclRules), @@ -88,6 +91,7 @@ func (irs *InstRuleSet) IsEmpty() bool { return irs == nil || (len(irs.FuncRules) == 0 && len(irs.StructRules) == 0 && + len(irs.StructLiteralRules) == 0 && len(irs.RawRules) == 0 && len(irs.CallRules) == 0 && len(irs.DirectiveRules) == 0 && @@ -118,6 +122,10 @@ func (irs *InstRuleSet) AddCallRule(file string, rule *InstCallRule) { addRule(file, rule, irs.CallRules) } +func (irs *InstRuleSet) AddStructLiteralRule(file string, rule *InstStructLiteralRule) { + addRule(file, rule, irs.StructLiteralRules) +} + func (irs *InstRuleSet) AddDirectiveRule(file string, rule *InstDirectiveRule) { addRule(file, rule, irs.DirectiveRules) } diff --git a/tool/internal/rule/struct_literal_rule.go b/tool/internal/rule/struct_literal_rule.go new file mode 100644 index 000000000..3e4444511 --- /dev/null +++ b/tool/internal/rule/struct_literal_rule.go @@ -0,0 +1,64 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package rule + +import ( + "strings" + + "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/ex" + "gopkg.in/yaml.v3" +) + +// InstStructLiteralRule represents a rule that wraps a struct literal with a custom template. +// For example, to wrap `net/http.Server{}` with `otelhttp.NewServer`: +// +// wrap_struct_literal: +// target: "*" +// struct_literal: "net/http.Server" +// match: "value-only" # value-only|pointer-only|any +// template: | +// func(s http.Server) http.Server { +// return s +// }({{ . }}) +type InstStructLiteralRule struct { + InstBaseRule `yaml:",inline"` + + StructLiteral string `json:"struct_literal" yaml:"struct_literal"` // The type name of the struct literal to be matched + Match string `json:"match" yaml:"match"` // "value-only", "pointer-only", or "any" + Template string `json:"template" yaml:"template"` // The Go template to wrap the literal +} + +// NewInstStructLiteralRule loads and validates an InstStructLiteralRule from YAML data. +func NewInstStructLiteralRule(data []byte, name string) (*InstStructLiteralRule, error) { + var r InstStructLiteralRule + if err := yaml.Unmarshal(data, &r); err != nil { + return nil, ex.Wrap(err) + } + if r.Name == "" { + r.Name = name + } + if err := r.validate(); err != nil { + return nil, ex.Wrapf(err, "invalid struct_literal rule %q", name) + } + return &r, nil +} + +func (r *InstStructLiteralRule) validate() error { + if strings.TrimSpace(r.StructLiteral) == "" { + return ex.Newf("struct_literal cannot be empty") + } + if strings.TrimSpace(r.Template) == "" { + return ex.Newf("template cannot be empty") + } + match := strings.ToLower(strings.TrimSpace(r.Match)) + switch match { + case "": + r.Match = "any" // default to any + case "value-only", "pointer-only", "any": + r.Match = match + default: + return ex.Newf("match must be 'value-only', 'pointer-only', or 'any'") + } + return nil +} diff --git a/tool/internal/rule/struct_literal_rule_test.go b/tool/internal/rule/struct_literal_rule_test.go new file mode 100644 index 000000000..840482d82 --- /dev/null +++ b/tool/internal/rule/struct_literal_rule_test.go @@ -0,0 +1,90 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package rule + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewInstStructLiteralRule(t *testing.T) { + tests := []struct { + name string + yaml string + expectError bool + validate func(*testing.T, *InstStructLiteralRule) + }{ + { + name: "valid rule", + yaml: ` +target: "*" +struct_literal: "net/http.Server" +match: pointer-only +template: | + func(s *http.Server) *http.Server { + return s + }({{ . }}) +`, + expectError: false, + validate: func(t *testing.T, r *InstStructLiteralRule) { + assert.Equal(t, "net/http.Server", r.StructLiteral) + assert.Equal(t, "pointer-only", r.Match) + assert.Contains(t, r.Template, "func(s *http.Server)") + }, + }, + { + name: "default match", + yaml: ` +target: "*" +struct_literal: "net/http.Server" +template: "wrapped({{ . }})" +`, + expectError: false, + validate: func(t *testing.T, r *InstStructLiteralRule) { + assert.Equal(t, "any", r.Match) + }, + }, + { + name: "invalid match", + yaml: ` +target: "*" +struct_literal: "net/http.Server" +match: "something-else" +template: "wrapped({{ . }})" +`, + expectError: true, + }, + { + name: "missing struct_literal", + yaml: ` +target: "*" +template: "wrapped({{ . }})" +`, + expectError: true, + }, + { + name: "missing template", + yaml: ` +target: "*" +struct_literal: "net/http.Server" +`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := NewInstStructLiteralRule([]byte(tt.yaml), "test-rule") + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, r) + } + } + }) + } +} diff --git a/tool/internal/setup/match.go b/tool/internal/setup/match.go index ba6ed231d..b982b9a92 100644 --- a/tool/internal/setup/match.go +++ b/tool/internal/setup/match.go @@ -37,6 +37,8 @@ const ( //nolint:nilnil // factory function func createRuleFromFields(raw []byte, name string, fields map[string]any) (rule.InstRule, error) { switch { + case fields["struct_literal"] != nil: + return rule.NewInstStructLiteralRule(raw, name) case fields["struct"] != nil: return rule.NewInstStructRule(raw, name) case fields["file"] != nil: @@ -253,6 +255,10 @@ func (sp *SetupPhase) matchOneRule( // Files without matching calls are a no-op in applyCallRule. set.AddCallRule(source, rt) sp.Info("Match call rule", "rule", rt, "dep", dep) + case *rule.InstStructLiteralRule: + // Struct literal rules are added unconditionally, just like call rules. + set.AddStructLiteralRule(source, rt) + sp.Info("Match struct literal rule", "rule", rt, "dep", dep) case *rule.InstDirectiveRule: if ast.FileHasDirective(tree, rt.Directive) { set.AddDirectiveRule(source, rt)