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)