From ab5b70704d88ddefe2f9766876238d10662ffaeb Mon Sep 17 00:00:00 2001 From: Natalia Goc Date: Thu, 16 May 2024 13:45:13 +0200 Subject: [PATCH] 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. --- example/main.go | 4 +- pkg/attr/attr.go | 224 +++++++++++++++++++++++++++++ pkg/code_location/code_location.go | 13 ++ pkg/gelf_exporter/gelf.go | 4 +- pkg/gelf_exporter/trace.go | 24 ++-- pkg/level/level.go | 86 ++++++----- pkg/syslog/syslog.go | 84 +++++++++++ pkg/tracer/event.go | 18 --- 8 files changed, 390 insertions(+), 67 deletions(-) create mode 100644 pkg/attr/attr.go create mode 100644 pkg/code_location/code_location.go create mode 100644 pkg/syslog/syslog.go diff --git a/example/main.go b/example/main.go index 532431e..ac13540 100644 --- a/example/main.go +++ b/example/main.go @@ -70,7 +70,7 @@ func main() { } func Serv(ctx context.Context) *fiber.Error { - ctx, span := tracer.Service(ctx, "name of the span") + ctx, span := tracer.Service(ctx, "service", "service span") defer span.End() for range []int{1, 2, 3} { @@ -86,7 +86,7 @@ func Serv(ctx context.Context) *fiber.Error { } func Repo(ctx context.Context) error { - ctx, span := tracer.Repository(ctx, "name of the span") + ctx, span := tracer.Repository(ctx, "repo", "repo span") defer span.End() for range []int{1, 2, 3} { diff --git a/pkg/attr/attr.go b/pkg/attr/attr.go new file mode 100644 index 0000000..b90c7e8 --- /dev/null +++ b/pkg/attr/attr.go @@ -0,0 +1,224 @@ +package attr + +import ( + "encoding/json" + "os" + "runtime" + "runtime/debug" + + "git.ma-al.com/gora_filip/pkg/level" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/semconv/v1.25.0" +) + +type IntoTraceAttribute interface { + IntoTraceAttribute() attribute.KeyValue +} + +type IntoTraceAttributes interface { + IntoTraceAttributes() []attribute.KeyValue +} + +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") +) + +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)), + } +} diff --git a/pkg/code_location/code_location.go b/pkg/code_location/code_location.go new file mode 100644 index 0000000..4c9b955 --- /dev/null +++ b/pkg/code_location/code_location.go @@ -0,0 +1,13 @@ +package code_location + +type CodeLocation struct { + FilePath string + FuncName string + LineNumber int + ColumnNumber int +} + +func FromStackTrace(...atDepth int) { + pc, file, line, _ := runtime.Caller(1 + skipLevelsInCallStack) + funcName := runtime.FuncForPC(pc).Name() +} diff --git a/pkg/gelf_exporter/gelf.go b/pkg/gelf_exporter/gelf.go index b31805b..47cf51e 100644 --- a/pkg/gelf_exporter/gelf.go +++ b/pkg/gelf_exporter/gelf.go @@ -6,7 +6,7 @@ import ( "time" - "git.ma-al.com/gora_filip/observer/pkg/level" + "git.ma-al.com/gora_filip/pkg/syslog" "gopkg.in/Graylog2/go-gelf.v2/gelf" ) @@ -20,7 +20,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"` diff --git a/pkg/gelf_exporter/trace.go b/pkg/gelf_exporter/trace.go index 8a7316a..e2057ed 100644 --- a/pkg/gelf_exporter/trace.go +++ b/pkg/gelf_exporter/trace.go @@ -4,7 +4,9 @@ import ( "context" "sync" - "git.ma-al.com/gora_filip/observer/pkg/level" + "git.ma-al.com/gora_filip/pkg/attr" + "git.ma-al.com/gora_filip/pkg/level" + "git.ma-al.com/gora_filip/pkg/syslog" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" @@ -69,24 +71,26 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) 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 == "long_message" { + gelf.LongMessage = 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) diff --git a/pkg/level/level.go b/pkg/level/level.go index 7dc774b..ab31dab 100644 --- a/pkg/level/level.go +++ b/pkg/level/level.go @@ -1,47 +1,51 @@ package level import ( + "git.ma-al.com/gora_filip/pkg/syslog" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) -type SyslogLevel uint8 +type SeverityLevel uint8 const ( - EMERG SyslogLevel = iota - ALERT + // 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 SeverityLevel = iota + // 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: @@ -51,10 +55,8 @@ func (l SyslogLevel) String() string { } } -func LevelFromString(level string) SyslogLevel { +func FromString(level string) SeverityLevel { switch level { - case "EMERG": - return EMERG case "ALERT": return ALERT case "CRIT": @@ -62,9 +64,7 @@ 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": @@ -74,11 +74,27 @@ func LevelFromString(level string) SyslogLevel { } } -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 + } } diff --git a/pkg/syslog/syslog.go b/pkg/syslog/syslog.go new file mode 100644 index 0000000..4713093 --- /dev/null +++ b/pkg/syslog/syslog.go @@ -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 +} diff --git a/pkg/tracer/event.go b/pkg/tracer/event.go index 97117e3..7894c43 100644 --- a/pkg/tracer/event.go +++ b/pkg/tracer/event.go @@ -1,29 +1,11 @@ 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)