/
t3
/
cli
Обзор
Документация
Войти
/
t3
/
cli
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
internal/events/handler.go
210 строк
6 KB
Ivan
kilo fix controller log format
02 июл 2026, 20:07
02 июл 2026, 20:07
d80bd22
Код
Авторство
О чём код?
package events import ( "context" "fmt" "log/slog" "time" "gitverse.ru/t3/cli/pkg/pb" "google.golang.org/protobuf/proto" ) type GrpcHandler struct { level slog.Leveler sender LogSender groups []string // Active groups for FUTURE attributes tree []*pb.LogArgument // "Frozen" tree of pre-configured attributes hostname string // agent hostname for identification } func NewGrpcHandler(level slog.Leveler, sender LogSender, hostname string) *GrpcHandler { return &GrpcHandler{ level: level, sender: sender, tree: make([]*pb.LogArgument, 0), hostname: hostname, } } func (h *GrpcHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.level.Level() } // Handle formats the slog.Record into a LogEvent and sends it via the sender. func (h *GrpcHandler) Handle(ctx context.Context, r slog.Record) error { // Clone the pre-configured tree so we don't mutate shared state recordTree := cloneTree(h.tree) // Append record-specific attributes to the tree r.Attrs(func(a slog.Attr) bool { addToTree(&recordTree, h.groups, a) return true }) event := &pb.LogEvent{ Severity: mapSeverity(r.Level), Message: r.Message, Args: recordTree, Hostname: h.hostname, TimestampMs: time.Now().UnixMilli(), } return h.sender.SendLog(event) } func (h *GrpcHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } // Clone the tree and append new attributes at the current group depth newTree := cloneTree(h.tree) for _, a := range attrs { addToTree(&newTree, h.groups, a) } return &GrpcHandler{ level: h.level, sender: h.sender, groups: h.groups, tree: newTree, hostname: h.hostname, } } func (h *GrpcHandler) WithGroup(name string) slog.Handler { if name == "" { return h } // Push the new group onto the stack. Future attributes will be nested deeper. newGroups := make([]string, len(h.groups)+1) copy(newGroups, h.groups) newGroups[len(h.groups)] = name return &GrpcHandler{ level: h.level, sender: h.sender, groups: newGroups, tree: h.tree, hostname: h.hostname, } } // addToTree traverses or creates nested groups and appends the attribute to the correct slice. // This guarantees strict ordering based on when the attribute was added to the logger. func addToTree(tree *[]*pb.LogArgument, groups []string, a slog.Attr) { val := a.Value.Resolve() // Slog spec: empty keys are ignored unless they are a Group (which inlines its children) if a.Key == "" { if val.Kind() == slog.KindGroup { for _, ga := range val.Group() { addToTree(tree, groups, ga) } } return } // If the value itself is a group, inline its children with the group name added to the path if val.Kind() == slog.KindGroup { newGroups := append(append([]string{}, groups...), a.Key) for _, ga := range val.Group() { addToTree(tree, newGroups, ga) } return } // Traverse down the tree based on the active `groups` stack currentLevel := tree for _, g := range groups { var found *pb.LogArgument for _, arg := range *currentLevel { if arg.Key == g { found = arg break } } // If the group doesn't exist yet, create it if found == nil { newMap := &pb.LogMap{Items: make([]*pb.LogArgument, 0)} found = &pb.LogArgument{ Key: g, Value: &pb.LogValue{Value: &pb.LogValue_MapValue{MapValue: newMap}}, } *currentLevel = append(*currentLevel, found) } // Ensure the found node is actually a map (conflict resolution) mapVal, ok := found.Value.Value.(*pb.LogValue_MapValue) if !ok || mapVal.MapValue == nil { newMap := &pb.LogMap{Items: make([]*pb.LogArgument, 0)} found.Value = &pb.LogValue{Value: &pb.LogValue_MapValue{MapValue: newMap}} mapVal = &pb.LogValue_MapValue{MapValue: newMap} } // Move pointer down to the nested slice currentLevel = &mapVal.MapValue.Items } // Append the final typed value to the current level's slice pbVal := convertToLogValue(val) *currentLevel = append(*currentLevel, &pb.LogArgument{ Key: a.Key, Value: pbVal, }) } // cloneTree performs a deep copy of the protobuf arguments to prevent mutation across log records. func cloneTree(tree []*pb.LogArgument) []*pb.LogArgument { if len(tree) == 0 { return make([]*pb.LogArgument, 0) } newTree := make([]*pb.LogArgument, len(tree)) for i, arg := range tree { newTree[i] = proto.Clone(arg).(*pb.LogArgument) } return newTree } // convertToLogValue maps slog.Value types to the correct Protobuf oneof field. func convertToLogValue(v slog.Value) *pb.LogValue { v = v.Resolve() switch v.Kind() { case slog.KindString: return &pb.LogValue{Value: &pb.LogValue_StringValue{StringValue: v.String()}} case slog.KindInt64: return &pb.LogValue{Value: &pb.LogValue_IntValue{IntValue: v.Int64()}} case slog.KindUint64: u := v.Uint64() if u > uint64(1<<63-1) { return &pb.LogValue{Value: &pb.LogValue_DoubleValue{DoubleValue: float64(u)}} } return &pb.LogValue{Value: &pb.LogValue_IntValue{IntValue: int64(u)}} case slog.KindFloat64: return &pb.LogValue{Value: &pb.LogValue_DoubleValue{DoubleValue: v.Float64()}} case slog.KindBool: return &pb.LogValue{Value: &pb.LogValue_BoolValue{BoolValue: v.Bool()}} case slog.KindDuration: return &pb.LogValue{Value: &pb.LogValue_StringValue{StringValue: v.Duration().String()}} case slog.KindTime: return &pb.LogValue{Value: &pb.LogValue_StringValue{StringValue: v.Time().Format(time.RFC3339Nano)}} case slog.KindAny: return &pb.LogValue{Value: &pb.LogValue_StringValue{StringValue: fmt.Sprintf("%+v", v.Any())}} default: return &pb.LogValue{Value: &pb.LogValue_StringValue{StringValue: v.String()}} } } func mapSeverity(level slog.Level) string { switch { case level >= slog.LevelError: return "ERROR" case level >= slog.LevelWarn: return "WARNING" case level >= slog.LevelInfo: return "INFO" default: return "DEBUG" } }