38 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
a2b61ea706 do not Printf GELF messages when logging 2024-06-04 11:59:28 +02:00
3024e888c7 adjust tracing middleware for practical use 2024-05-27 07:37:54 +02:00
d7b45a1439 copy code and remove dependency on fiber-otel 2024-05-21 07:54:22 +02:00
7dd3dc70d5 make span names more generic by using Route().Path instead of Path 2024-05-20 20:06:50 +02:00
031177f30c fix spans in UserContext() not connected properly to span from fiber.Ctx 2024-05-20 19:35:21 +02:00
86e9128c68 add db.execution_time_ms 2024-05-20 17:29:00 +02:00
16dbdeec3e alias KeyValue to make users slightly more independent from otel 2024-05-20 17:23:06 +02:00
265731010e set error status based on severity level of an error 2024-05-20 13:55:53 +02:00
f5819972a4 allow skipping callstack in errors and events 2024-05-20 13:39:55 +02:00
c70a285e70 fix panic on missing formatter in console_exporter.ProcessorOptions 2024-05-20 12:18:03 +02:00
d119563c7d connect contexts managed by fiber middleware to join at c.UserContext() 2024-05-20 11:45:27 +02:00
372f4367ed allow retrieving spans from fiber.Ctx 2024-05-20 11:04:36 +02:00
77ab12c3ac move repository and module imports to new location 2024-05-20 08:20:13 +02:00
076196c03e added event wrappers, bug fixes, API improvements 2024-05-17 18:21:09 +02:00
4f4a7e09c5 cleanup unused and add short message to GELF 2024-05-17 15:46:25 +02:00
d4dc790298 rename ExporterWithConfig to be less confusing and closer to otel 2024-05-17 15:36:35 +02:00
3c51f5575b plenty of changes to make the package more ergonomic
Including: bug fixes, api changes, new packages, and more!
2024-05-17 15:31:35 +02:00
e835318689 fix incorrect API usage in the example
`attr.SourceCodeLocation` already adds to callstack search depth to match
its call site.
2024-05-17 10:39:32 +02:00
e9c3ae1a7b reorganize exporters and simplify their use 2024-05-17 10:37:05 +02:00
fc92995cc8 start reworking exporters to be more composable 2024-05-16 18:19:36 +02:00
ab5b70704d standardize commonly used attributes
Some commonly used at maal attributes have been encoded as consts with
convinience wrappers similar to those of semconv package from otel sdk.
Additionally some utils that can generate mutliple attributes were added.
2024-05-16 13:45:13 +02:00
23 changed files with 2145 additions and 366 deletions

View File

