17 Commits

Author SHA1 Message Date
e6016ceed8 cat query string at VALUES if is set to not log values in query 2024-11-14 20:30:18 +01:00
1230538295 remove println 2024-11-14 17:39:17 +01:00
280fbdda84 fix tag name to be included 2024-11-14 17:37:25 +01:00
deb7986cad check 2024-11-14 17:19:55 +01:00
03256699b2 check 2024-11-14 17:10:06 +01:00
7fbf9095f5 add tag name 2024-11-14 12:00:44 +01:00
39bf8b8356 feat: loosen the type restrictions on attr.Json
Just like `json.Marshal` takes arguments of `any` type, the `attr.Json`
should have similarly loose restrictions on input type.
2024-09-25 16:11:08 +02:00
a37354cb18 fix: message formatting errors in GELF exporter
The library used to transport GELF messages did not prefix extra fields
with `_` as was previously assumed and the application name was not
passed correctly from configuration to the exporter. Now fixed.

Related: goc_marek/salego#693
2024-09-25 11:23:06 +02:00
0f866a0ded fix: attributes incorrectly added as extra fields in gelf 2024-09-24 16:24:19 +02:00
4654238920 feat: add attributes from resource to gelf exports 2024-09-24 16:05:58 +02:00
019de457c9 feat: allow providing arbitrary extra attributes to fiber_tracing middleware resource 2024-09-24 15:59:57 +02:00
9a1b41b1ad feat: new helpers and masking/encrypting attributes
New helper functions were added to make call-site less likely to need
to pull `go.opentelemetry.io/otel/attribute` as a dependency.
Additionally `Encrypted` and `Masked` were added to add a possibility of
logging sensitive data in a more secure manner.
2024-09-13 15:48:16 +02:00
2004e1b2f5 feat: add a new single-line formatter for console_exporter
Closes #7
2024-09-10 15:53:36 +02:00
dc4d3942f5 feat: add gorm tracing middleware 2024-07-24 17:17:13 +02:00
9971ef17cb feat: add trace context propagation to http headers
Trace context should now be propagated correctly in headers of requests
and responses touched by the fiber_tracing middleware. This should enable
true distributed tracing between multiple services.
2024-07-24 15:19:51 +02:00
fc38f26e1f fix: numbers invisible in console exporter output
closes #5
2024-06-25 15:26:25 +02:00
225620da48 feat: enable filtering console output by severity level 2024-06-04 13:04:11 +02:00
14 changed files with 746 additions and 57 deletions

View File

