2024-05-16 11:45:13 +00:00
|
|
|
package attr
|
|
|
|
|
|
|
|
import (
|
2024-09-13 13:48:16 +00:00
|
|
|
"crypto/aes"
|
|
|
|
"crypto/cipher"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/hex"
|
2024-05-16 11:45:13 +00:00
|
|
|
"encoding/json"
|
2024-09-13 13:48:16 +00:00
|
|
|
"errors"
|
|
|
|
"io"
|
2024-05-16 11:45:13 +00:00
|
|
|
"os"
|
|
|
|
"runtime"
|
|
|
|
"runtime/debug"
|
2024-05-20 15:29:00 +00:00
|
|
|
"time"
|
2024-05-16 11:45:13 +00:00
|
|
|
|
2024-05-20 06:20:13 +00:00
|
|
|
"git.ma-al.com/maal-libraries/observer/pkg/level"
|
2024-09-13 13:48:16 +00:00
|
|
|
"github.com/gofrs/uuid"
|
2024-05-16 11:45:13 +00:00
|
|
|
"go.opentelemetry.io/otel/attribute"
|
2024-09-13 13:48:16 +00:00
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
|
2024-05-16 16:19:36 +00:00
|
|
|
"go.opentelemetry.io/otel/trace"
|
2024-05-16 11:45:13 +00:00
|
|
|
)
|
|
|
|
|
2024-09-13 13:48:16 +00:00
|
|
|
type KV = attribute.KeyValue
|
2024-05-20 15:23:06 +00:00
|
|
|
type KeyValue = attribute.KeyValue
|
2024-05-20 15:29:00 +00:00
|
|
|
type Key = attribute.Key
|
|
|
|
type Value = attribute.Value
|
2024-05-20 15:23:06 +00:00
|
|
|
|
2024-05-16 11:45:13 +00:00
|
|
|
type IntoTraceAttribute interface {
|
|
|
|
IntoTraceAttribute() attribute.KeyValue
|
|
|
|
}
|
|
|
|
|
|
|
|
type IntoTraceAttributes interface {
|
|
|
|
IntoTraceAttributes() []attribute.KeyValue
|
|
|
|
}
|
|
|
|
|
2024-05-16 16:19:36 +00:00
|
|
|
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...)...)
|
|
|
|
}
|
|
|
|
|
2024-05-16 11:45:13 +00:00
|
|
|
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")
|
2024-05-17 16:21:09 +00:00
|
|
|
ServiceLayerNameKey = attribute.Key("service.layer_name")
|
2024-05-20 15:29:00 +00:00
|
|
|
DBExecutionTimeMsKey = attribute.Key("db.execution_time_ms")
|
2024-07-24 13:40:18 +00:00
|
|
|
DBRowsAffectedKey = attribute.Key("db.rows_affected")
|
2024-05-16 11:45:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type ServiceArchitectureLayer string
|
|
|
|
|
|
|
|
const (
|
|
|
|
LayerFrameworkMiddleware ServiceArchitectureLayer = "framework_middleware"
|
|
|
|
LayerHandler = "handler"
|
|
|
|
LayerService = "service"
|
|
|
|
LayerRepository = "repository"
|
|
|
|
LayerORM = "orm"
|
|
|
|
LayerUtil = "util"
|
|
|
|
)
|
|
|
|
|
2024-09-13 13:48:16 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2024-05-16 11:45:13 +00:00
|
|
|
// 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))}
|
2024-09-13 13:48:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create an arbitrary attribute with value marshalled to json.
|
|
|
|
// In case of marshalling error, it is returned in place of value.
|
2024-09-25 14:11:08 +00:00
|
|
|
func Json(key string, val any) attribute.KeyValue {
|
2024-09-13 13:48:16 +00:00
|
|
|
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])
|
|
|
|
}
|
2024-05-16 11:45:13 +00:00
|
|
|
|
2024-09-13 13:48:16 +00:00
|
|
|
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(resBytes))}
|
2024-05-16 11:45:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)),
|
|
|
|
}
|
|
|
|
}
|
2024-05-17 16:21:09 +00:00
|
|
|
|
|
|
|
func ServiceLayerName(name string) attribute.KeyValue {
|
|
|
|
return attribute.KeyValue{
|
|
|
|
Key: ServiceLayerNameKey,
|
|
|
|
Value: attribute.StringValue(name),
|
|
|
|
}
|
|
|
|
}
|
2024-05-20 15:29:00 +00:00
|
|
|
|
|
|
|
// 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()),
|
|
|
|
}
|
|
|
|
}
|
2024-07-24 13:40:18 +00:00
|
|
|
|
|
|
|
func DBRowsAffected(rows int64) attribute.KeyValue {
|
|
|
|
return attribute.KeyValue{
|
|
|
|
Key: DBRowsAffectedKey,
|
|
|
|
Value: attribute.Int64Value(rows),
|
|
|
|
}
|
|
|
|
}
|