@ -8,52 +8,61 @@ import (
"os/signal"
"time"
"git.ma-al.com/gora_filip/observer/pkg/level"
"git.ma-al.com/gora_filip/observer/pkg/tracer"
"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"
"git.ma-al.com/maal-libraries/observer/pkg/exporters/console_exporter"
tracing "git.ma-al.com/maal-libraries/observer/pkg/fiber_tracing"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"github.com/gofiber/fiber/v2"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/trace"
)
type AttributesX struct {
}
func main() {
main := fiber.New()
main := fiber.New(fiber.Config{
StreamRequestBody: true,
})
exps := make([]exporters.TraceExporter, 0)
main.Use(tracer.NewTracer(tracer.Config{
AppName: "test",
JaegerUrl: "http://localhost:4318/v1/traces",
GelfUrl: "192.168.220.30:12201",
Version: "1",
envFilter := "OBSERVER_CONSOLE"
singlelineFmt := console_exporter.NewSimpleSinglelineFormatter()
exps = append(exps, exporters.DevConsoleExporter(console_exporter.ProcessorOptions{
FilterFromEnvVar: &envFilter,
TraceFormatter: &singlelineFmt,
}))
defer tracer.ShutdownTracer()
main.Get("/", func(c *fiber.Ctx) error {
ctx, span := tracer.Handler(c)
defer span.End()
gelfExp, err := exporters.GelfExporter()
if err == nil {
exps = append(exps, gelfExp)
}
jaegerExp, err := exporters.OtlpHTTPExporter(otlptracehttp.WithEndpointURL("http://localhost:4318/v1/traces"))
if err == nil {
exps = append(exps, jaegerExp)
}
span.AddEvent(
"smthing is happening",
trace.WithAttributes(
tracer.LongMessage("smthing is happening - long"),
tracer.JsonAttr("smth", map[string]interface{}{
"xd": 1,
}),
tracer.Level(level.ALERT),
),
)
main.Use(tracing.NewMiddleware(tracing.Config{
AppName: "example",
Version: "0.0.0",
ServiceProvider: "maal",
Exporters: exps,
}))
defer tracing.ShutdownTracer()
err := Serv(ctx)
if err != nil {
return tracer.RecordError(span, err)
}
main.Use(func(c *fiber.Ctx) error {
span := tracing.SpanFromContext(c)
span.AddEvent("pushed into a span an event from middleware")
return c.SendString("xd")
span = trace.SpanFromContext(c.UserContext())
span.AddEvent("span also available from c.UserContext()")
return c.Next()
})
main.Get("/", Handler)
main.Get("/just/some/more/complex/path/:with/params", Handler)
// handle interrupts (shutdown)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
@ -69,8 +78,29 @@ func main() {
}
func Handler(c *fiber.Ctx) error {
ctx, span := tracing.FStart(c, layer_attr.Handler{
Level: level.DEBUG,
}.AsOpts())
defer span.End()
event.NewInSpan(event.Event{
Level: level.WARN,
ShortMessage: "a warning event",
}, span)
err := Serv(ctx)
if err != nil {
return event.NewErrInSpan(event.Error{Err: err, Level: level.ERR}, span)
}
return c.SendStatus(fiber.StatusOK)
}
func Serv(ctx context.Context) *fiber.Error {
ctx, span := tracer.Service(ctx, "name of the span")
ctx, span := tracing.Start(ctx, "service span", layer_attr.Service{
Level: level.INFO,
Name: "some service",
}.AsOpts())
defer span.End()
for range []int{1, 2, 3} {
@ -82,11 +112,19 @@ 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")
}
func Repo(ctx context.Context) error {
ctx, span := tracer.Repository(ctx, "name of the span")
ctx, span := tracing.Start(ctx, "repo span", layer_attr.Repo{
Level: level.DEBUG,
Name: "some repo",
}.AsOpts())
defer span.End()
for range []int{1, 2, 3} {

View File

@ -1,2 +1,5 @@
GET http://127.0.0.1:3344/
HTTP 200
HTTP 500
GET http://127.0.0.1:3344/just/some/more/complex/path/jjj/params
HTTP 500

13
go.mod
View File

@ -1,18 +1,16 @@
module git.ma-al.com/gora_filip
module git.ma-al.com/maal-libraries/observer
go 1.21.0
go 1.21
require (
git.ma-al.com/gora_filip/observer v0.0.0-20240430124205-be03e0ce4205
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 v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace 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 (
@ -22,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
@ -30,6 +30,7 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
golang.org/x/net v0.23.0 // indirect

41
go.sum
View File

@ -1,30 +1,28 @@
git.ma-al.com/gora_filip/observer v0.0.0-20240430124205-be03e0ce4205 h1:tuJ7e4EAWx/IbVESEO6l3yNoGiUAoaXWssMc26Ft3Hs=
git.ma-al.com/gora_filip/observer v0.0.0-20240430124205-be03e0ce4205/go.mod h1:NLwhsfm3SE3YwR+3z4DbH82OBjRPcE+M/GPGEc7DPUM=
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=
@ -36,62 +34,39 @@ 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=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0 h1:0W5o9SzoR15ocYHEQfvfipzcNog1lBxOLfnex91Hk6s=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0/go.mod h1:zVZ8nz+VSggWmnh6tTsJqXQ7rU4xLwRtna1M4x5jq58=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
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=
@ -104,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=

466
pkg/attr/attr.go Normal file
View File

@ -0,0 +1,466 @@
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"
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
type IntoTraceAttribute interface {
IntoTraceAttribute() attribute.KeyValue
}
type IntoTraceAttributes interface {
IntoTraceAttributes() []attribute.KeyValue
}
func CollectAttributes(attrs ...interface{}) []attribute.KeyValue {
collected := make([]attribute.KeyValue, len(attrs))
for _, a := range attrs {
switch a.(type) {
case []attribute.KeyValue:
collected = append(collected, a.([]attribute.KeyValue)...)
case attribute.KeyValue:
collected = append(collected, a.(attribute.KeyValue))
case IntoTraceAttribute:
collected = append(collected, a.(IntoTraceAttribute).IntoTraceAttribute())
case IntoTraceAttributes:
collected = append(collected, a.(IntoTraceAttributes).IntoTraceAttributes()...)
}
}
return collected
}
func WithAttributes(attrs ...interface{}) trace.SpanStartEventOption {
return trace.WithAttributes(CollectAttributes(attrs...)...)
}
const (
SeverityLevelKey = attribute.Key("level")
LogMessageLongKey = attribute.Key("log_message.long")
LogMessageShortKey = attribute.Key("log_message.short")
EnduserResponseMessageKey = attribute.Key("enduser.response_message")
SessionLanguageIdKey = attribute.Key("session.language_id")
SessionCountryIdKey = attribute.Key("session.country_id")
SessionCurrencyIdKey = attribute.Key("session.currency_id")
ProcessThreadsAvailableKey = attribute.Key("process.threads_available")
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
const (
LayerFrameworkMiddleware ServiceArchitectureLayer = "framework_middleware"
LayerHandler = "handler"
LayerService = "service"
LayerRepository = "repository"
LayerORM = "orm"
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
// can map to syslog level.
func SeverityLevel(lvl level.SeverityLevel) attribute.KeyValue {
return attribute.String(string(SeverityLevelKey), lvl.String())
}
func LogMessage(short string, expanded string) []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 2)
attrs = append(attrs, LogMessageShort(short), LogMessageLong(expanded))
return attrs
}
// An attribute which value could be used as the full message field within the GELF format.
func LogMessageLong(msg string) attribute.KeyValue {
return attribute.String(string(LogMessageLongKey), msg)
}
// An attribute which value could be used as the short message field within the GELF format.
func LogMessageShort(msg string) attribute.KeyValue {
return attribute.String(string(LogMessageShortKey), msg)
}
// A message provided to the end user. For example, in case of API server errors it might be desired
// to provide to the user a message that does not leak too many details instead of sending an original
// (for a given package) error message.
func EnduserResponseMessage(msg string) attribute.KeyValue {
return attribute.String(string(EnduserResponseMessageKey), msg)
}
// Inspect the call stack to retrieve the information about a call site location including
// function name, file path, and line number.
func SourceCodeLocation(skipLevelsInCallStack int) []attribute.KeyValue {
pc, file, line, _ := runtime.Caller(1 + skipLevelsInCallStack)
funcName := runtime.FuncForPC(pc).Name()
return []attribute.KeyValue{
{
Key: semconv.CodeFunctionKey,
Value: attribute.StringValue(funcName),
},
{
Key: semconv.CodeFilepathKey,
Value: attribute.StringValue(file),
},
{
Key: semconv.CodeLineNumberKey,
Value: attribute.IntValue(line),
},
}
}
// Use within some panic handler to generate an attribute that will contain a stack trace.
func PanicStackTrace() attribute.KeyValue {
stackTrace := string(debug.Stack())
return semconv.ExceptionStacktrace(stackTrace)
}
// Builds attributes describing a server.
func Server(address string, port int) []attribute.KeyValue {
return []attribute.KeyValue{
{
Key: semconv.ServerAddressKey,
Value: attribute.StringValue(address),
},
{
Key: semconv.ServerPortKey,
Value: attribute.IntValue(port),
},
}
}
// Investigates the running process to derive attributes that describe it. This will only
// try to retrive these details which provide any valuable information at the start of a
// process.
func ProcessStart() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 5)
executablePath, err := os.Executable()
if err == nil {
attrs = append(attrs, semconv.ProcessExecutablePath(executablePath))
}
hostname, err := os.Hostname()
if err == nil {
attrs = append(attrs, semconv.HostName(hostname))
}
runtimeVersion := runtime.Version()
cpuThreads := runtime.NumCPU()
pid := os.Getpid()
attrs = append(attrs, semconv.ProcessParentPID(pid), semconv.ProcessRuntimeVersion(runtimeVersion), attribute.KeyValue{
Key: ProcessThreadsAvailableKey,
Value: attribute.IntValue(cpuThreads),
})
return attrs
}
// Id of an end user's session.
func SessionId(id string) attribute.KeyValue {
return attribute.KeyValue{
Key: semconv.SessionIDKey,
Value: attribute.StringValue(id),
}
}
// Id of a language associated with a user's session.
func SessionLanguageId(id uint) attribute.KeyValue {
return attribute.KeyValue{
Key: SessionLanguageIdKey,
Value: attribute.IntValue(int(id)),
}
}
// Id of a country associated with a user's session.
func SessionCountryId(id uint) attribute.KeyValue {
return attribute.KeyValue{
Key: SessionCountryIdKey,
Value: attribute.IntValue(int(id)),
}
}
// Id of a currency associated with a user's session.
func SessionCurrencyId(id uint) attribute.KeyValue {
return attribute.KeyValue{
Key: SessionCurrencyIdKey,
Value: attribute.IntValue(int(id)),
}
}
// Render details about session as attributes.
func Session(deets SessionDetails) []attribute.KeyValue {
return deets.IntoTraceAttributes()
}
// A collection of attributes that we at maal frequently attach to user sessions that can
// be converted into a collection of trace attributes. All fields are optional.
type SessionDetails struct {
ID *string
PreviousID *string
LanguageID *uint
CountryID *uint
CurrencyID *uint
}
func (deets SessionDetails) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 4) // most frequently we won't have previous session ID
if deets.ID != nil {
attrs = append(attrs, SessionId(*deets.ID))
}
if deets.PreviousID != nil {
attrs = append(attrs, attribute.KeyValue{
Key: semconv.SessionPreviousIDKey,
Value: attribute.StringValue(*deets.PreviousID),
})
}
if deets.LanguageID != nil {
attrs = append(attrs, SessionLanguageId(*deets.LanguageID))
}
if deets.CountryID != nil {
attrs = append(attrs, SessionCountryId(*deets.CountryID))
}
if deets.CurrencyID != nil {
attrs = append(attrs, SessionCurrencyId(*deets.CurrencyID))
}
return attrs
}
// Describes a layer of a web server architecture with some of terms frequently used at maal.
func ServiceLayer(layer ServiceArchitectureLayer) attribute.KeyValue {
return attribute.KeyValue{
Key: ServiceLayerKey,
Value: attribute.StringValue(string(layer)),
}
}
func ServiceLayerName(name string) attribute.KeyValue {
return attribute.KeyValue{
Key: ServiceLayerNameKey,
Value: attribute.StringValue(name),
}
}
// Take duration as an execution time of a query measured in milisecongs.
func DBExecutionTimeMs(duration time.Duration) attribute.KeyValue {
return attribute.KeyValue{
Key: DBExecutionTimeMsKey,
Value: attribute.Int64Value(duration.Milliseconds()),
}
}
func DBRowsAffected(rows int64) attribute.KeyValue {
return attribute.KeyValue{
Key: DBRowsAffectedKey,
Value: attribute.Int64Value(rows),
}
}

View File

@ -0,0 +1,124 @@
package layer_attr
import (
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type Handler struct {
// Extra attributes to be attached. Can be also added with [Handler.CustomAttrs] method.
Attributes []attribute.KeyValue
Level level.SeverityLevel
Name string
extraSkipInStack int
}
func (h Handler) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 5+len(h.Attributes))
attrs = append(attrs, attr.SourceCodeLocation(1+h.extraSkipInStack)...) // 3 attrs added here
attrs = append(attrs, attr.ServiceLayer(attr.LayerHandler), attr.SeverityLevel(h.Level))
if len(h.Name) > 0 {
attrs = append(attrs, attr.ServiceLayerName(h.Name))
}
attrs = append(attrs, h.Attributes...)
return attrs
}
func (h Handler) CustomAttrs(attrs ...interface{}) Handler {
h.Attributes = append(h.Attributes, attr.CollectAttributes(attrs...)...)
return h
}
func (h *Handler) SkipMoreInCallStack(skip int) {
h.extraSkipInStack += skip
}
// Works the same as [Handler.IntoTraceAttributes]
func (h Handler) AsAttrs() []attribute.KeyValue {
h.extraSkipInStack += 1
return h.IntoTraceAttributes()
}
func (h Handler) AsOpts() trace.SpanStartEventOption {
h.extraSkipInStack += 1
return trace.WithAttributes(h.IntoTraceAttributes()...)
}
type Service struct {
// Extra attributes to be attached. Can be also added with [Service.CustomAttrs] method.
Attributes []attribute.KeyValue
Level level.SeverityLevel
Name string
extraSkipInStack int
}
func (s Service) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 6+len(s.Attributes))
attrs = append(attrs, attr.SourceCodeLocation(1+s.extraSkipInStack)...)
attrs = append(attrs, attr.ServiceLayer(attr.LayerService), attr.SeverityLevel(s.Level))
if len(s.Name) > 0 {
attrs = append(attrs, attr.ServiceLayerName(s.Name))
}
attrs = append(attrs, s.Attributes...)
return attrs
}
// Works the same as [Service.IntoTraceAttributes]
func (s Service) AsAttrs() []attribute.KeyValue {
s.extraSkipInStack += 1
return s.IntoTraceAttributes()
}
func (s Service) CustomAttrs(attrs ...interface{}) Service {
s.Attributes = append(s.Attributes, attr.CollectAttributes(attrs...)...)
return s
}
func (s *Service) SkipMoreInCallStack(skip int) {
s.extraSkipInStack += skip
}
func (s Service) AsOpts() trace.SpanStartEventOption {
s.extraSkipInStack += 1
return trace.WithAttributes(s.IntoTraceAttributes()...)
}
type Repo struct {
// Extra attributes to be attached. Can be also added with [Repo.CustomAttrs] method
Attributes []attribute.KeyValue
Level level.SeverityLevel
Name string
extraSkipInStack int
}
func (r Repo) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 6+len(r.Attributes))
attrs = append(attrs, attr.SourceCodeLocation(1+r.extraSkipInStack)...)
attrs = append(attrs, attr.ServiceLayer(attr.LayerRepository), attr.SeverityLevel(r.Level))
if len(r.Name) > 0 {
attrs = append(attrs, attr.ServiceLayerName(r.Name))
}
attrs = append(attrs, r.Attributes...)
return attrs
}
// Works the same as [Repo.IntoTraceAttributes]
func (r Repo) AsAttrs() []attribute.KeyValue {
r.extraSkipInStack += 1
return r.IntoTraceAttributes()
}
func (r Repo) CustomAttrs(attrs ...interface{}) Repo {
r.Attributes = append(r.Attributes, attr.CollectAttributes(attrs...)...)
return r
}
func (r *Repo) SkipMoreInCallStack(skip int) {
r.extraSkipInStack += skip
}
func (r Repo) AsOpts() trace.SpanStartEventOption {
r.extraSkipInStack += 1
return trace.WithAttributes(r.IntoTraceAttributes()...)
}

View File

@ -0,0 +1,28 @@
package code_location
import (
"runtime"
)
type CodeLocation struct {
FilePath string
FuncName string
LineNumber int
ColumnNumber int
}
func FromStackTrace(atDepth ...int) CodeLocation {
skipLevelsInCallStack := 0
if len(atDepth) > 1 {
skipLevelsInCallStack = atDepth[0]
}
pc, file, line, _ := runtime.Caller(1 + skipLevelsInCallStack)
funcName := runtime.FuncForPC(pc).Name()
return CodeLocation{
FilePath: file,
LineNumber: line,
FuncName: funcName,
}
}

85
pkg/console_fmt/fmt.go Normal file
View File

@ -0,0 +1,85 @@
package console_fmt
import (
"git.ma-al.com/maal-libraries/observer/pkg/level"
)
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorPurple = "\033[35m"
ColorCyan = "\033[36m"
ColorWhite = "\033[37m"
ColorBlackOnYellow = "\033[43m\033[30m"
ColorWhiteOnRed = "\033[37m\033[41m"
ColorWhiteOnRedBlinking = "\033[37m\033[41m\033[5m"
ColorBold = "\033[1m"
)
func Bold(txt string) string {
return ColorBold + txt + ColorReset
}
func Red(txt string) string {
return ColorRed + txt + ColorReset
}
func Green(txt string) string {
return ColorGreen + txt + ColorReset
}
func Yellow(txt string) string {
return ColorYellow + txt + ColorReset
}
func Blue(txt string) string {
return ColorBlue + txt + ColorReset
}
func Purple(txt string) string {
return ColorPurple + txt + ColorReset
}
func Cyan(txt string) string {
return ColorCyan + txt + ColorReset
}
func White(txt string) string {
return ColorWhite + txt + ColorReset
}
func BlackOnYellow(txt string) string {
return ColorBlackOnYellow + txt + ColorReset
}
func WhiteOnRed(txt string) string {
return ColorWhiteOnRed + txt + ColorReset
}
func WhiteOnRedBlinking(txt string) string {
return ColorWhiteOnRedBlinking + txt + ColorReset
}
func SeverityLevelToColor(lvl level.SeverityLevel) string {
switch lvl {
case level.TRACE:
return ColorWhite
case level.DEBUG:
return ColorPurple
case level.INFO:
return ColorBlue
case level.WARN:
return ColorYellow
case level.ERR:
return ColorRed
case level.CRIT:
return ColorBlackOnYellow
case level.ALERT:
return ColorWhiteOnRed
default:
return ColorWhite
}
}

115
pkg/event/event.go Normal file
View File

@ -0,0 +1,115 @@
package event
import (
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// An event that maps well to a log message
type Event struct {
FullMessage string
ShortMessage string
Attributes []attribute.KeyValue
// Defaults to INFO when converted into attributes and unset
Level level.SeverityLevel
extraSkipInStack int
}
type IntoEvent interface {
IntoEvent() Event
}
func (e Event) IntoEvent() Event {
return e
}
func (e Event) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 0)
attrs = append(attrs, attr.SourceCodeLocation(1+e.extraSkipInStack)...)
attrs = append(attrs, e.Attributes...)
if len(e.FullMessage) > 0 {
attrs = append(attrs, attr.LogMessageLong(e.FullMessage))
}
if len(e.ShortMessage) > 0 {
attrs = append(attrs, attr.LogMessageShort(e.ShortMessage))
}
if e.Level == 0 {
e.Level = level.INFO
}
attrs = append(attrs, attr.SeverityLevel(e.Level))
return attrs
}
func (e Event) AsOpts() trace.EventOption {
e.extraSkipInStack += 1
return trace.WithAttributes(e.IntoTraceAttributes()...)
}
func (e Event) SkipMoreInCallStack(skip int) Event {
e.extraSkipInStack += skip
return e
}
func NewInSpan[E IntoEvent](ev E, span trace.Span) {
event := ev.IntoEvent()
event.extraSkipInStack += 1
span.AddEvent(event.ShortMessage, trace.WithAttributes(event.IntoTraceAttributes()...))
}
type Error struct {
Err error
ExtendedMessage string
// Defaults to ALERT when converted into attributes and unset
Level level.SeverityLevel
Attributes []attribute.KeyValue
extraSkipInStack int
}
type IntoErrorEvent interface {
IntoErrorEvent() Error
}
func (e Error) Error() string {
return e.Err.Error()
}
func (e Error) IntoErrorEvent() Error {
return e
}
func (e Error) IntoTraceAttributes() []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 0)
attrs = append(attrs, e.Attributes...)
if len(e.ExtendedMessage) > 0 {
attrs = append(attrs, attr.LogMessageLong(e.ExtendedMessage))
}
if e.Level == 0 {
e.Level = level.ALERT
}
attrs = append(attrs, attr.SeverityLevel(e.Level), attr.LogMessageShort(e.Err.Error()))
attrs = append(attrs, attr.SourceCodeLocation(1+e.extraSkipInStack)...)
return attrs
}
func (e Error) AsOpts() trace.EventOption {
e.extraSkipInStack += 1
return trace.WithAttributes(e.IntoTraceAttributes()...)
}
func (e Error) SkipMoreInCallStack(skip int) Error {
e.extraSkipInStack += skip
return e
}
func NewErrInSpan[E IntoErrorEvent](err E, span trace.Span) E {
er := err.IntoErrorEvent()
er.extraSkipInStack += 1
span.RecordError(er.Err, er.AsOpts())
if er.Level <= level.ERR {
span.SetStatus(codes.Error, er.Error())
}
return err
}