@ -8,6 +8,7 @@ import (
"os/signal"
"time"
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/attr/layer_attr"
"git.ma-al.com/maal-libraries/observer/pkg/event"
"git.ma-al.com/maal-libraries/observer/pkg/exporters"
@ -23,7 +24,16 @@ func main() {
main := fiber.New()
exps := make([]exporters.TraceExporter, 0)
exps = append(exps, exporters.DevConsoleExporter(console_exporter.ProcessorOptions{}))
envFilter := "OBSERVER_CONSOLE"
singlelineFmt := console_exporter.NewSimpleSinglelineFormatter()
exps = append(exps, exporters.DevConsoleExporter(console_exporter.ProcessorOptions{
FilterFromEnvVar: &envFilter,
TraceFormatter: &singlelineFmt,
}))
gelfExp, err := exporters.GelfExporter()
if err == nil {
exps = append(exps, gelfExp)
@ -102,6 +112,11 @@ func Serv(ctx context.Context) *fiber.Error {
return fiber.NewError(500, "xd")
}
span.SetAttributes(
attr.Masked("some_masked_value", "some_masked_value"),
attr.Encrypted("some_encrypted_value", "some_encrypted_value"),
)
return fiber.NewError(500, "x")
}

5
go.mod
View File

@ -4,12 +4,13 @@ go 1.21
require (
github.com/gofiber/fiber/v2 v2.52.4
github.com/psmarcin/fiber-opentelemetry v1.2.0
github.com/gofrs/uuid v4.4.0+incompatible
go.opentelemetry.io/otel v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0
go.opentelemetry.io/otel/sdk v1.26.0
go.opentelemetry.io/otel/trace v1.26.0
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0
gorm.io/gorm v1.25.11
)
require (
@ -19,6 +20,8 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect

37
go.sum
View File

@ -1,28 +1,28 @@
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -34,23 +34,17 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psmarcin/fiber-opentelemetry v1.2.0 h1:3e3bz1jmwKMnoM5RVU4YzMXBq8tZQzzMDyM7DW1mTz8=
github.com/psmarcin/fiber-opentelemetry v1.2.0/go.mod h1:qcEVkzlD0GrjtCS+hd5/0QhbTOy12KNyaqmp4yfXi1c=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
@ -61,33 +55,18 @@ go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgS
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
@ -100,7 +79,7 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0 h1:Xg23ydYYJLmb9AK3XdcEpplHZd1MpN3X2ZeeMoBClmY=
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0/go.mod h1:CeDeqW4tj9FrgZXF/dQCWZrBdcZWWBenhJtxLH4On2g=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@ -1,18 +1,27 @@
package attr
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"io"
"os"
"runtime"
"runtime/debug"
"time"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"github.com/gofrs/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/semconv/v1.25.0"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"go.opentelemetry.io/otel/trace"
)
type KV = attribute.KeyValue
type KeyValue = attribute.KeyValue
type Key = attribute.Key
type Value = attribute.Value
@ -58,6 +67,7 @@ const (
ServiceLayerKey = attribute.Key("service.layer")
ServiceLayerNameKey = attribute.Key("service.layer_name")
DBExecutionTimeMsKey = attribute.Key("db.execution_time_ms")
DBRowsAffectedKey = attribute.Key("db.rows_affected")
)
type ServiceArchitectureLayer string
@ -71,11 +81,192 @@ const (
LayerUtil = "util"
)
type secretKey struct {
Key []byte
Cipher cipher.Block
Nonce []byte
}
// Interprets a string as AES secret key (32 bytes) first decoding it with base64
// It can be used to set the variable `EncryptSecretKey` which is responsible
// for encrypting the `Encrypted` attributes.
func NewSecretKey(key string) (secretKey, error) {
keyBytes, err := base64.RawStdEncoding.DecodeString(key)
if err != nil {
return secretKey{}, err
}
if len(keyBytes) != 32 {
return secretKey{}, errors.New("wrong length of encryption key, should be 32 bits")
}
cipher, err := aes.NewCipher(keyBytes)
if err != nil {
return secretKey{}, err
}
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return secretKey{}, err
}
return secretKey{
Key: keyBytes,
Cipher: cipher,
Nonce: nonce,
}, nil
}
// **Unless set, it will default to a random key that cannot be later retrievied!**
//
// The variable is used to encrypt values provided to the `Encrypted` attribute.
var EncryptSecretKey secretKey = func() secretKey {
key := make([]byte, 32)
rand.Read(key)
cipher, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err)
}
return secretKey{
Key: key,
Cipher: cipher,
Nonce: nonce,
}
}()
// Build an attribute with a value formatted as json
func JsonAttr(key string, jsonEl map[string]interface{}) attribute.KeyValue {
jsonStr, _ := json.Marshal(jsonEl)
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(jsonStr))}
}
// Create an arbitrary attribute with value marshalled to json.
// In case of marshalling error, it is returned in place of value.
func Json(key string, val any) attribute.KeyValue {
data, err := json.Marshal(val)
if err != nil {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())}
} else {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(data))}
}
}
// Create an arbitrary attribute with a `string` value.
func String(key string, val string) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val)}
}
// Create an arbitrary attribute with a `[]string` value.
func StringSlice(key string, val []string) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringSliceValue(val)}
}
// Create an arbitrary attribute with an `int` value.
func Int(key string, val int) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(val)}
}
// Create an arbitrary attribute with an `int64` value.
func Int64(key string, val int64) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.Int64Value(val)}
}
// Cast value to an `int` to create a new attribute.
func Uint(key string, val uint) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))}
}
// Cast value to an `int` to create a new attribute.
func Uint8(key string, val uint8) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))}
}
// Create an arbitrary attribute using an `uuid.UUID` from `github.com/gofrs/uuid` as value.
func Uuid(key string, val uuid.UUID) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.String())}
}
// Create an arbitrary attribute using standard library's `time.Time` as value. It will be formatted using RFC3339.
func Time(key string, val time.Time) attribute.KeyValue {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.Format(time.RFC3339))}
}
// Create an arbitrary attribute with bytes encoded to base64 (RFC 4648) as value.
func BytesB64(key string, val []byte) attribute.KeyValue {
res := base64.StdEncoding.EncodeToString(val)
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
}
// Create an arbitrary attribute with bytes encoded to hexadecimal format as value.
func BytesHex(key string, val []byte) attribute.KeyValue {
res := hex.EncodeToString(val)
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
}
// Create an arbitrary attribute with value encrypted using `secretKey` which should be set on
// global variable `EncryptSecretKey` using `NewSecretKey`. The result will be encoded using
// base64.
//
// This approach is an alternative to logs tokenization. It is using AES symmetric encryption
// that is suspectible to brute force attacks. It is a computionally expensive attribute to
// generate.
//
// In most cases, for very sensitive data it would be a better approach to use masking instead.
// Encrypting the fields of the logs/traces can provide an extra protection while they are being
// transported to a log collector and when the collector does not encrypt logs at rest (but most
// should implement this feature). This will mostly protect the logs from developers working
// with them provided that they do not have access to the key. The key should be set from an
// environment variable defined on application deployment. Alternatively it could be set from
// a secure vault, a software for storing private keys.
func Encrypted(key string, val string) attribute.KeyValue {
aesGcm, err := cipher.NewGCM(EncryptSecretKey.Cipher)
if err != nil {
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())}
}
resBytes := aesGcm.Seal(nil, EncryptSecretKey.Nonce, []byte(val), nil)
res := base64.StdEncoding.EncodeToString(resBytes)
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
}
// Creates an arbitrary attribute with masked value. It will leave only last 4 (or less) characters
// unmasked by 'X' characters.
//
// It is not a good idea to use it for logging passwords as it preserves the lenght of the input.
//
// Masking is a good idea for very sensitive data like official identity numbers, or addresses.
// Storing such data in logs is usually too much of a risk even when it is encrypted.
// However, for the purpose of debugging it might be convenient to be able to distinguish one record
// from another.
func Masked(key string, val string) attribute.KeyValue {
lenght := len(val)
var unmasked int
if lenght <= 4 {
unmasked = 1
} else {
if lenght <= 8 {
unmasked = 2
} else {
if lenght <= 12 {
unmasked = 3
} else {
unmasked = 4
}
}
}
masked := lenght - unmasked
resBytes := make([]byte, lenght)
i := 0
for ; i < masked; i++ {
resBytes[i] = byte('X')
}
for ; i < lenght; i++ {
resBytes[i] = byte(val[i])
}
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(resBytes))}
}
// An attribute informing about the severity or importance of an event using our own standard of log levels that
@ -266,3 +457,10 @@ func DBExecutionTimeMs(duration time.Duration) attribute.KeyValue {
Value: attribute.Int64Value(duration.Milliseconds()),
}
}
func DBRowsAffected(rows int64) attribute.KeyValue {
return attribute.KeyValue{
Key: DBRowsAffectedKey,
Value: attribute.Int64Value(rows),
}
}

