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") DBRowsAffectedKey = attribute.Key("db.rows_affected") ) 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()), } }