this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cmd/well-known): dual logging colorized/JSON

+212 -14
+45 -14
cmd/well-known/main.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "errors" 6 - "log" 7 6 "log/slog" 8 7 "net/http" 9 8 "net/url" 10 9 "os" 11 10 "path/filepath" 12 11 "strings" 12 + 13 + "bsky.cat/internal/logger" 13 14 ) 14 15 15 - func wellKnownHandler(log *slog.Logger) http.HandlerFunc { 16 + var version string 17 + 18 + func wellKnownHandler(logger *slog.Logger) http.HandlerFunc { 16 19 root := os.Getenv("BSKYCAT_WELLKNOWN_ROOT") 17 20 return func(w http.ResponseWriter, r *http.Request) { 18 21 if r.Method != http.MethodGet { ··· 21 24 } 22 25 host := r.Host 23 26 if !strings.HasSuffix(host, ".bsky.cat") { 24 - log.Error("invalid host", slog.String("host", host)) 27 + logger.Error("invalid host", slog.String("host", host)) 25 28 w.WriteHeader(http.StatusNotFound) 26 29 return 27 30 } 28 31 hostData := strings.Split(host, ".") 29 32 if len(hostData) != 3 { 30 - log.Error("invalid host", slog.String("host", host)) 33 + logger.Error("invalid host", slog.String("host", host)) 31 34 w.WriteHeader(http.StatusNotFound) 32 35 return 33 36 } 34 37 did := filepath.Join(root, hostData[0]) 35 38 data, err := os.Open(did) 36 39 if err != nil { 37 - log.Error("failed to open file", slog.String("file", did), slog.String("error", err.Error())) 40 + logger.Error("failed to open file", slog.String("file", did), slog.String("error", err.Error())) 38 41 w.WriteHeader(http.StatusNotFound) 39 42 return 40 43 } 41 44 defer data.Close() 42 45 _, _ = data.WriteTo(w) 43 - log.Info("well-known request", slog.String("host", host)) 46 + logger.Info("well-known request", slog.String("host", host)) 44 47 } 45 48 } 46 49 ··· 82 85 return nil 83 86 } 84 87 88 + func loggerOptions(args []string) *logger.HandlerOptions { 89 + opts := &logger.HandlerOptions{} 90 + if len(args) >= 2 && args[1] == "server" { 91 + opts.Raw = true 92 + return opts 93 + } 94 + opts.ReplaceAttr = logger.SupressDefaults(opts.ReplaceAttr) 95 + return opts 96 + } 97 + 85 98 func main() { 86 - args := os.Args 99 + var ( 100 + args = os.Args 101 + loggerOpts = loggerOptions(args) 102 + logger = slog.New(logger.NewHandler(loggerOpts)) 103 + executable = filepath.Base(args[0]) 104 + ) 87 105 if len(args) < 2 { 88 - log.Fatalf("usage: %s <command>", args[0]) 106 + logger.Error("insufficient arguments", slog.String("usage", executable+" command")) 107 + os.Exit(1) 89 108 } 90 109 switch args[1] { 91 110 case "server": 92 - logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ 93 - Level: slog.LevelInfo, 94 - })) 111 + host := os.Getenv("BSKYCAT_PORT") 112 + if host == "" { 113 + host = "127.0.0.1:3002" 114 + } 115 + logger.Info("starting server", slog.String("host", host)) 95 116 http.Handle("/.well-known/atproto-did", wellKnownHandler(logger)) 96 - http.ListenAndServe("127.0.0.1:3001", nil) 117 + if err := http.ListenAndServe(host, nil); err != nil { 118 + logger.Error("server failed", slog.String("error", err.Error())) 119 + os.Exit(1) 120 + } 97 121 case "add-account": 98 122 if len(args) != 4 { 99 - log.Fatalf("usage: %s add-account <old> <new>", args[0]) 123 + logger.Error("insufficient arguments", slog.String("usage", executable+" add-account old new")) 124 + os.Exit(1) 100 125 } 101 126 old := args[2] 102 127 new := args[3] 128 + logger.Info("adding account", slog.String("old", old), slog.String("new", new)) 103 129 if err := addAccount(old, new); err != nil { 104 - log.Fatalf("failed to add account: %s", err) 130 + logger.Error("failed to add account", slog.String("error", err.Error())) 131 + os.Exit(1) 105 132 } 133 + logger.Info("account added successfully") 134 + default: 135 + logger.Error("unknown command", slog.String("command", args[1])) 136 + os.Exit(1) 106 137 } 107 138 }
+167
internal/logger/logger.go
··· 1 + package logger 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "io" 9 + "log/slog" 10 + "os" 11 + "strconv" 12 + "sync" 13 + ) 14 + 15 + const ( 16 + reset = "\033[0m" 17 + timeFormat = "[15:04:05.000]" 18 + ) 19 + 20 + const ( 21 + black = 30 + iota 22 + red 23 + green 24 + yellow 25 + blue 26 + magenta 27 + cyan 28 + lightGray 29 + ) 30 + 31 + const ( 32 + darkGray = 90 + iota 33 + lightRed 34 + lightGreen 35 + lightYellow 36 + lightBlue 37 + lightMagenta 38 + lightCyan 39 + white 40 + ) 41 + 42 + func colorize(colorCode int, v string) string { 43 + return fmt.Sprintf("\033[%sm%s%s", strconv.Itoa(colorCode), v, reset) 44 + } 45 + 46 + func SupressDefaults(next func([]string, slog.Attr) slog.Attr) func([]string, slog.Attr) slog.Attr { 47 + return func(groups []string, attr slog.Attr) slog.Attr { 48 + switch attr.Key { 49 + case slog.TimeKey: 50 + fallthrough 51 + case slog.LevelKey: 52 + fallthrough 53 + case slog.MessageKey: 54 + return slog.Attr{} 55 + } 56 + if next == nil { 57 + return attr 58 + } 59 + return next(groups, attr) 60 + } 61 + } 62 + 63 + type HandlerOptions struct { 64 + slog.HandlerOptions 65 + 66 + Raw bool 67 + } 68 + 69 + type Handler struct { 70 + *sync.Mutex 71 + 72 + raw bool 73 + inner slog.Handler 74 + buf *bytes.Buffer 75 + } 76 + 77 + func NewHandler(opts *HandlerOptions) *Handler { 78 + if opts == nil { 79 + opts = &HandlerOptions{} 80 + } 81 + var ( 82 + w io.Writer 83 + b *bytes.Buffer 84 + ) 85 + if opts.Raw { 86 + w = os.Stdout 87 + } else { 88 + b = &bytes.Buffer{} 89 + w = b 90 + } 91 + return &Handler{ 92 + Mutex: &sync.Mutex{}, 93 + raw: opts.Raw, 94 + buf: b, 95 + inner: slog.NewJSONHandler(w, &opts.HandlerOptions), 96 + } 97 + } 98 + 99 + func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { 100 + return h.inner.Enabled(ctx, level) 101 + } 102 + 103 + func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 104 + return &Handler{ 105 + Mutex: h.Mutex, 106 + raw: h.raw, 107 + inner: h.inner.WithAttrs(attrs), 108 + buf: h.buf, 109 + } 110 + } 111 + 112 + func (h *Handler) WithGroup(name string) slog.Handler { 113 + return &Handler{ 114 + Mutex: h.Mutex, 115 + raw: h.raw, 116 + inner: h.inner.WithGroup(name), 117 + buf: h.buf, 118 + } 119 + } 120 + 121 + func (h *Handler) Handle(ctx context.Context, r slog.Record) error { 122 + if h.raw { 123 + return h.inner.Handle(ctx, r) 124 + } 125 + level := r.Level.String() + ":" 126 + switch r.Level { 127 + case slog.LevelDebug: 128 + level = colorize(darkGray, level) 129 + case slog.LevelInfo: 130 + level = colorize(cyan, level) 131 + case slog.LevelWarn: 132 + level = colorize(lightYellow, level) 133 + case slog.LevelError: 134 + level = colorize(lightRed, level) 135 + } 136 + attrs, err := h.computeAttrs(ctx, r) 137 + if err != nil { 138 + return err 139 + } 140 + bytes, err := json.MarshalIndent(attrs, "", " ") 141 + if err != nil { 142 + return fmt.Errorf("error when marshaling attrs: %w", err) 143 + } 144 + fmt.Println( 145 + colorize(lightGray, r.Time.Format(timeFormat)), 146 + level, 147 + colorize(white, r.Message), 148 + colorize(darkGray, string(bytes)), 149 + ) 150 + return nil 151 + } 152 + 153 + func (h *Handler) computeAttrs(ctx context.Context, r slog.Record) (map[string]any, error) { 154 + h.Lock() 155 + defer func() { 156 + h.buf.Reset() 157 + h.Unlock() 158 + }() 159 + if err := h.inner.Handle(ctx, r); err != nil { 160 + return nil, fmt.Errorf("error when calling inner handler's Handle: %w", err) 161 + } 162 + var attrs map[string]any 163 + if err := json.Unmarshal(h.buf.Bytes(), &attrs); err != nil { 164 + return nil, fmt.Errorf("error when unmarshalling inner handler's Handle: %w", err) 165 + } 166 + return attrs, nil 167 + }