View File

@ -3,6 +3,7 @@ package console_exporter
import (
"context"
"fmt"
"os"
"slices"
"git.ma-al.com/maal-libraries/observer/pkg/attr"
@ -22,7 +23,8 @@ type TraceFormatter interface {
// Most of options are passed to the formatter.
type ProcessorOptions struct {
// Try to parse filters from an environment variable with a name provided by this field.
// Result will only by applied to unset options. NOT IMPLEMENTED!
// Result will only by applied to unset options. Currently it will only attempt to parse
// severity level from the variable and use that as a filter.
FilterFromEnvVar *string
// Filter the output based on the [level.SeverityLevel].
FilterOnLevel level.SeverityLevel
@ -79,9 +81,16 @@ func NewProcessor(opts ProcessorOptions) trace.SpanProcessor {
fmt := NewPrettyMultilineFormatter()
formatter = fmt
}
if opts.FilterFromEnvVar != nil {
envFilter := os.Getenv(*opts.FilterFromEnvVar)
parsedLvl := level.FromString(envFilter)
if parsedLvl != level.SeverityLevel(0) {
lvl = parsedLvl
}
}
if opts.FilterOnLevel != level.SeverityLevel(0) {
lvl = opts.FilterOnLevel
} else {
} else if lvl == level.SeverityLevel(0) {
lvl = level.TRACE + 1
}

View File

@ -18,6 +18,30 @@ func NewPrettyMultilineFormatter() TraceFormatter {
// It uses attributes from the [attr] and [semconv] packages.
type PrettyMultilineFormatter struct{}
func AttrValueToString(val attribute.Value) string {
switch val.Type() {
case attribute.STRING:
return val.AsString()
case attribute.BOOL:
if val.AsBool() {
return "true"
} else {
return "false"
}
case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE:
json, _ := val.MarshalJSON()
return string(json)
case attribute.FLOAT64:
fmt.Sprintf("%f", val.AsFloat64())
case attribute.INT64:
return fmt.Sprintf("%d", val.AsInt64())
default:
json, _ := val.MarshalJSON()
return string(json)
}
return ""
}
func (f *PrettyMultilineFormatter) FormatSpanStart(span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error) {
attrs := ""
slices.SortFunc(selectedAttrs, func(a, b attribute.KeyValue) int {
@ -30,7 +54,7 @@ func (f *PrettyMultilineFormatter) FormatSpanStart(span trace.ReadOnlySpan, sele
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), kv.Value.AsString())
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}
@ -54,7 +78,7 @@ func (f *PrettyMultilineFormatter) FormatSpanEnd(span trace.ReadOnlySpan, select
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), kv.Value.AsString())
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}
@ -79,7 +103,7 @@ func (f *PrettyMultilineFormatter) FormatEvent(event trace.Event, span trace.Rea
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), kv.Value.AsString())
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}

View File

@ -0,0 +1,102 @@
package console_exporter
import (
"fmt"
"slices"
"time"
"git.ma-al.com/maal-libraries/observer/pkg/console_fmt"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/trace"
)
func NewSimpleSinglelineFormatter() TraceFormatter {
return &SimpleSinglelineFormatter{}
}
// A simple formatter that will print only events using a single line per entry
type SimpleSinglelineFormatter struct{}
func (f *SimpleSinglelineFormatter) FormatSpanStart(span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error) {
attrs := ""
slices.SortFunc(selectedAttrs, func(a, b attribute.KeyValue) int {
if a.Key > b.Key {
return 1
} else {
return -1
}
})
if len(selectedAttrs) > 0 {
attrs += " |"
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf(" %s=%s", string(kv.Key), AttrValueToString(kv.Value))
}
}
}
formattedSpanString := fmt.Sprintf(
"%s%s\n",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("%s %s [SpanStart] - ", time.Now().Format("2006-01-02 15:04:05"), lvl.String())+span.Name()),
attrs,
)
return formattedSpanString, nil
}
func (f *SimpleSinglelineFormatter) FormatSpanEnd(span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error) {
attrs := ""
slices.SortFunc(selectedAttrs, func(a, b attribute.KeyValue) int {
if a.Key > b.Key {
return 1
} else {
return -1
}
})
if len(selectedAttrs) > 0 {
attrs += " |"
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf(" %s=%s", string(kv.Key), AttrValueToString(kv.Value))
}
}
}
formattedSpanString := fmt.Sprintf(
"%s%s\n",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("%s %s [SpanEnd] - ", time.Now().Format("2006-01-02 15:04:05"), lvl.String())+span.Name()),
attrs,
)
return formattedSpanString, nil
}
func (f *SimpleSinglelineFormatter) FormatEvent(event trace.Event, span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error) {
attrs := ""
slices.SortFunc(selectedAttrs, func(a, b attribute.KeyValue) int {
if a.Key > b.Key {
return 1
} else {
return -1
}
})
if len(selectedAttrs) > 0 {
attrs += " |"
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf(" %s=%s", string(kv.Key), AttrValueToString(kv.Value))
}
}
}
formattedSpanString := fmt.Sprintf(
"%s%s\n",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("%s %s [Event] - ", time.Now().Format("2006-01-02 15:04:05"), lvl.String())+event.Name),
attrs,
)
return formattedSpanString, nil
}

