observer/pkg/attr/attr.go
Natalia Goc 39bf8b8356 feat: loosen the type restrictions on attr.Json
Just like `json.Marshal` takes arguments of `any` type, the `attr.Json`
should have similarly loose restrictions on input type.
2024-09-25 16:11:08 +02:00

467 lines
15 KiB
Go

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),
}
}