standardize commonly used attributes

Some commonly used at maal attributes have been encoded as consts with
convinience wrappers similar to those of semconv package from otel sdk.
Additionally some utils that can generate mutliple attributes were added.
This commit is contained in:
Natalia Goc 2024-05-16 13:45:13 +02:00
parent 6fb12e9e9d
commit ab5b70704d
8 changed files with 390 additions and 67 deletions

View File

@ -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} {

224
pkg/attr/attr.go Normal file
View File

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

View File

@ -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()
}

View File

@ -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"`

View File

@ -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)

View File

@ -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
}
}

84
pkg/syslog/syslog.go Normal file
View File

@ -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
}

View File

@ -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)