View File

@ -3,9 +3,10 @@ package gelfexporter
type config struct {
GelfUrl string
AppName string
Tag string
}
// newConfig creates a validated Config configured with options.
// newConfig creates a validated Config configured with options
func newConfig(options ...Option) (config, error) {
cfg := config{}
for _, opt := range options {
@ -40,3 +41,14 @@ func (o appName) apply(cfg config) config {
cfg.AppName = string(o)
return cfg
}
func WithTag(tagStr string) Option {
return tag(tagStr)
}
type tag string
func (o tag) apply(cfg config) config {
cfg.Tag = string(o)
return cfg
}

View File

@ -31,10 +31,17 @@ func Log(writer *gelf.UDPWriter, msg GELFMessage) {
if err != nil {
log.Println(err)
}
} else {
log.Fatalln("gelf.UDPWriter is not set!")
}
}
func (g GELFMessage) GELFFormat() *gelf.Message {
prefixedExtras := make(map[string]interface{}, len(g.ExtraFields))
for k, v := range g.ExtraFields {
prefixedExtras["_"+k] = v
}
return &gelf.Message{
Version: "1.1",
Host: g.Host,
@ -42,6 +49,6 @@ func (g GELFMessage) GELFFormat() *gelf.Message {
Full: g.LongMessage,
TimeUnix: float64(g.Timestamp.Unix()),
Level: int32(g.Level),
Extra: g.ExtraFields,
Extra: prefixedExtras,
}
}

View File

@ -29,6 +29,8 @@ func New(options ...Option) (*Exporter, error) {
return &Exporter{
gelfWriter: writer,
appName: cfg.AppName,
tag: cfg.Tag,
}, nil
}
@ -36,6 +38,7 @@ func New(options ...Option) (*Exporter, error) {
type Exporter struct {
gelfWriter *gelf.UDPWriter
appName string
tag string
stoppedMu sync.RWMutex
stopped bool
}
@ -61,10 +64,15 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
stub := &stubs[i]
var attributes = make(map[string]interface{})
for _, attr := range stub.Attributes {
attributes[string(attr.Key)] = GetByType(attr.Value)
}
for _, attr := range stub.Resource.Attributes() {
attributes[string(attr.Key)] = GetByType(attr.Value)
}
for i := range stub.Events {
event := stub.Events[i]
@ -74,8 +82,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
Timestamp: stub.StartTime,
// Defaults to ALERT since we consider lack of the level a serious error that should be fixed ASAP.
// Otherwise some dangerous unexpected behaviour could go unnoticed.
Level: syslog.ALERT,
ExtraFields: attributes,
Level: syslog.ALERT,
}
for _, attrKV := range event.Attributes {
if attrKV.Key == attr.LogMessageLongKey {
@ -95,6 +102,8 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
attributes[string(attrKV.Key)] = GetByType(attrKV.Value)
}
gelf.ExtraFields = attributes
gelf.ExtraFields["tag"] = e.tag
Log(e.gelfWriter, gelf)
}

View File

@ -23,17 +23,22 @@ type Config struct {
AppName string
Version string
// Name of an organization providing the service
ServiceProvider string
Exporters []exporters.TraceExporter
ServiceProvider string
Exporters []exporters.TraceExporter
ResourceAttributes []attribute.KeyValue
}
func newResource(config Config) *resource.Resource {
r := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(config.AppName),
semconv.ServiceVersionKey.String(config.Version),
allAttributes := make([]attribute.KeyValue, 0, 3)
allAttributes = append(
allAttributes,
semconv.ServiceName(config.AppName),
semconv.ServiceVersion(config.Version),
attribute.String("service.provider", config.ServiceProvider),
)
allAttributes = append(allAttributes, config.ResourceAttributes...)
r := resource.NewWithAttributes(semconv.SchemaURL, allAttributes...)
return r
}

View File

@ -8,6 +8,7 @@ import (
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"github.com/gofiber/fiber/v2"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"go.opentelemetry.io/otel/trace"
)
@ -45,6 +46,38 @@ func configDefaults(config ...middlewareConfig) middlewareConfig {
return cfg
}
// A helper implementing `propagation.TextMapCarrier`
type headersCarrier struct {
*fiber.Ctx
}
func (h headersCarrier) Get(key string) string {
c := h.Ctx
headers := c.GetReqHeaders()
if val, ok := headers[key]; ok {
if len(val) > 0 {
return val[len(val)-1]
}
}
return ""
}
func (h headersCarrier) Set(key string, value string) {
c := h.Ctx
c.Set(key, value)
return
}
func (h headersCarrier) Keys() []string {
c := h.Ctx
headers := c.GetRespHeaders()
keys := make([]string, 0)
for k, _ := range headers {
keys = append(keys, k)
}
return keys
}
func new(config ...middlewareConfig) fiber.Handler {
// Set default config
cfg := configDefaults(config...)
@ -54,7 +87,6 @@ func new(config ...middlewareConfig) fiber.Handler {
spanStartAttributes := []attr.KeyValue{
semconv.HTTPMethod(c.Method()),
semconv.HTTPTarget(string(c.Request().RequestURI())),
semconv.HTTPRoute(c.Route().Path),
semconv.HTTPURL(c.OriginalURL()),
semconv.HTTPUserAgent(string(c.Request().Header.UserAgent())),
semconv.HTTPRequestContentLength(c.Request().Header.ContentLength()),
@ -69,22 +101,32 @@ func new(config ...middlewareConfig) fiber.Handler {
}
opts = append(opts, cfg.TracerStartAttributes...)
// Init context using values from request headers if possible:
headersCarrier := headersCarrier{c}
ctx := c.UserContext()
tctx := propagation.TraceContext{}
ctx = tctx.Extract(ctx, headersCarrier)
otelCtx, span := Tracer.Start(
c.UserContext(),
ctx,
c.Method()+" "+c.OriginalURL(),
opts...,
)
c.SetUserContext(otelCtx)
defer span.End()
err := c.Next()
statusCode := c.Response().StatusCode()
attrs := semconv.HTTPResponseStatusCode(statusCode)
span.SetAttributes(attrs)
// Return with trace context added to headers
ctx = c.UserContext()
tctx.Inject(ctx, headersCarrier)
return err
}
}

View File

@ -0,0 +1,281 @@
package gorm_tracing
import (
"errors"
"fmt"
"strings"
"time"
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/event"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"go.opentelemetry.io/otel/trace"
"gorm.io/gorm"
)
type Option func(p *gormPlugin)
func defaultSqlQueryFormatter(query string) string {
return strings.Join(strings.Fields(strings.TrimSpace(query)), " ")
}
// WithEveryStatementAsEvent configures the plugin to log all statements as distinct events, not just errors and warnings.
func WithEveryStatementAsEvent() Option {
return func(p *gormPlugin) {
p.everyStatementAsEvent = true
}
}
// WithSlowQueryDuration configures the duration at which the query will be considered slow an logged with warning.
func WithSlowQueryDuration(duration time.Duration) Option {
return func(p *gormPlugin) {
p.slowQueryDuration = duration
}
}
// WithTracerProvider configures a tracer provider that is used to create a tracer.
func WithTracerProvider(provider trace.TracerProvider) Option {
return func(p *gormPlugin) {
p.provider = provider
}
}
// WithAttributes configures attributes that are used to create a span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return func(p *gormPlugin) {
p.attrs = append(p.attrs, attrs...)
}
}
// WithDBName configures a db.name attribute.
func WithDBName(name string) Option {
return func(p *gormPlugin) {
p.attrs = append(p.attrs, semconv.DBNameKey.String(name))
}
}
// WithoutQueryVariables configures the db.statement attribute to exclude query variables
func WithoutQueryVariables() Option {
return func(p *gormPlugin) {
p.excludeQueryVars = true
}
}
// WithQueryFormatter configures a query formatter
func WithQueryFormatter(queryFormatter func(query string) string) Option {
return func(p *gormPlugin) {
p.queryFormatter = queryFormatter
}
}
// WithDefaultQueryFormatter adds a simple formatter that trims any excess whitespaces from the query
func WithDefaultQueryFormatter() Option {
return func(p *gormPlugin) {
p.queryFormatter = defaultSqlQueryFormatter
}
}
type gormPlugin struct {
provider trace.TracerProvider
tracer trace.Tracer
attrs []attribute.KeyValue
excludeQueryVars bool
queryFormatter func(query string) string
slowQueryDuration time.Duration
everyStatementAsEvent bool
}
// Overrides and sets some options with recommended defaults
func DefaultGormPlugin(opts ...Option) gorm.Plugin {
p := &gormPlugin{}
for _, opt := range opts {
opt(p)
}
WithDefaultQueryFormatter()(p)
WithoutQueryVariables()(p)
WithEveryStatementAsEvent()(p)
p.provider = otel.GetTracerProvider()
p.tracer = p.provider.Tracer("git.ma-al.com/maal-libraries/observer/pkg/gorm_tracing")
return p
}
func NewGormPlugin(opts ...Option) gorm.Plugin {
p := &gormPlugin{}
for _, opt := range opts {
opt(p)
}
if p.provider == nil {
p.provider = otel.GetTracerProvider()
}
p.tracer = p.provider.Tracer("git.ma-al.com/maal-libraries/observer/pkg/gorm_tracing")
return p
}
func (p gormPlugin) Name() string {
return "observerGorm"
}
type gormHookFunc func(tx *gorm.DB)
type gormRegister interface {
Register(name string, fn func(*gorm.DB)) error
}
func (p gormPlugin) Initialize(db *gorm.DB) (err error) {
cb := db.Callback()
hooks := []struct {
callback gormRegister
hook gormHookFunc
name string
}{
{cb.Create().Before("gorm:create"), p.before("gorm.Create"), "before:create"},
{cb.Create().After("gorm:create"), p.after(), "after:create"},
{cb.Query().Before("gorm:query"), p.before("gorm.Query"), "before:select"},
{cb.Query().After("gorm:query"), p.after(), "after:select"},
{cb.Delete().Before("gorm:delete"), p.before("gorm.Delete"), "before:delete"},
{cb.Delete().After("gorm:delete"), p.after(), "after:delete"},
{cb.Update().Before("gorm:update"), p.before("gorm.Update"), "before:update"},
{cb.Update().After("gorm:update"), p.after(), "after:update"},
{cb.Row().Before("gorm:row"), p.before("gorm.Row"), "before:row"},
{cb.Row().After("gorm:row"), p.after(), "after:row"},
{cb.Raw().Before("gorm:raw"), p.before("gorm.Raw"), "before:raw"},
{cb.Raw().After("gorm:raw"), p.after(), "after:raw"},
}
var firstErr error
for _, h := range hooks {
if err := h.callback.Register("observer:"+h.name, h.hook); err != nil && firstErr == nil {
firstErr = fmt.Errorf("callback register %s failed: %w", h.name, err)
}
}
return firstErr
}
func (p *gormPlugin) before(spanName string) gormHookFunc {
return func(tx *gorm.DB) {
ctx := tx.Statement.Context
tx.Statement.Set("observer:statement_start", time.Now())
tx.Statement.Context, _ = p.tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient))
}
}
func (p *gormPlugin) after() gormHookFunc {
return func(tx *gorm.DB) {
span := trace.SpanFromContext(tx.Statement.Context)
if !span.IsRecording() {
return
}
defer span.End()
attrs := make([]attribute.KeyValue, 0, len(p.attrs)+4)
attrs = append(attrs, p.attrs...)
if sys := dbSystem(tx); sys.Valid() {
attrs = append(attrs, sys)
}
query := ""
if p.excludeQueryVars {
// cat query at VALUES and add tree dots
pos := strings.Index(tx.Statement.SQL.String(), "VALUES (")
if pos != -1 {
query = tx.Statement.SQL.String()[:pos+len("VALUES ")] + " ..."
} else {
query = tx.Statement.SQL.String()
}
}
attrs = append(attrs, semconv.DBStatementKey.String(p.formatQuery(query)))
if tx.Statement.Table != "" {
attrs = append(attrs, semconv.DBSQLTableKey.String(tx.Statement.Table))
}
if tx.Statement.RowsAffected != -1 {
attrs = append(attrs, attr.DBRowsAffected(tx.Statement.RowsAffected))
}
slowQuery := false
if statementStart, ok := tx.Statement.Get("observer:statement_start"); ok {
start := statementStart.(time.Time)
duration := time.Now().Sub(start)
attrs = append(attrs, attr.DBExecutionTimeMs(duration))
if p.slowQueryDuration != time.Duration(0) {
if duration >= p.slowQueryDuration {
slowQuery = true
event.NewErrInSpan(event.Error{
Level: level.WARN,
Err: errors.New("slow query execution"),
Attributes: attrs,
}.SkipMoreInCallStack(3), span)
attrs = append(attrs, attr.SeverityLevel(level.WARN))
}
}
}
errQuery := false
if tx.Statement.Error != nil {
errQuery = true
event.NewErrInSpan(event.Error{
Level: level.ERR,
Err: tx.Statement.Error,
Attributes: attrs,
}.SkipMoreInCallStack(3), span)
attrs = append(attrs, attr.SeverityLevel(level.ERR))
}
if !slowQuery && !errQuery && p.everyStatementAsEvent {
event.NewInSpan(event.Event{
Level: level.DEBUG,
ShortMessage: "executed an sql query with gorm",
Attributes: attrs,
}.SkipMoreInCallStack(3), span)
attrs = append(attrs, attr.SeverityLevel(level.DEBUG))
}
span.SetAttributes(attrs...)
}
}
func (p *gormPlugin) formatQuery(query string) string {
if p.queryFormatter != nil {
return p.queryFormatter(query)
}
return query
}
func dbSystem(tx *gorm.DB) attribute.KeyValue {
switch tx.Dialector.Name() {
case "mysql":
return semconv.DBSystemMySQL
case "mssql":
return semconv.DBSystemMSSQL
case "postgres", "postgresql":
return semconv.DBSystemPostgreSQL
case "sqlite":
return semconv.DBSystemSqlite
case "sqlserver":
return semconv.DBSystemKey.String("sqlserver")
case "clickhouse":
return semconv.DBSystemKey.String("clickhouse")
default:
return attribute.KeyValue{}
}
}

View File

@ -1,6 +1,8 @@
package level
import (
"strings"
"git.ma-al.com/maal-libraries/observer/pkg/syslog"
"go.opentelemetry.io/otel/attribute"
)
@ -61,20 +63,21 @@ func (l SeverityLevel) String() string {
}
func FromString(level string) SeverityLevel {
level = strings.ToLower(level)
switch level {
case "ALERT":
case "alert":
return ALERT
case "CRIT":
case "crit", "critical":
return CRIT
case "ERR":
case "err", "error":
return ERR
case "WARN":
case "warn", "warning":
return WARN
case "INFO":
case "info":
return INFO
case "DEBUG":
case "debug":
return DEBUG
case "TRACE":
case "trace":
return TRACE
default:
return unset