View File

@ -0,0 +1,227 @@
package console_exporter
import (
"context"
"fmt"
"os"
"slices"
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/trace"
)
type TraceFormatter interface {
FormatSpanStart(span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error)
FormatSpanEnd(span trace.ReadOnlySpan, selectedAttrs []attribute.KeyValue, lvl level.SeverityLevel) (string, error)
FormatEvent(event trace.Event, span trace.ReadOnlySpan, selectedAttr []attribute.KeyValue, lvl level.SeverityLevel) (string, error)
}
// Configuration for the exporter.
//
// 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. 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
// Fields that should be removed from the output.
FilterOutFields []attribute.Key
// Print only trace events instead of whole traces.
EmitEventsOnly bool
// Add trace id as an attribute.
AddTraceId bool
// Print output only when an error is found
SkipNonErrors bool
// Used only when `EmitEventsOnly` is set to true.
TraceFormatter *TraceFormatter
SkipEmittingOnSpanStart bool
SkipEmittingOnSpanEnd bool
SkipAttributesOnSpanEnd bool
SkippAttributesOnSpanStart bool
}
type Processor struct {
lvl level.SeverityLevel
removedFields []attribute.Key
addTraceId bool
onlyErrs bool
onlyEvents bool
traceFormatter TraceFormatter
skipSpanStart bool
skipSpanEnd bool
skipAttrsOnSpanEnd bool
skipAttrsOnSpanStart bool
}
// NOTE: The configuration might change in future releases
func DefaultConsoleExportProcessor() trace.SpanProcessor {
fmt := NewPrettyMultilineFormatter()
return NewProcessor(ProcessorOptions{
FilterFromEnvVar: nil,
FilterOutFields: []attribute.Key{},
EmitEventsOnly: false,
SkipEmittingOnSpanStart: false,
SkippAttributesOnSpanStart: true,
AddTraceId: false,
TraceFormatter: &fmt,
})
}
func NewProcessor(opts ProcessorOptions) trace.SpanProcessor {
var formatter TraceFormatter
var lvl level.SeverityLevel
if opts.TraceFormatter != nil {
formatter = *opts.TraceFormatter
} else {
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 if lvl == level.SeverityLevel(0) {
lvl = level.TRACE + 1
}
return &Processor{
traceFormatter: formatter,
removedFields: opts.FilterOutFields,
addTraceId: opts.AddTraceId,
onlyEvents: opts.EmitEventsOnly,
onlyErrs: opts.SkipNonErrors,
skipSpanStart: opts.SkipEmittingOnSpanStart,
skipSpanEnd: opts.SkipEmittingOnSpanEnd,
skipAttrsOnSpanEnd: opts.SkipAttributesOnSpanEnd,
skipAttrsOnSpanStart: opts.SkippAttributesOnSpanStart,
lvl: lvl,
}
}
// Implements [trace.SpanProcessor]
func (e *Processor) OnStart(ctx context.Context, span trace.ReadWriteSpan) {
if !e.skipSpanStart && !e.onlyEvents {
attrs := span.Attributes()
filteredAttrs := make([]attribute.KeyValue, 0)
severityLvl := level.TRACE
if !e.skipAttrsOnSpanStart {
for i := range attrs {
if !slices.Contains(e.removedFields, attrs[i].Key) {
filteredAttrs = append(filteredAttrs, attrs[i])
}
if attrs[i].Key == attr.SeverityLevelKey {
severityLvl = level.FromString(attrs[i].Value.AsString())
}
}
}
if e.addTraceId {
filteredAttrs = append(filteredAttrs, attribute.String("trace_id", span.SpanContext().TraceID().String()))
}
if severityLvl <= e.lvl {
line, err := e.traceFormatter.FormatSpanStart(span, filteredAttrs, severityLvl)
if err != nil {
fmt.Println("FAILED TO FORMAT SPAN START")
} else {
fmt.Printf("%s", line)
}
}
}
return
}
// Implements [trace.SpanProcessor]
func (e *Processor) OnEnd(span trace.ReadOnlySpan) {
eventsString := ""
spanEndString := ""
for _, event := range span.Events() {
attrs := event.Attributes
filteredAttrs := make([]attribute.KeyValue, 0)
severityLvl := level.TRACE
for i := range attrs {
if !slices.Contains(e.removedFields, attrs[i].Key) {
filteredAttrs = append(filteredAttrs, attrs[i])
}
if attrs[i].Key == attr.SeverityLevelKey {
severityLvl = level.FromString(attrs[i].Value.AsString())
}
}
if e.addTraceId {
filteredAttrs = append(filteredAttrs, attribute.String("trace_id", span.SpanContext().TraceID().String()))
}
if severityLvl <= e.lvl {
eventString, err := e.traceFormatter.FormatEvent(event, span, filteredAttrs, severityLvl)
if err != nil {
fmt.Println("FAILED TO FORMAT TRACE EVENT")
} else {
eventsString += eventString
}
}
}
if !e.skipSpanEnd && !e.onlyEvents {
attrs := span.Attributes()
filteredAttrs := make([]attribute.KeyValue, len(attrs))
severityLvl := level.TRACE
if !e.skipAttrsOnSpanEnd {
for i := range attrs {
if !slices.Contains(e.removedFields, attrs[i].Key) {
filteredAttrs = append(filteredAttrs, attrs[i])
}
if attrs[i].Key == attr.SeverityLevelKey {
severityLvl = level.FromString(attrs[i].Value.AsString())
}
}
}
if e.addTraceId {
filteredAttrs = append(filteredAttrs, attribute.String("trace_id", span.SpanContext().TraceID().String()))
}
if severityLvl <= e.lvl {
spanString, err := e.traceFormatter.FormatSpanEnd(span, filteredAttrs, severityLvl)
if err != nil {
fmt.Println("FAILED TO FORMAT SPAN END")
} else {
spanEndString += spanString
}
}
}
if e.skipSpanStart {
fmt.Printf("%s", spanEndString+eventsString)
} else {
fmt.Printf("%s", eventsString+spanEndString)
}
return
}
// Implements [trace.SpanProcessor]
func (e *Processor) ForceFlush(ctx context.Context) error {
return nil
}
// Implements [trace.SpanExporter]
func (e *Processor) Shutdown(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return nil
}

View File

@ -0,0 +1,117 @@
package console_exporter
import (
"fmt"
"slices"
"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 NewPrettyMultilineFormatter() TraceFormatter {
return &PrettyMultilineFormatter{}
}
// A formatter that will print only events using a multiline format with colors.
// 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 {
if a.Key > b.Key {
return 1
} else {
return -1
}
})
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}
formattedSpanString := fmt.Sprintf(
"%s\n%s",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("[%s][SpanStart] ", lvl.String())+span.Name()),
attrs,
)
return formattedSpanString, nil
}
func (f *PrettyMultilineFormatter) 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
}
})
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}
formattedSpanString := fmt.Sprintf(
"%s\n%s",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("[%s][SpanEnd] ", lvl.String())+span.Name()),
attrs,
)
return formattedSpanString, nil
}
func (f *PrettyMultilineFormatter) 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
}
})
for _, kv := range selectedAttrs {
if len(kv.Key) > 0 {
attrs += fmt.Sprintf("\t%s = %s\n", string(kv.Key), AttrValueToString(kv.Value))
}
}
formattedSpanString := fmt.Sprintf(
"%s\n%s",
console_fmt.Bold(console_fmt.SeverityLevelToColor(lvl)+fmt.Sprintf("[%s][Event] ", lvl.String())+event.Name),
attrs,
)
return formattedSpanString, nil
}

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

