Compare commits

21 Commits

Author SHA1 Message Date
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
21 changed files with 1421 additions and 331 deletions

View File

@ -8,52 +8,51 @@ 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/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)
exps = append(exps, exporters.DevConsoleExporter(console_exporter.ProcessorOptions{}))
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)
}
main.Use(tracer.NewTracer(tracer.Config{
AppName: "test",
JaegerUrl: "http://localhost:4318/v1/traces",
GelfUrl: "192.168.220.30:12201",
Version: "1",
main.Use(tracing.NewMiddleware(tracing.Config{
AppName: "example",
Version: "0.0.0",
ServiceProvider: "maal",
Exporters: exps,
}))
defer tracer.ShutdownTracer()
defer tracing.ShutdownTracer()
main.Get("/", func(c *fiber.Ctx) error {
ctx, span := tracer.Handler(c)
defer span.End()
main.Use(func(c *fiber.Ctx) error {
span := tracing.SpanFromContext(c)
span.AddEvent("pushed into a span an event from middleware")
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),
),
)
err := Serv(ctx)
if err != nil {
return tracer.RecordError(span, err)
}
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 +68,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} {
@ -86,7 +106,10 @@ func Serv(ctx context.Context) *fiber.Error {
}
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

8
go.mod
View File

@ -1,15 +1,12 @@
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
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
@ -30,6 +27,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

4
go.sum
View File

@ -1,5 +1,3 @@
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=
@ -59,8 +57,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDp
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=

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

@ -0,0 +1,268 @@
package attr
import (
"encoding/json"
"os"
"runtime"
"runtime/debug"
"time"
"git.ma-al.com/maal-libraries/observer/pkg/level"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/semconv/v1.25.0"
"go.opentelemetry.io/otel/trace"
)
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")
)
type ServiceArchitectureLayer string
const (
LayerFrameworkMiddleware ServiceArchitectureLayer = "framework_middleware"
LayerHandler = "handler"
LayerService = "service"
LayerRepository = "repository"
LayerORM = "orm"
LayerUtil = "util"
)
// 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))}
}
// 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()),
}
}

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,218 @@
package console_exporter
import (
"context"
"fmt"
"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. NOT IMPLEMENTED!
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.FilterOnLevel != level.SeverityLevel(0) {
lvl = opts.FilterOnLevel
} else {
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,93 @@
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 (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), kv.Value.AsString())
}
}
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), kv.Value.AsString())
}
}
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), kv.Value.AsString())
}
}
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,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

@ -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,7 +31,6 @@ func Log(writer *gelf.UDPWriter, msg GELFMessage) {
if err != nil {
log.Println(err)
}
fmt.Printf("msg: %v sent\n", msg)
}
}

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"
@ -63,30 +65,34 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
attributes[string(attr.Key)] = GetByType(attr.Value)
}
attributes["from"] = "test"
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,
ExtraFields: attributes,
}
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)
}
Log(e.gelfWriter, gelf)

View File

@ -0,0 +1,96 @@
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
}
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", config.ServiceProvider),
)
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,90 @@
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"
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
}
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.HTTPRoute(c.Route().Path),
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...)
otelCtx, span := Tracer.Start(
c.UserContext(),
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 err
}
}

View File

@ -1,60 +1,67 @@
package level
import (
"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 {
switch level {
case "EMERG":
return EMERG
case "ALERT":
return ALERT
case "CRIT":
@ -62,23 +69,39 @@ func LevelFromString(level string) SyslogLevel {
case "ERR":
return ERR
case "WARN":
return WARNING
case "NOTICE":
return NOTICE
return WARN
case "INFO":
return INFO
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
}