package attr import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "io" "os" "runtime" "runtime/debug" "time" "git.ma-al.com/maal-libraries/observer/pkg/level" "github.com/gofrs/uuid" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.25.0" "go.opentelemetry.io/otel/trace" ) type KV = attribute.KeyValue 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" ) type secretKey struct { Key []byte Cipher cipher.Block Nonce []byte } // Interprets a string as AES secret key (32 bytes) first decoding it with base64 // It can be used to set the variable `EncryptSecretKey` which is responsible // for encrypting the `Encrypted` attributes. func NewSecretKey(key string) (secretKey, error) { keyBytes, err := base64.RawStdEncoding.DecodeString(key) if err != nil { return secretKey{}, err } if len(keyBytes) != 32 { return secretKey{}, errors.New("wrong length of encryption key, should be 32 bits") } cipher, err := aes.NewCipher(keyBytes) if err != nil { return secretKey{}, err } nonce := make([]byte, 12) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return secretKey{}, err } return secretKey{ Key: keyBytes, Cipher: cipher, Nonce: nonce, }, nil } // **Unless set, it will default to a random key that cannot be later retrievied!** // // The variable is used to encrypt values provided to the `Encrypted` attribute. var EncryptSecretKey secretKey = func() secretKey { key := make([]byte, 32) rand.Read(key) cipher, err := aes.NewCipher(key) if err != nil { panic(err) } nonce := make([]byte, 12) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { panic(err) } return secretKey{ Key: key, Cipher: cipher, Nonce: nonce, } }() // 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))} } // Create an arbitrary attribute with value marshalled to json. // In case of marshalling error, it is returned in place of value. func Json(key string, val any) attribute.KeyValue { data, err := json.Marshal(val) if err != nil { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())} } else { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(data))} } } // Create an arbitrary attribute with a `string` value. func String(key string, val string) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val)} } // Create an arbitrary attribute with a `[]string` value. func StringSlice(key string, val []string) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringSliceValue(val)} } // Create an arbitrary attribute with an `int` value. func Int(key string, val int) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(val)} } // Create an arbitrary attribute with an `int64` value. func Int64(key string, val int64) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.Int64Value(val)} } // Cast value to an `int` to create a new attribute. func Uint(key string, val uint) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))} } // Cast value to an `int` to create a new attribute. func Uint8(key string, val uint8) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))} } // Create an arbitrary attribute using an `uuid.UUID` from `github.com/gofrs/uuid` as value. func Uuid(key string, val uuid.UUID) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.String())} } // Create an arbitrary attribute using standard library's `time.Time` as value. It will be formatted using RFC3339. func Time(key string, val time.Time) attribute.KeyValue { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.Format(time.RFC3339))} } // Create an arbitrary attribute with bytes encoded to base64 (RFC 4648) as value. func BytesB64(key string, val []byte) attribute.KeyValue { res := base64.StdEncoding.EncodeToString(val) return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)} } // Create an arbitrary attribute with bytes encoded to hexadecimal format as value. func BytesHex(key string, val []byte) attribute.KeyValue { res := hex.EncodeToString(val) return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)} } // Create an arbitrary attribute with value encrypted using `secretKey` which should be set on // global variable `EncryptSecretKey` using `NewSecretKey`. The result will be encoded using // base64. // // This approach is an alternative to logs tokenization. It is using AES symmetric encryption // that is suspectible to brute force attacks. It is a computionally expensive attribute to // generate. // // In most cases, for very sensitive data it would be a better approach to use masking instead. // Encrypting the fields of the logs/traces can provide an extra protection while they are being // transported to a log collector and when the collector does not encrypt logs at rest (but most // should implement this feature). This will mostly protect the logs from developers working // with them provided that they do not have access to the key. The key should be set from an // environment variable defined on application deployment. Alternatively it could be set from // a secure vault, a software for storing private keys. func Encrypted(key string, val string) attribute.KeyValue { aesGcm, err := cipher.NewGCM(EncryptSecretKey.Cipher) if err != nil { return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())} } resBytes := aesGcm.Seal(nil, EncryptSecretKey.Nonce, []byte(val), nil) res := base64.StdEncoding.EncodeToString(resBytes) return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)} } // Creates an arbitrary attribute with masked value. It will leave only last 4 (or less) characters // unmasked by 'X' characters. // // It is not a good idea to use it for logging passwords as it preserves the lenght of the input. // // Masking is a good idea for very sensitive data like official identity numbers, or addresses. // Storing such data in logs is usually too much of a risk even when it is encrypted. // However, for the purpose of debugging it might be convenient to be able to distinguish one record // from another. func Masked(key string, val string) attribute.KeyValue { lenght := len(val) var unmasked int if lenght <= 4 { unmasked = 1 } else { if lenght <= 8 { unmasked = 2 } else { if lenght <= 12 { unmasked = 3 } else { unmasked = 4 } } } masked := lenght - unmasked resBytes := make([]byte, lenght) i := 0 for ; i < masked; i++ { resBytes[i] = byte('X') } for ; i < lenght; i++ { resBytes[i] = byte(val[i]) } return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(resBytes))} } // 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()), } } func DBRowsAffected(rows int64) attribute.KeyValue { return attribute.KeyValue{ Key: DBRowsAffectedKey, Value: attribute.Int64Value(rows), } }