@ -0,0 +1,76 @@
package exporters
import (
"context"
"git.ma-al.com/maal-libraries/observer/pkg/exporters/console_exporter"
gelf_exporter "git.ma-al.com/maal-libraries/observer/pkg/exporters/gelf_exporter"
otlphttp_exporter "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// Private type preventing implementation of TraceProcessor by external packages.
type traceProviderOpt sdktrace.TracerProviderOption
type TraceExporter interface {
IntoTraceProviderOption() traceProviderOpt
}
func NewFromSpanExporter(exporter sdktrace.SpanExporter) ExporterWithOptions {
return ExporterWithOptions{
exporter: exporter,
}
}
// Sneaky wrapper that makes it so that the TraceExporter can be created from SpanProcessor.
type proc struct {
sdktrace.SpanProcessor
}
func (p proc) IntoTraceProviderOption() traceProviderOpt {
return sdktrace.WithSpanProcessor(p)
}
func NewFromSpanProcessor(processor sdktrace.SpanProcessor) TraceExporter {
return TraceExporter(proc{
SpanProcessor: processor,
})
}
// Combined exporter with batch processor config
type ExporterWithOptions struct {
exporter sdktrace.SpanExporter
opts []sdktrace.BatchSpanProcessorOption
}
func (ecfg ExporterWithOptions) AddOption(opt sdktrace.BatchSpanProcessorOption) ExporterWithOptions {
ecfg.opts = append(ecfg.opts, opt)
return ecfg
}
func (ecfg ExporterWithOptions) IntoTraceProviderOption() traceProviderOpt {
return sdktrace.WithBatcher(ecfg.exporter, ecfg.opts...)
}
// An exporter printing to console with very small delay
func DevConsoleExporter(opts ...console_exporter.ProcessorOptions) TraceExporter {
var exporter TraceExporter
if len(opts) > 0 {
exporter = NewFromSpanProcessor(console_exporter.NewProcessor(opts[0]))
} else {
exporter = NewFromSpanProcessor(console_exporter.DefaultConsoleExportProcessor())
}
return TraceExporter(exporter)
}
// Default exporter to Graylog.
func GelfExporter(opts ...gelf_exporter.Option) (ExporterWithOptions, error) {
gelfExp, err := gelf_exporter.New(opts...)
return NewFromSpanExporter(gelfExp), err
}
// Exporter for traces over HTTP. Can be used with Jaeger.
// See documentation of [otlhttp_exporter] for details.
func OtlpHTTPExporter(opts ...otlphttp_exporter.Option) (ExporterWithOptions, error) {
otlpExp, err := otlphttp_exporter.New(context.Background(), opts...)
return NewFromSpanExporter(otlpExp), err
}

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

@ -1,12 +1,11 @@
package gelfexporter
import (
"fmt"
"log"
"time"
"git.ma-al.com/gora_filip/observer/pkg/level"
"git.ma-al.com/maal-libraries/observer/pkg/syslog"
"gopkg.in/Graylog2/go-gelf.v2/gelf"
)
@ -20,7 +19,7 @@ type GELFMessage struct {
// Timestamp in Unix
Timestamp time.Time `json:"timestamp"`
// Severity level matching Syslog standard.
Level level.SyslogLevel `json:"level"`
Level syslog.SyslogLevel `json:"level"`
// All additional field names must start with an underline.
ExtraFields map[string]interface{} `json:"extrafields,omitempty"`
@ -32,11 +31,17 @@ func Log(writer *gelf.UDPWriter, msg GELFMessage) {
if err != nil {
log.Println(err)
}
fmt.Printf("msg: %v sent\n", msg)
} 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,
@ -44,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

@ -4,7 +4,9 @@ import (
"context"
"sync"
"git.ma-al.com/gora_filip/observer/pkg/level"
"git.ma-al.com/maal-libraries/observer/pkg/attr"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"git.ma-al.com/maal-libraries/observer/pkg/syslog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
@ -27,6 +29,8 @@ func New(options ...Option) (*Exporter, error) {
return &Exporter{
gelfWriter: writer,
appName: cfg.AppName,
tag: cfg.Tag,
}, nil
}
@ -34,6 +38,7 @@ func New(options ...Option) (*Exporter, error) {
type Exporter struct {
gelfWriter *gelf.UDPWriter
appName string
tag string
stoppedMu sync.RWMutex
stopped bool
}
@ -59,36 +64,46 @@ 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)
}
attributes["from"] = "test"
for _, attr := range stub.Resource.Attributes() {
attributes[string(attr.Key)] = GetByType(attr.Value)
}
for i := range stub.Events {
event := stub.Events[i]
var gelf GELFMessage = GELFMessage{
Host: "salego",
Host: e.appName,
ShortMessage: event.Name,
Timestamp: stub.StartTime,
Level: level.DEBUG,
ExtraFields: attributes,
// 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,
}
for _, attr := range event.Attributes {
if attr.Key == "long_message_" {
gelf.LongMessage = attr.Value.AsString()
for _, attrKV := range event.Attributes {
if attrKV.Key == attr.LogMessageLongKey {
gelf.LongMessage = attrKV.Value.AsString()
continue
}
if attrKV.Key == attr.LogMessageShortKey {
gelf.ShortMessage = attrKV.Value.AsString()
continue
}
if attr.Key == "level_" {
gelf.Level = level.SyslogLevel(attr.Value.AsInt64())
if attrKV.Key == attr.SeverityLevelKey {
gelf.Level = level.FromString(attrKV.Value.AsString()).IntoSyslogLevel()
continue
}
attributes[string(attr.Key)] = GetByType(attr.Value)
attributes[string(attrKV.Key)] = GetByType(attrKV.Value)
}
gelf.ExtraFields = attributes
gelf.ExtraFields["tag"] = e.tag
Log(e.gelfWriter, gelf)
}

View File

@ -0,0 +1,101 @@
package fiber_tracing
import (
"context"
"log"
"git.ma-al.com/maal-libraries/observer/pkg/exporters"
"github.com/gofiber/fiber/v2"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
trc "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
trace "go.opentelemetry.io/otel/trace"
)
var (
TracingError error = nil
TP trc.TracerProvider
)
type Config struct {
AppName string
Version string
// Name of an organization providing the service
ServiceProvider string
Exporters []exporters.TraceExporter
ResourceAttributes []attribute.KeyValue
}
func newResource(config Config) *resource.Resource {
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
}
// NOTE: You can use [trace.WithAttributes] as a parameter to opts argument
func Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return Tracer.Start(ctx, spanName, opts...)
}
// NOTE: You can use [trace.WithAttributes] as a parameter to opts argument
// Returns [c.UserContext] as [context.Context]
func FStart(c *fiber.Ctx, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return Tracer.Start(c.UserContext(), c.Method()+" "+c.Route().Path, opts...)
}
// Just like [FStart] but makes it possible to assign custom span name.
func FStartRenamed(c *fiber.Ctx, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return Tracer.Start(c.UserContext(), spanName, opts...)
}
// Retrieve span using [fiber.Ctx]
func SpanFromContext(c *fiber.Ctx) trace.Span {
return trace.SpanFromContext(c.UserContext())
}
func NewMiddleware(config Config) func(*fiber.Ctx) error {
var tracerProviders []trc.TracerProviderOption
for _, exp := range config.Exporters {
tracerProviders = append(tracerProviders, exp.IntoTraceProviderOption())
}
tracerProviders = append(tracerProviders, trc.WithResource(newResource(config)))
TP = *trc.NewTracerProvider(tracerProviders...)
otel.SetTracerProvider(&TP)
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
if err != TracingError {
TracingError = err
log.Println(err)
}
}))
tracer := TP.Tracer("_maal-fiber-otel_")
return new(
middlewareConfig{
Tracer: tracer,
TracerStartAttributes: []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindServer),
trace.WithNewRoot(),
},
},
)
}
func ShutdownTracer() {
if err := TP.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,132 @@
package fiber_tracing
// This was copied from "github.com/psmarcin/fiber-opentelemetry/pkg/fiber-otel"
// and slighltly modified but this piece of code is yet to be fully integrated
// into the package.
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"
)
var Tracer = otel.Tracer("fiber_tracing_middleware")
// Config defines the config for middleware.
type middlewareConfig struct {
Tracer trace.Tracer
TracerStartAttributes []trace.SpanStartOption
}
// ConfigDefault is the default config
var configDefault = middlewareConfig{
Tracer: Tracer,
TracerStartAttributes: []trace.SpanStartOption{
trace.WithNewRoot(),
},
}
// Helper function to set default values
func configDefaults(config ...middlewareConfig) middlewareConfig {
// Return default config if nothing provided
if len(config) < 1 {
return configDefault
}
// Override default config
cfg := config[0]
if len(cfg.TracerStartAttributes) == 0 {
cfg.TracerStartAttributes = configDefault.TracerStartAttributes
}
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...)
// Return new handler
return func(c *fiber.Ctx) error {
spanStartAttributes := []attr.KeyValue{
semconv.HTTPMethod(c.Method()),
semconv.HTTPTarget(string(c.Request().RequestURI())),
semconv.HTTPURL(c.OriginalURL()),
semconv.HTTPUserAgent(string(c.Request().Header.UserAgent())),
semconv.HTTPRequestContentLength(c.Request().Header.ContentLength()),
semconv.HTTPScheme(c.Protocol()),
semconv.NetTransportTCP,
}
spanStartAttributes = append(spanStartAttributes, attr.ProcessStart()...)
opts := []trace.SpanStartOption{
trace.WithAttributes(spanStartAttributes...),
trace.WithSpanKind(trace.SpanKindServer),
}
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(
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,84 +1,110 @@
package level
import (
"strings"
"git.ma-al.com/maal-libraries/observer/pkg/syslog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type SyslogLevel uint8
type SeverityLevel uint8
const (
EMERG SyslogLevel = iota
// A magical zero value.
// WARN: DO NOT USE IN LOGS OR BASICALLY EVER
unset SeverityLevel = iota
// This event requires an immediate action. If you suspect that occurence of an event may signal that the
// data will get lost, corrupted, or that the application will change its behaviour following the event in
// an undesired way, select the ALERT level.
ALERT
// A critical error has occured. Critical errors are such which can be tough to fix or made the users
// experience significantly worse but unlike errors that trigger ALERT they can be fixed at any moment.
CRIT
// An error has occured but it is not expected to cause any serious issues. These will be often
// `Internal Server Error` responses from an HTTP server.
ERR
WARNING
NOTICE
// Signals that something suspicious has happened, for example, a query took too long to execute, gaining access
// to a resource took multiple attempts, a conflict was automatically resolved, etc.
WARN
// Used to inform about standard, expected events of an application, like creation of a new object or a new
// log-in from a user. Information that could be:
// - used to audit the application,
// - resolve customer's complaints,
// - track history of significant changes,
// - calculate valuable statistics;
// should be collected and logged at this level.
INFO
// Verbose information that is useful and meaningful to application developers and system administrators.
DEBUG
// Extremely verbose information that can be used to investigate performance of specific parts of an application.
// It is transled to [syslog.DEBUG] by [IntoSyslogLevel].
TRACE
)
// Level Keyword Description
// 0 emergencies System is unusable
// 1 alerts Immediate action is needed
// 2 critical Critical conditions exist
// 3 errors Error conditions exist
// 4 warnings Warning conditions exist
// 5 notification Normal, but significant, conditions exist
// 6 informational Informational messages
// 7 debugging Debugging messages
func (l SyslogLevel) String() string {
func (l SeverityLevel) String() string {
switch l {
case EMERG:
return "EMERG"
case ALERT:
return "ALERT"
case CRIT:
return "CRIT"
case ERR:
return "ERR"
case WARNING:
case WARN:
return "WARN"
case NOTICE:
return "NOTICE"
case INFO:
return "INFO"
case DEBUG:
return "DEBUG"
case TRACE:
return "TRACE"
default:
return "CRIT"
}
}
func LevelFromString(level string) SyslogLevel {
func FromString(level string) SeverityLevel {
level = strings.ToLower(level)
switch level {
case "EMERG":
return EMERG
case "ALERT":
case "alert":
return ALERT
case "CRIT":
case "crit", "critical":
return CRIT
case "ERR":
case "err", "error":
return ERR
case "WARN":
return WARNING
case "NOTICE":
return NOTICE
case "INFO":
case "warn", "warning":
return WARN
case "info":
return INFO
case "DEBUG":
case "debug":
return DEBUG
case "trace":
return TRACE
default:
return CRIT
return unset
}
}
func (lvl SyslogLevel) SetAttribute(att ...attribute.KeyValue) trace.SpanStartEventOption {
att = append(att, attribute.Int("level", int(lvl)))
return trace.WithAttributes(
att...,
)
func (lvl SeverityLevel) IntoTraceAttribute() attribute.KeyValue {
return attribute.String("level", lvl.String())
}
func (lvl SeverityLevel) IntoSyslogLevel() syslog.SyslogLevel {
switch lvl {
case ALERT:
return syslog.ALERT
case CRIT:
return syslog.CRIT
case ERR:
return syslog.ERR
case WARN:
return syslog.WARNING
case INFO:
return syslog.INFO
case DEBUG:
return syslog.DEBUG
case TRACE:
return syslog.DEBUG
default:
return syslog.EMERG
}
}

84
pkg/syslog/syslog.go Normal file
View File

@ -0,0 +1,84 @@
package syslog
import (
"go.opentelemetry.io/otel/attribute"
)
type SyslogLevel uint8
type IntoSyslogLevel interface {
IntoSyslogLevel() SyslogLevel
}
const (
// System is unusable
EMERG SyslogLevel = iota
// Immediate action is needed
ALERT
// Critical condition exists
CRIT
// An error condition has occured
ERR
// A suspicious behaviour has been observed
WARNING
// Significant but acceptable event has occured
NOTICE
// Informational details
INFO
// Data useful during debugging
DEBUG
)
func (l SyslogLevel) String() string {
switch l {
case EMERG:
return "EMERG"
case ALERT:
return "ALERT"
case CRIT:
return "CRIT"
case ERR:
return "ERR"
case WARNING:
return "WARN"
case NOTICE:
return "NOTICE"
case INFO:
return "INFO"
case DEBUG:
return "DEBUG"
default:
return "CRIT"
}
}
func LevelFromString(level string) SyslogLevel {
switch level {
case "EMERG":
return EMERG
case "ALERT":
return ALERT
case "CRIT":
return CRIT
case "ERR":
return ERR
case "WARN":
return WARNING
case "NOTICE":
return NOTICE
case "INFO":
return INFO
case "DEBUG":
return DEBUG
default:
return CRIT
}
}
func (lvl SyslogLevel) IntoTraceAttribute() attribute.KeyValue {
return attribute.String("level", lvl.String())
}
func (lvl SyslogLevel) IntoSyslogLevel() SyslogLevel {
return lvl
}

View File

@ -1,32 +0,0 @@
package tracer
import (
"encoding/json"
"git.ma-al.com/gora_filip/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func LongMessage(message string) attribute.KeyValue {
return attribute.KeyValue{Key: "long_message_", Value: attribute.StringValue(message)}
}
func Level(level level.SyslogLevel) attribute.KeyValue {
return attribute.KeyValue{Key: "level_", Value: attribute.Int64Value(int64(level))}
}
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))}
}
func RecordError(span trace.Span, err error) error {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return nil
}

View File

@ -1,202 +0,0 @@
package tracer
import (
"context"
"fmt"
"log"
"os"
"runtime"
gelfexporter "git.ma-al.com/gora_filip/observer/pkg/gelf_exporter"
"github.com/gofiber/fiber/v2"
fiberOpentelemetry "github.com/psmarcin/fiber-opentelemetry/pkg/fiber-otel"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
trc "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
trace "go.opentelemetry.io/otel/trace"
)
var (
TracingError error = nil
TP trc.TracerProvider
)
type CustomExporter struct {
jaeger *otlptrace.Exporter
stdouttrace *stdouttrace.Exporter
}
type Config struct {
AppName string
JaegerUrl string
GelfUrl string
Version string
}
func NewCustomExporter(jaegerUrl string) (trc.SpanExporter, error) {
var jaeg *otlptrace.Exporter
var outrace *stdouttrace.Exporter
var err error
outrace, err = stdouttrace.New(
stdouttrace.WithWriter(os.Stdout),
stdouttrace.WithPrettyPrint(),
stdouttrace.WithoutTimestamps(),
)
if err != nil {
return &CustomExporter{}, err
}
if len(jaegerUrl) > 0 {
jaeg = otlptracehttp.NewUnstarted(otlptracehttp.WithEndpointURL(jaegerUrl))
}
return &CustomExporter{
jaeger: jaeg,
stdouttrace: outrace,
}, nil
}
func (e *CustomExporter) ExportSpans(ctx context.Context, spans []trc.ReadOnlySpan) error {
if TracingError == nil {
if e.jaeger != nil {
err := e.jaeger.ExportSpans(ctx, spans)
return err
} else {
return e.printOnlyOnError(ctx, spans)
}
}
return nil
}
func (e *CustomExporter) printOnlyOnError(ctx context.Context, spans []trc.ReadOnlySpan) error {
var err error
for _, s := range spans {
if s.Status().Code == codes.Error {
err = e.stdouttrace.ExportSpans(ctx, spans)
break
}
}
return err
}
func (e *CustomExporter) Shutdown(ctx context.Context) error {
if e.jaeger != nil {
e.stdouttrace.Shutdown(ctx)
return e.jaeger.Shutdown(ctx)
} else {
return e.stdouttrace.Shutdown(ctx)
}
}
func newResource(config Config) *resource.Resource {
r := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(config.AppName),
semconv.ServiceVersionKey.String(config.Version),
attribute.String("service.provider", "maal"),
)
return r
}
func NewTracer(config Config) func(*fiber.Ctx) error {
l := log.New(os.Stdout, "", 0)
var tracerProviders []trc.TracerProviderOption
otlpExporter := otlptracehttp.NewUnstarted(otlptracehttp.WithEndpointURL(config.JaegerUrl))
gelfExporter, err := gelfexporter.New(
gelfexporter.WithGelfUrl(config.GelfUrl),
gelfexporter.WithAppName("salego"),
)
if err != nil {
l.Fatal(err)
}
tracerProviders = append(tracerProviders, trc.WithBatcher(otlpExporter))
tracerProviders = append(tracerProviders, trc.WithBatcher(gelfExporter))
tracerProviders = append(tracerProviders, trc.WithResource(newResource(config)))
TP = *trc.NewTracerProvider(tracerProviders...)
otel.SetTracerProvider(&TP)
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
if err != TracingError {
TracingError = err
log.Println(err)
}
}))
tracer := TP.Tracer("fiber-otel-router")
return fiberOpentelemetry.New(
fiberOpentelemetry.Config{
Tracer: tracer,
SpanName: "{{ .Method }} {{ .Path }}",
TracerStartAttributes: []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindServer),
trace.WithNewRoot(),
},
},
)
}
func ShutdownTracer() {
if err := TP.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
}
func Handler(fc *fiber.Ctx) (context.Context, trace.Span) {
spanName := fmt.Sprint(fc.OriginalURL())
simpleCtx, span := fiberOpentelemetry.Tracer.Start(fc.UserContext(), spanName)
fc.SetUserContext(simpleCtx)
_, file, line, _ := runtime.Caller(1)
span.SetAttributes(
attribute.String("service.layer", "handler"),
attribute.String("file", file),
attribute.String("line", fmt.Sprintf("%d", line)),
)
return simpleCtx, span
}
func Service(c context.Context, spanName string) (context.Context, trace.Span) {
simpleCtx, span := fiberOpentelemetry.Tracer.Start(c, spanName)
var attribs []attribute.KeyValue
_, file, line, _ := runtime.Caller(1)
attribs = append(
attribs,
attribute.String("service.layer", "service"),
attribute.String("file", file),
attribute.String("line", fmt.Sprintf("%d", line)),
)
span.SetAttributes(attribs...)
return simpleCtx, span
}
func Repository(c context.Context, spanName string) (context.Context, trace.Span) {
ctx2, span := fiberOpentelemetry.Tracer.Start(c, spanName)
var attribs []attribute.KeyValue
_, file, line, _ := runtime.Caller(1)
attribs = append(attribs,
attribute.String("file", file),
attribute.String("line", fmt.Sprintf("%d", line)),
)
span.SetAttributes(attribs...)
return ctx2, span
}