diff --git a/logging/zerolog/README.md b/logging/zerolog/README.md new file mode 100644 index 0000000..5ac03d4 --- /dev/null +++ b/logging/zerolog/README.md @@ -0,0 +1,54 @@ +# Zerolog + OpenTelemetry + Hezrt + +## [Document](https://www.cloudwego.io/docs/hertz/tutorials/observability/open-telemetry/) + +## Example + +1. See this [example](https://github.com/cloudwego/hertz-examples/tree/main/opentelemetry) +2. Small change in herzt server + +```go +import ( + // ... + + "github.com/cloudwego/hertz/pkg/common/hlog" + hertzZerolog "github.com/hertz-contrib/logger/zerolog" + hertzOtelZerolog "github.com/hertz-contrib/obs-opentelemetry/logging/zerolog" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + // ... + + p := provider.NewOpenTelemetryProvider( + provider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + provider.WithExportEndpoint("localhost:4317"), + provider.WithInsecure(), + ) + defer p.Shutdown(context.Background()) + + tracer, cfg := hertztracing.NewServerTracer() + h := server.Default(tracer) + h.Use(hertztracing.ServerMiddleware(cfg)) + + logger := hertzZerolog.New( + hertzZerolog.WithOutput(w), // allows to specify output + hertzZerolog.WithLevel(hlog.LevelInfo), // option with log level + hertzZerolog.WithCaller(), // option with caller + hertzZerolog.WithTimestamp(), // option with timestamp + hertzZerolog.WithFormattedTimestamp(time.RFC3339), + ) + + log.Logger = logger.Unwrap() // log.Output(w).With().Caller().Logger() + log.Logger = log.Level(zerolog.InfoLevel) + + otelLogger := hertzOtelZerolog.NewLogger(hertzOtelZerolog.WithLogger(logger)) + log.Logger = otelLogger.Unwrap() + hlog.SetLogger(otelLogger) + + // ... +} +``` + diff --git a/logging/zerolog/go.mod b/logging/zerolog/go.mod new file mode 100644 index 0000000..b29f1c1 --- /dev/null +++ b/logging/zerolog/go.mod @@ -0,0 +1,27 @@ +module github.com/hertz-contrib/obs-opentelemetry/logging/zerolog + +go 1.19 + +require ( + github.com/cloudwego/hertz v0.8.0 + github.com/hertz-contrib/logger/zerolog v0.0.0-20231211035138-acc7b4e2984b + github.com/rs/zerolog v1.31.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0 + go.opentelemetry.io/otel/sdk v1.22.0 + go.opentelemetry.io/otel/trace v1.22.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + golang.org/x/sys v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/logging/zerolog/go.sum b/logging/zerolog/go.sum new file mode 100644 index 0000000..00c27dc --- /dev/null +++ b/logging/zerolog/go.sum @@ -0,0 +1,109 @@ +github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/cloudwego/hertz v0.8.0 h1:rjALfbD/E3IkaNDksQ4oF0nA5d03FfSEx3yc2PkJklo= +github.com/cloudwego/hertz v0.8.0/go.mod h1:WliNtVbwihWHHgAaIQEbVXl0O3aWj0ks1eoPrcEAnjs= +github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-logr/logr v1.2.2/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= +github.com/hertz-contrib/logger/zerolog v0.0.0-20231211035138-acc7b4e2984b h1:PZsjswxU/8veXL1uPj6vC8Fg36gA4RocVhR0TFK5Qaw= +github.com/hertz-contrib/logger/zerolog v0.0.0-20231211035138-acc7b4e2984b/go.mod h1:GieP9vSEQR5CO5QgJCXy27GXr4GxgM9VqqFG7u+Q3h4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0 h1:zr8ymM5OWWjjiWRzwTfZ67c905+2TMHYp2lMJ52QTyM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0/go.mod h1:sQs7FT2iLVJ+67vYngGJkPe1qr39IzaBzaj9IDNNY8k= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/logging/zerolog/logger.go b/logging/zerolog/logger.go new file mode 100644 index 0000000..199fffc --- /dev/null +++ b/logging/zerolog/logger.go @@ -0,0 +1,123 @@ +// Copyright 2024 CloudWeGo Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zerolog + +import ( + "context" + "errors" + + "github.com/cloudwego/hertz/pkg/common/hlog" + hertzzerolog "github.com/hertz-contrib/logger/zerolog" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type Logger struct { + *hertzzerolog.Logger + config *config +} + +// Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md#json-formats +const ( + TraceIDKey = "trace_id" + SpanIDKey = "span_id" + TraceFlagsKey = "trace_flags" +) + +type ExtraKey string + +func NewLogger(opts ...Option) *Logger { + cfg := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(cfg) + } + logger := *cfg.logger + zerologLogger := logger.Unwrap().Hook(cfg.getZerologHookFn()) + + return &Logger{ + Logger: hertzzerolog.From(zerologLogger), + config: cfg, + } +} + +func (l *Logger) CtxLogf(level hlog.Level, ctx context.Context, format string, kvs ...any) { + var zlevel zerolog.Level + + switch level { + case hlog.LevelDebug, hlog.LevelTrace: + zlevel = zerolog.DebugLevel + l.Logger.CtxDebugf(ctx, format, kvs...) + case hlog.LevelInfo: + zlevel = zerolog.InfoLevel + l.Logger.CtxInfof(ctx, format, kvs...) + case hlog.LevelNotice, hlog.LevelWarn: + zlevel = zerolog.WarnLevel + l.Logger.CtxWarnf(ctx, format, kvs...) + case hlog.LevelError: + zlevel = zerolog.ErrorLevel + l.Logger.CtxErrorf(ctx, format, kvs...) + case hlog.LevelFatal: + zlevel = zerolog.FatalLevel + l.Logger.CtxFatalf(ctx, format, kvs...) + default: + zlevel = zerolog.WarnLevel + l.Logger.CtxWarnf(ctx, format, kvs...) + } + + span := trace.SpanFromContext(ctx) + if !span.IsRecording() { + l.Logger.Logf(level, format, kvs...) + return + } + + // set span status + if zlevel >= l.config.traceConfig.errorSpanLevel { + msg := getMessage(format, kvs) + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(msg), trace.WithStackTrace(l.config.traceConfig.recordStackTraceInSpan)) + } +} + +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelInfo, ctx, format, v...) +} + +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...any) { + l.CtxLogf(hlog.LevelError, ctx, format, v...) +} + +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...any) { + // l.CtxLogf(hlog.LevelFatal, ctx, format, v...) + l.Logger.CtxFatalf(ctx, format, v...) +} diff --git a/logging/zerolog/logger_test.go b/logging/zerolog/logger_test.go new file mode 100644 index 0000000..c746f3d --- /dev/null +++ b/logging/zerolog/logger_test.go @@ -0,0 +1,134 @@ +// Copyright 2024 CloudWeGo Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zerolog + +import ( + "bytes" + "context" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + hertzZerolog "github.com/hertz-contrib/logger/zerolog" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +// TestLogger test logger work with opentelemetry +func TestLogger(t *testing.T) { + ctx := context.Background() + + buf := new(bytes.Buffer) + + shutdown := stdoutProvider(ctx) + defer shutdown() + + hertzZerologer := hertzZerolog.New( + hertzZerolog.WithOutput(buf), + hertzZerolog.WithLevel(hlog.LevelDebug), + ) + + logger := NewLogger( + WithLogger(hertzZerologer), + WithTraceErrorSpanLevel(zerolog.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + + hlog.SetLogger(logger) + + logger.Info("log from origin zerolog") + assert.Contains(t, buf.String(), "log from origin zerolog") + buf.Reset() + + tracer := otel.Tracer("test otel std logger") + + ctx, span := tracer.Start(ctx, "root") + + hlog.CtxInfof(ctx, "hello %s", "world") + assert.Contains(t, buf.String(), "trace_id") + assert.Contains(t, buf.String(), "span_id") + assert.Contains(t, buf.String(), "trace_flags") + buf.Reset() + + span.End() + + ctx, child1 := tracer.Start(ctx, "child1") + + hlog.CtxTracef(ctx, "trace %s", "this is a trace log") + hlog.CtxDebugf(ctx, "debug %s", "this is a debug log") + hlog.CtxInfof(ctx, "info %s", "this is a info log") + + child1.End() + assert.Equal(t, codes.Unset, child1.(sdktrace.ReadOnlySpan).Status().Code) + + ctx, child2 := tracer.Start(ctx, "child2") + hlog.CtxNoticef(ctx, "notice %s", "this is a notice log") + hlog.CtxWarnf(ctx, "warn %s", "this is a warn log") + hlog.CtxErrorf(ctx, "error %s", "this is a error log") + + child2.End() + assert.Equal(t, codes.Error, child2.(sdktrace.ReadOnlySpan).Status().Code) + + _, errSpan := tracer.Start(ctx, "error") + + hlog.Info("no trace context") + + errSpan.End() +} + +// TestLogLevel test SetLevel +func TestLogLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger( + WithTraceErrorSpanLevel(zerolog.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + + logger.SetLevel(hlog.LevelError) + + // output to buffer + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.NotContains(t, buf.String(), "this is a debug log") + + logger.SetLevel(hlog.LevelDebug) + + logger.Debug("this is a debug log") + assert.Contains(t, buf.String(), "this is a debug log") +} diff --git a/logging/zerolog/option.go b/logging/zerolog/option.go new file mode 100644 index 0000000..d97d7e6 --- /dev/null +++ b/logging/zerolog/option.go @@ -0,0 +1,107 @@ +// Copyright 2024 CloudWeGo Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zerolog + +import ( + "errors" + + hertzzerolog "github.com/hertz-contrib/logger/zerolog" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type traceConfig struct { + recordStackTraceInSpan bool + errorSpanLevel zerolog.Level +} + +type config struct { + logger *hertzzerolog.Logger + traceConfig *traceConfig + hookFunc zerolog.HookFunc +} + +// defaultConfig default config +func defaultConfig() *config { + return &config{ + traceConfig: &traceConfig{ + recordStackTraceInSpan: true, + errorSpanLevel: zerolog.ErrorLevel, + }, + logger: hertzzerolog.New(), + } +} + +// WithLogger configures logger +func WithLogger(logger *hertzzerolog.Logger) Option { + return option(func(cfg *config) { + cfg.logger = logger + }) +} + +// WithTraceErrorSpanLevel trace error span level option +func WithTraceErrorSpanLevel(level zerolog.Level) Option { + return option(func(cfg *config) { + cfg.traceConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan record stack track option +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} + +func (cfg config) getZerologHookFn() zerolog.HookFunc { + if cfg.hookFunc != nil { + return cfg.hookFunc + } + return func(e *zerolog.Event, level zerolog.Level, message string) { + ctx := e.GetCtx() + span := trace.SpanFromContext(ctx) + spanCtx := span.SpanContext() + + if !spanCtx.IsValid() { + return + } + + e.Any(SpanIDKey, spanCtx.SpanID()) + e.Any(TraceIDKey, spanCtx.TraceID()) + e.Any(TraceFlagsKey, spanCtx.TraceFlags()) + + if !span.IsRecording() { + return + } + + // set span status + if level >= cfg.traceConfig.errorSpanLevel { + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(message), + trace.WithStackTrace(cfg.traceConfig.recordStackTraceInSpan)) + } + } +} diff --git a/logging/zerolog/utils.go b/logging/zerolog/utils.go new file mode 100644 index 0000000..b501e98 --- /dev/null +++ b/logging/zerolog/utils.go @@ -0,0 +1,50 @@ +// Copyright 2024 CloudWeGo Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zerolog + +import ( + "fmt" + "strings" + + "github.com/rs/zerolog" +) + +// getMessage format with Sprint, Sprintf, or neither. +func getMessage(template string, fmtArgs []any) string { + if len(fmtArgs) == 0 { + return template + } + + if template != "" { + return fmt.Sprintf(template, fmtArgs...) + } + + if len(fmtArgs) == 1 { + if str, ok := fmtArgs[0].(string); ok { + return str + } + } + return fmt.Sprint(fmtArgs...) +} + +// OtelSeverityText convert zerolog level to otel severityText +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields +func OtelSeverityText(lv zerolog.Level) string { + s := strings.ToUpper(lv.String()) + if s == "PANIC" { + s = "FATAL" + } + return s +}