package starlog import ( "context" "fmt" "reflect" "sync/atomic" "b612.me/starlog/internal/routerx" ) type LevelMatcher = routerx.Matcher type Route struct { Name string Match LevelMatcher Formatter Formatter Sink Sink } type routeSnapshot struct { name string match LevelMatcher formatter Formatter sink Sink } type RouteHandler struct { routes atomic.Value } func NewRouteHandler(routes ...Route) *RouteHandler { handler := &RouteHandler{} handler.routes.Store([]routeSnapshot{}) handler.SetRoutes(routes) return handler } func sinkIdentity(sink Sink) (string, bool) { if sink == nil { return "", false } val := reflect.ValueOf(sink) if !val.IsValid() { return "", false } switch val.Kind() { case reflect.Ptr, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.UnsafePointer: if val.IsNil() { return "", false } return fmt.Sprintf("%T:%x", sink, val.Pointer()), true default: return "", false } } func (handler *RouteHandler) SetRoutes(routes []Route) { if handler == nil { return } handler.routes.Store(normalizeRoutes(routes)) } func (handler *RouteHandler) ReplaceRoutes(routes ...Route) { handler.SetRoutes(routes) } func (handler *RouteHandler) Handle(ctx context.Context, entry *Entry) error { if handler == nil || entry == nil { return nil } rawRoutes := handler.routes.Load() snapshots, ok := rawRoutes.([]routeSnapshot) if !ok || len(snapshots) == 0 { return nil } var firstErr error for _, route := range snapshots { if route.match != nil && !route.match(entry.Level) { continue } formatter := route.formatter if formatter == nil { formatter = NewTextFormatter() } formatted, err := formatter.Format(entry) if err != nil { wrapErr := fmt.Errorf("route %s format failed: %w", route.name, err) reportWriteError(wrapErr, LogData{ Name: route.name, Log: entry.Message, }) if firstErr == nil { firstErr = wrapErr } continue } if route.sink == nil { continue } if err = route.sink.Write(formatted); err != nil { wrapErr := fmt.Errorf("route %s write failed: %w", route.name, err) reportWriteError(wrapErr, LogData{ Name: route.name, Log: string(formatted), }) if firstErr == nil { firstErr = wrapErr } continue } } return firstErr } func (handler *RouteHandler) Close() error { if handler == nil { return nil } rawRoutes := handler.routes.Load() snapshots, ok := rawRoutes.([]routeSnapshot) if !ok || len(snapshots) == 0 { return nil } closed := make(map[string]struct{}, len(snapshots)) var firstErr error for _, route := range snapshots { if route.sink == nil { continue } if id, ok := sinkIdentity(route.sink); ok { if _, exists := closed[id]; exists { continue } closed[id] = struct{}{} } if err := route.sink.Close(); err != nil { wrapErr := fmt.Errorf("route %s close failed: %w", route.name, err) reportWriteError(wrapErr, LogData{ Name: route.name, }) if firstErr == nil { firstErr = wrapErr } } } return firstErr } func MatchAllLevels() LevelMatcher { return routerx.MatchAllLevels() } func MatchLevels(levels ...int) LevelMatcher { return routerx.MatchLevels(levels...) } func MatchAtLeast(minLevel int) LevelMatcher { return routerx.MatchAtLeast(minLevel) } type chainedHandler struct { handlers []Handler } func NewChainHandler(handlers ...Handler) Handler { filtered := make([]Handler, 0, len(handlers)) for _, handler := range handlers { if handler == nil { continue } filtered = append(filtered, handler) } return &chainedHandler{ handlers: filtered, } } func ChainHandler(handlers ...Handler) Handler { return NewChainHandler(handlers...) } func (handler *chainedHandler) Handle(ctx context.Context, entry *Entry) error { if handler == nil { return nil } var firstErr error for _, item := range handler.handlers { if item == nil { continue } if err := item.Handle(ctx, entry); err != nil && firstErr == nil { firstErr = err } } return firstErr } func (handler *chainedHandler) Close() error { if handler == nil { return nil } var firstErr error for _, item := range handler.handlers { if item == nil { continue } closer, ok := item.(interface{ Close() error }) if !ok { continue } if err := closer.Close(); err != nil && firstErr == nil { firstErr = err } } return firstErr } func normalizeRoutes(routes []Route) []routeSnapshot { if len(routes) == 0 { return nil } baseRoutes := make([]routerx.Route, 0, len(routes)) for index, route := range routes { baseRoutes = append(baseRoutes, routerx.Route{ Index: index, Name: route.Name, Match: route.Match, Enabled: route.Sink != nil, }) } normalized := routerx.Normalize(baseRoutes) result := make([]routeSnapshot, 0, len(normalized)) for _, item := range normalized { route := routes[item.Index] result = append(result, routeSnapshot{ name: item.Name, match: item.Match, formatter: route.Formatter, sink: route.Sink, }) } return result }