pax_global_header00006660000000000000000000000064146631577540014534gustar00rootroot0000000000000052 comment=7f417d6527408ca25f828dcd475e36a539bdd59b golang-github-charmbracelet-log-0.4.0/000077500000000000000000000000001466315775400176355ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/.github/000077500000000000000000000000001466315775400211755ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/.github/CODEOWNERS000066400000000000000000000000211466315775400225610ustar00rootroot00000000000000* @aymanbagabas golang-github-charmbracelet-log-0.4.0/.github/dependabot.yml000066400000000000000000000011171466315775400240250ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" labels: - "dependencies" commit-message: prefix: "feat" include: "scope" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" labels: - "dependencies" commit-message: prefix: "feat" include: "scope"golang-github-charmbracelet-log-0.4.0/.github/workflows/000077500000000000000000000000001466315775400232325ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/.github/workflows/build.yml000066400000000000000000000007071466315775400250600ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: strategy: matrix: go_version: ["1.19", stable] uses: charmbracelet/meta/.github/workflows/build.yml@main with: go_version: ${{ matrix.go_version }} snapshot: uses: charmbracelet/meta/.github/workflows/snapshot.yml@main secrets: goreleaser_key: ${{ secrets.GORELEASER_KEY }} coverage: uses: charmbracelet/meta/.github/workflows/coverage.yml@main golang-github-charmbracelet-log-0.4.0/.github/workflows/goreleaser.yml000066400000000000000000000012211466315775400261010ustar00rootroot00000000000000name: goreleaser on: push: tags: - v*.*.* concurrency: group: goreleaser cancel-in-progress: true jobs: goreleaser: uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main secrets: docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }} gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }} goreleaser_key: ${{ secrets.GORELEASER_KEY }} fury_token: ${{ secrets.FURY_TOKEN }} nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }} nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }} # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json golang-github-charmbracelet-log-0.4.0/.github/workflows/lint.yml000066400000000000000000000006241466315775400247250ustar00rootroot00000000000000name: lint on: push: pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ^1 - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: # Optional: golangci-lint command line arguments. args: --issues-exit-code=1 golang-github-charmbracelet-log-0.4.0/.github/workflows/nightly.yml000066400000000000000000000004651466315775400254400ustar00rootroot00000000000000name: nightly on: push: branches: - main jobs: nightly: uses: charmbracelet/meta/.github/workflows/nightly.yml@main secrets: docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }} goreleaser_key: ${{ secrets.GORELEASER_KEY }} golang-github-charmbracelet-log-0.4.0/.gitignore000066400000000000000000000004311466315775400216230ustar00rootroot00000000000000*.txt *.gif examples/batch2/batch2 examples/chocolate-chips/chocolate-chips examples/cookie/cookie examples/error/error examples/format/format examples/log/log examples/new/new examples/options/options examples/oven/oven examples/temperature/temperature .vscode .history go.work golang-github-charmbracelet-log-0.4.0/.golangci.yml000066400000000000000000000007151466315775400222240ustar00rootroot00000000000000run: tests: false issues: include: - EXC0001 - EXC0005 - EXC0011 - EXC0012 - EXC0013 max-issues-per-linter: 0 max-same-issues: 0 linters: enable: - bodyclose - dupl - exportloopref - goconst - godot - godox - goimports - goprintffuncname - gosec - misspell - nolintlint - prealloc - revive - rowserrcheck - sqlclosecheck - unconvert - unparam - whitespace golang-github-charmbracelet-log-0.4.0/.goreleaser.yml000066400000000000000000000001171466315775400225650ustar00rootroot00000000000000includes: - from_url: url: charmbracelet/meta/main/goreleaser-lib.yaml golang-github-charmbracelet-log-0.4.0/LICENSE000066400000000000000000000020701466315775400206410ustar00rootroot00000000000000MIT License Copyright (c) 2022-2023 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-charmbracelet-log-0.4.0/README.md000066400000000000000000000320541466315775400211200ustar00rootroot00000000000000# Log


Latest Release Go Docs Build Status Codecov branch Go Report Card

A minimal and colorful Go logging library. ๐Ÿชต Made with VHS It provides a leveled structured human readable logger with a small API. Unlike [standard `log`][stdlog], the Charm logger provides customizable colorful human readable logging with batteries included. - Uses [Lip Gloss][lipgloss] to style and colorize the output. - Colorful, human readable format. - Ability to customize the time stamp format. - Skips caller frames and marks function as helpers. - Leveled logging. - Text, JSON, and Logfmt formatters. - Store and retrieve logger in and from context. - Slog handler. - Standard log adapter. ## Usage Use `go get` to download the dependency. ```bash go get github.com/charmbracelet/log@latest ``` Then, `import` it in your Go files: ```go import "github.com/charmbracelet/log" ``` The Charm logger comes with a global package-wise logger with timestamps turned on, and the logging level set to `info`. ```go log.Debug("Cookie ๐Ÿช") // won't print anything log.Info("Hello World!") ``` Made with VHS All logging levels accept optional key/value pairs to be printed along with a message. ```go err := fmt.Errorf("too much sugar") log.Error("failed to bake cookies", "err", err) ``` Made with VHS You can use `log.Print()` to print messages without a level prefix. ```go log.Print("Baking 101") // 2023/01/04 10:04:06 Baking 101 ``` ### New loggers Use `New()` to create new loggers. ```go logger := log.New(os.Stderr) if butter { logger.Warn("chewy!", "butter", true) } ``` ### Levels Log offers multiple levels to filter your logs on. Available levels are: ```go log.DebugLevel log.InfoLevel log.WarnLevel log.ErrorLevel log.FatalLevel ``` Use `log.SetLevel()` to set the log level. You can also create a new logger with a specific log level using `log.Options{Level: }`. Use the corresponding function to log a message: ```go err := errors.New("Baking error 101") log.Debug(err) log.Info(err) log.Warn(err) log.Error(err) log.Fatal(err) // this calls os.Exit(1) log.Print(err) // prints regardless of log level ``` Or use the formatter variant: ```go format := "%s %d" log.Debugf(format, "chocolate", 10) log.Warnf(format, "adding more", 5) log.Errorf(format, "increasing temp", 420) log.Fatalf(format, "too hot!", 500) // this calls os.Exit(1) log.Printf(format, "baking cookies") // prints regardless of log level // Use these in conjunction with `With(...)` to add more context log.With("err", err).Errorf("unable to start %s", "oven") ``` ### Structured All the functions above take a message and key-value pairs of anything. The message can also be of type any. ```go ingredients := []string{"flour", "butter", "sugar", "chocolate"} log.Debug("Available ingredients", "ingredients", ingredients) // DEBUG Available ingredients ingredients="[flour butter sugar chocolate]" ``` ### Options You can customize the logger with options. Use `log.NewWithOptions()` and `log.Options{}` to customize your new logger. ```go logger := log.NewWithOptions(os.Stderr, log.Options{ ReportCaller: true, ReportTimestamp: true, TimeFormat: time.Kitchen, Prefix: "Baking ๐Ÿช ", }) logger.Info("Starting oven!", "degree", 375) time.Sleep(10 * time.Minute) logger.Info("Finished baking") ``` You can also use logger setters to customize the logger. ```go logger := log.New(os.Stderr) logger.SetReportTimestamp(false) logger.SetReportCaller(false) logger.SetLevel(log.DebugLevel) ``` Use `log.SetFormatter()` or `log.Options{Formatter: }` to change the output format. Available options are: - `log.TextFormatter` (_default_) - `log.JSONFormatter` - `log.LogfmtFormatter` > **Note** styling only affects the `TextFormatter`. Styling is disabled if the > output is not a TTY. For a list of available options, refer to [options.go](./options.go). ### Styles You can customize the logger styles using [Lipgloss][lipgloss]. The styles are defined at a global level in [styles.go](./styles.go). ```go // Override the default error level style. styles := log.DefaultStyles() styles.Levels[log.ErrorLevel] = lipgloss.NewStyle(). SetString("ERROR!!"). Padding(0, 1, 0, 1). Background(lipgloss.Color("204")). Foreground(lipgloss.Color("0")) // Add a custom style for key `err` styles.Keys["err"] = lipgloss.NewStyle().Foreground(lipgloss.Color("204")) styles.Values["err"] = lipgloss.NewStyle().Bold(true) logger := log.New(os.Stderr) logger.SetStyles(styles) logger.Error("Whoops!", "err", "kitchen on fire") ``` ### Sub-logger Create sub-loggers with their specific fields. ```go logger := log.NewWithOptions(os.Stderr, log.Options{ Prefix: "Baking ๐Ÿช " }) batch2 := logger.With("batch", 2, "chocolateChips", true) batch2.Debug("Preparing batch 2...") batch2.Debug("Adding chocolate chips") ``` ### Format Messages You can use `fmt.Sprintf()` to format messages. ```go for item := 1; i <= 100; i++ { log.Info(fmt.Sprintf("Baking %d/100...", item)) } ``` Or arguments: ```go for temp := 375; temp <= 400; temp++ { log.Info("Increasing temperature", "degree", fmt.Sprintf("%dยฐF", temp)) } ``` ### Helper Functions Skip caller frames in helper functions. Similar to what you can do with `testing.TB().Helper()`. ```go func startOven(degree int) { log.Helper() log.Info("Starting oven", "degree", degree) } log.SetReportCaller(true) startOven(400) // INFO Starting oven degree=400 ``` This will use the _caller_ function (`startOven`) line number instead of the logging function (`log.Info`) to report the source location. ### Slog Handler You can use Log as an [`log/slog`](https://pkg.go.dev/log/slog) handler. Just pass a logger instance to Slog and you're good to go. ```go handler := log.New(os.Stderr) logger := slog.New(handler) logger.Error("meow?") ``` ### Standard Log Adapter Some Go libraries, especially the ones in the standard library, will only accept the [standard logger][stdlog] interface. For instance, the HTTP Server from `net/http` will only take a `*log.Logger` for its `ErrorLog` field. For this, you can use the standard log adapter, which simply wraps the logger in a `*log.Logger` interface. ```go logger := log.NewWithOptions(os.Stderr, log.Options{Prefix: "http"}) stdlog := logger.StandardLog(log.StandardLogOptions{ ForceLevel: log.ErrorLevel, }) s := &http.Server{ Addr: ":8080", Handler: handler, ErrorLog: stdlog, } stdlog.Printf("Failed to make bake request, %s", fmt.Errorf("temperature is too low")) // ERROR http: Failed to make bake request, temperature is too low ``` ## Gum Running gum log with debug and error levels Log integrates with [Gum][gum] to log messages to output. Use `gum log [flags] [message]` to handle logging in your shell scripts. See [charmbracelet/gum](https://github.com/charmbracelet/gum#log) for more information. [gum]: https://github.com/charmbracelet/gum [lipgloss]: https://github.com/charmbracelet/lipgloss [stdlog]: https://pkg.go.dev/log ## License [MIT](https://github.com/charmbracelet/log/raw/master/LICENSE) --- Part of [Charm](https://charm.sh). the Charm logo Charm็ƒญ็ˆฑๅผ€ๆบ โ€ข Charm loves open source โ€ข ู†ุญู†ู ู†ุญุจ ุงู„ู…ุตุงุฏุฑ ุงู„ู…ูุชูˆุญุฉ golang-github-charmbracelet-log-0.4.0/context.go000066400000000000000000000011551466315775400216520ustar00rootroot00000000000000package log import "context" // WithContext wraps the given logger in context. func WithContext(ctx context.Context, logger *Logger) context.Context { return context.WithValue(ctx, ContextKey, logger) } // FromContext returns the logger from the given context. // This will return the default package logger if no logger // found in context. func FromContext(ctx context.Context) *Logger { if logger, ok := ctx.Value(ContextKey).(*Logger); ok { return logger } return Default() } type contextKey struct{ string } // ContextKey is the key used to store the logger in context. var ContextKey = contextKey{"log"} golang-github-charmbracelet-log-0.4.0/context_test.go000066400000000000000000000012041466315775400227040ustar00rootroot00000000000000package log import ( "bytes" "context" "io" "testing" "github.com/stretchr/testify/require" ) func TestLogContext_empty(t *testing.T) { require.Equal(t, Default(), FromContext(context.TODO())) } func TestLogContext_simple(t *testing.T) { l := New(io.Discard) ctx := WithContext(context.Background(), l) require.Equal(t, l, FromContext(ctx)) } func TestLogContext_fields(t *testing.T) { var buf bytes.Buffer l := New(&buf) l.SetLevel(DebugLevel) ctx := WithContext(context.Background(), l.With("foo", "bar")) l = FromContext(ctx) require.NotNil(t, l) l.Debug("test") require.Equal(t, "DEBU test foo=bar\n", buf.String()) } golang-github-charmbracelet-log-0.4.0/examples/000077500000000000000000000000001466315775400214535ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/README.md000066400000000000000000000004151466315775400227320ustar00rootroot00000000000000# Examples For those of you who are new to Go, check out [pkg.go.dev](https://https://pkg.go.dev/github.com/charmbracelet/log) to view our library's API. From there, you can search the repo for more detailed usage examples for whichever functions match your use case. golang-github-charmbracelet-log-0.4.0/examples/app/000077500000000000000000000000001466315775400222335ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/app/app.go000066400000000000000000000022361466315775400233450ustar00rootroot00000000000000package main import ( "fmt" "strings" "time" "github.com/charmbracelet/log" ) type cup int func (c cup) String() string { s := fmt.Sprintf("%d cup", c) if c > 1 { s += "s" } return s } func startOven(degree int) { log.Helper() log.Debug("Starting oven", "temperature", degree) } func main() { log.SetTimeFormat(time.Kitchen) log.SetLevel(log.DebugLevel) var ( butter = cup(1) chocolate = cup(2) flour = cup(3) sugar = cup(5) temp = 375 bakeTime = 10 ) startOven(temp) time.Sleep(time.Second) log.Debug("Mixing ingredients", "ingredients", strings.Join([]string{ butter.String() + " of butter", chocolate.String() + " of chocolate", flour.String() + " of flour", sugar.String() + " of sugar", }, "\n"), ) time.Sleep(time.Second) if sugar > 2 { log.Warn("That's a lot of sugar", "amount", sugar) } log.Info("Baking cookies", "time", fmt.Sprintf("%d minutes", bakeTime)) time.Sleep(2 * time.Second) log.Info("Increasing temperature", "amount", 300) temp += 300 time.Sleep(time.Second) if temp > 500 { log.Error("Oven is too hot", "temperature", temp) log.Fatal("The kitchen is on fire ๐Ÿ”ฅ") } } golang-github-charmbracelet-log-0.4.0/examples/app/app.tape000066400000000000000000000001351466315775400236650ustar00rootroot00000000000000Output app.gif Set FontSize 24 Set Width 1200 Set Height 600 Type "./app" Enter Sleep 10s golang-github-charmbracelet-log-0.4.0/examples/batch2/000077500000000000000000000000001466315775400226165ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/batch2/batch2.go000066400000000000000000000005361466315775400243140ustar00rootroot00000000000000package main import ( "github.com/charmbracelet/log" ) func main() { logger := log.Default().With("batch", 2, "chocolateChips", true) logger.SetPrefix("baking ๐Ÿช ") logger.SetReportTimestamp(false) logger.SetReportCaller(false) logger.SetLevel(log.DebugLevel) logger.Debug("Preparing batch 2...") logger.Debug("Adding chocolate chips") } golang-github-charmbracelet-log-0.4.0/examples/batch2/batch2.tape000066400000000000000000000001361466315775400246340ustar00rootroot00000000000000Output batch2.gif Set Height 300 Set Width 1100 Type "./batch2" Sleep 500ms Enter Sleep 4s golang-github-charmbracelet-log-0.4.0/examples/chocolate-chips/000077500000000000000000000000001466315775400245205ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/chocolate-chips/chocolate-chips.go000066400000000000000000000006451466315775400301210ustar00rootroot00000000000000package main import ( "github.com/charmbracelet/log" ) func main() { logger := log.Default().With() logger.SetPrefix("Baking ๐Ÿช ") logger.SetReportTimestamp(false) logger.SetReportCaller(false) logger.SetLevel(log.DebugLevel) logger.Debug("Preparing batch 2...") // DEBUG baking ๐Ÿช: Preparing batch 2...} batch2 := logger.With("batch", 2, "chocolateChips", true) batch2.Debug("Adding chocolate chips") } golang-github-charmbracelet-log-0.4.0/examples/cookie/000077500000000000000000000000001466315775400227245ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/cookie/cookie.go000066400000000000000000000001711466315775400245230ustar00rootroot00000000000000package main import "github.com/charmbracelet/log" func main() { log.Debug("Cookie ๐Ÿช") log.Info("Hello World!") } golang-github-charmbracelet-log-0.4.0/examples/cookie/cookie.tape000066400000000000000000000001351466315775400250470ustar00rootroot00000000000000Output cookie.gif Set Height 250 Set Width 700 Type "./cookie" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/error/000077500000000000000000000000001466315775400226045ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/error/error.go000066400000000000000000000002501466315775400242610ustar00rootroot00000000000000package main import ( "fmt" "github.com/charmbracelet/log" ) func main() { err := fmt.Errorf("too much sugar") log.Error("failed to bake cookies", "err", err) } golang-github-charmbracelet-log-0.4.0/examples/error/error.tape000066400000000000000000000001341466315775400246060ustar00rootroot00000000000000Output error.gif Set Height 250 Set Width 1100 Type "./error" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/format/000077500000000000000000000000001466315775400227435ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/format/format.go000066400000000000000000000003351466315775400245630ustar00rootroot00000000000000package main import ( "fmt" "time" "github.com/charmbracelet/log" ) func main() { for item := 1; item <= 100; item++ { log.Info(fmt.Sprintf("Baking %d / 100 ...", item)) time.Sleep(100 * time.Millisecond) } } golang-github-charmbracelet-log-0.4.0/examples/format/format.tape000066400000000000000000000001351466315775400251050ustar00rootroot00000000000000Output format.gif Set Height 400 Set Width 800 Type "./format" Sleep 500ms Enter Sleep 6s golang-github-charmbracelet-log-0.4.0/examples/go.mod000066400000000000000000000012741466315775400225650ustar00rootroot00000000000000module examples go 1.17 replace github.com/charmbracelet/log => ../ require ( github.com/charmbracelet/lipgloss v0.10.0 github.com/charmbracelet/log v0.0.0-00010101000000-000000000000 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sys v0.13.0 // indirect ) golang-github-charmbracelet-log-0.4.0/examples/go.sum000066400000000000000000000206721466315775400226150ustar00rootroot00000000000000github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-charmbracelet-log-0.4.0/examples/log/000077500000000000000000000000001466315775400222345ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/log/log.go000066400000000000000000000001361466315775400233440ustar00rootroot00000000000000package main import "github.com/charmbracelet/log" func main() { log.Print("Baking 101") } golang-github-charmbracelet-log-0.4.0/examples/log/log.tape000066400000000000000000000001271466315775400236700ustar00rootroot00000000000000Output log.gif Set Height 250 Set Width 800 Type "./log" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/new/000077500000000000000000000000001466315775400222445ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/new/new.go000066400000000000000000000002261466315775400233640ustar00rootroot00000000000000package main import ( "os" "github.com/charmbracelet/log" ) func main() { logger := log.New(os.Stderr) logger.Warn("chewy!", "butter", true) } golang-github-charmbracelet-log-0.4.0/examples/new/new.tape000066400000000000000000000001271466315775400237100ustar00rootroot00000000000000Output new.gif Set Height 250 Set Width 500 Type "./new" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/options/000077500000000000000000000000001466315775400231465ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/options/options.go000066400000000000000000000005471466315775400251760ustar00rootroot00000000000000package main import ( "os" "time" "github.com/charmbracelet/log" ) func main() { logger := log.New(os.Stderr) logger.SetPrefix("Baking ๐Ÿช ") logger.SetTimeFormat(time.Kitchen) logger.SetReportTimestamp(true) logger.SetReportCaller(true) logger.Info("Starting oven!", "degree", 375) time.Sleep(3 * time.Second) logger.Info("Finished baking") } golang-github-charmbracelet-log-0.4.0/examples/options/options.tape000066400000000000000000000001401466315775400255070ustar00rootroot00000000000000Output options.gif Set Height 300 Set Width 1200 Type "./options" Sleep 500ms Enter Sleep 6s golang-github-charmbracelet-log-0.4.0/examples/oven/000077500000000000000000000000001466315775400224225ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/oven/oven.go000066400000000000000000000003131466315775400237150ustar00rootroot00000000000000package main import "github.com/charmbracelet/log" func startOven(degree int) { log.Helper() log.Info("Starting oven", "degree", degree) } func main() { log.SetReportCaller(true) startOven(400) } golang-github-charmbracelet-log-0.4.0/examples/oven/oven.tape000066400000000000000000000001321466315775400242400ustar00rootroot00000000000000Output oven.gif Set Height 250 Set Width 1200 Type "./oven" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/slog/000077500000000000000000000000001466315775400224175ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/slog/main.go000066400000000000000000000007131466315775400236730ustar00rootroot00000000000000package main import ( "fmt" "log/slog" "os" "time" "github.com/charmbracelet/log" ) func main() { // baseline fmt.Println(time.Now().UTC().Format(time.RFC3339), "foo") fmt.Println(time.Now().Format(time.RFC3339), "bar") handler := log.NewWithOptions(os.Stdout, log.Options{ ReportTimestamp: true, TimeFunction: log.NowUTC, TimeFormat: time.RFC3339, }) handler.Info("foobar") logger := slog.New(handler) logger.Info("foobar") } golang-github-charmbracelet-log-0.4.0/examples/styles/000077500000000000000000000000001466315775400227765ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/styles/styles.go000066400000000000000000000011611466315775400246470ustar00rootroot00000000000000package main import ( "os" "time" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" ) func main() { // Set up our custom styles styles := log.DefaultStyles() styles.Levels[log.ErrorLevel] = lipgloss.NewStyle(). SetString("ERROR!!"). Padding(0, 1, 0, 1). Background(lipgloss.Color("204")). Foreground(lipgloss.Color("0")) styles.Keys["err"] = lipgloss.NewStyle().Foreground(lipgloss.Color("204")) styles.Values["err"] = lipgloss.NewStyle().Bold(true) logger := log.New(os.Stderr) logger.SetStyles(styles) logger.Error("Whoops!", "err", "kitchen on fire") time.Sleep(3 * time.Second) } golang-github-charmbracelet-log-0.4.0/examples/styles/styles.tape000066400000000000000000000001351466315775400251730ustar00rootroot00000000000000Output styles.gif Set Height 250 Set Width 700 Type "./styles" Sleep 500ms Enter Sleep 3s golang-github-charmbracelet-log-0.4.0/examples/temperature/000077500000000000000000000000001466315775400240105ustar00rootroot00000000000000golang-github-charmbracelet-log-0.4.0/examples/temperature/temperature.go000066400000000000000000000003651466315775400267000ustar00rootroot00000000000000package main import ( "fmt" "time" "github.com/charmbracelet/log" ) func main() { for temp := 375; temp <= 400; temp++ { log.Info("Increasing temperature", "degree", fmt.Sprintf("%dยฐF", temp)) time.Sleep(100 * time.Millisecond) } } golang-github-charmbracelet-log-0.4.0/examples/temperature/temperature.tape000066400000000000000000000001501466315775400272140ustar00rootroot00000000000000Output temperature.gif Set Height 400 Set Width 1100 Type "./temperature" Sleep 500ms Enter Sleep 6s golang-github-charmbracelet-log-0.4.0/formatter.go000066400000000000000000000013411466315775400221660ustar00rootroot00000000000000package log // Formatter is a formatter for log messages. type Formatter uint8 const ( // TextFormatter is a formatter that formats log messages as text. Suitable for // console output and log files. TextFormatter Formatter = iota // JSONFormatter is a formatter that formats log messages as JSON. JSONFormatter // LogfmtFormatter is a formatter that formats log messages as logfmt. LogfmtFormatter ) var ( // TimestampKey is the key for the timestamp. TimestampKey = "time" // MessageKey is the key for the message. MessageKey = "msg" // LevelKey is the key for the level. LevelKey = "level" // CallerKey is the key for the caller. CallerKey = "caller" // PrefixKey is the key for the prefix. PrefixKey = "prefix" ) golang-github-charmbracelet-log-0.4.0/go.mod000066400000000000000000000013501466315775400207420ustar00rootroot00000000000000module github.com/charmbracelet/log go 1.19 require ( github.com/charmbracelet/lipgloss v0.10.0 github.com/go-logfmt/logfmt v0.6.0 github.com/muesli/termenv v0.15.2 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-charmbracelet-log-0.4.0/go.sum000066400000000000000000000061441466315775400207750ustar00rootroot00000000000000github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-charmbracelet-log-0.4.0/json.go000066400000000000000000000022501466315775400211340ustar00rootroot00000000000000package log import ( "encoding/json" "fmt" "time" ) func (l *Logger) jsonFormatter(keyvals ...interface{}) { m := make(map[string]interface{}, len(keyvals)/2) for i := 0; i < len(keyvals); i += 2 { switch keyvals[i] { case TimestampKey: if t, ok := keyvals[i+1].(time.Time); ok { m[TimestampKey] = t.Format(l.timeFormat) } case LevelKey: if level, ok := keyvals[i+1].(Level); ok { m[LevelKey] = level.String() } case CallerKey: if caller, ok := keyvals[i+1].(string); ok { m[CallerKey] = caller } case PrefixKey: if prefix, ok := keyvals[i+1].(string); ok { m[PrefixKey] = prefix } case MessageKey: if msg := keyvals[i+1]; msg != nil { m[MessageKey] = fmt.Sprint(msg) } default: var ( key string val interface{} ) switch k := keyvals[i].(type) { case fmt.Stringer: key = k.String() case error: key = k.Error() default: key = fmt.Sprint(k) } switch v := keyvals[i+1].(type) { case error: val = v.Error() case fmt.Stringer: val = v.String() default: val = v } m[key] = val } } e := json.NewEncoder(&l.b) e.SetEscapeHTML(false) _ = e.Encode(m) } golang-github-charmbracelet-log-0.4.0/json_test.go000066400000000000000000000112351466315775400221760ustar00rootroot00000000000000package log import ( "bytes" "errors" "fmt" "path/filepath" "runtime" "testing" "github.com/stretchr/testify/require" ) func TestJson(t *testing.T) { var buf bytes.Buffer l := New(&buf) l.SetFormatter(JSONFormatter) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "default logger info with timestamp", expected: "{\"level\":\"info\",\"msg\":\"info\"}\n", msg: "info", kvs: nil, f: l.Info, }, { name: "default logger debug with timestamp", expected: "", msg: "info", kvs: nil, f: l.Debug, }, { name: "default logger error with timestamp", expected: "{\"level\":\"error\",\"msg\":\"info\"}\n", msg: "info", kvs: nil, f: l.Error, }, { name: "multiline message", expected: "{\"level\":\"error\",\"msg\":\"info\\ninfo\"}\n", msg: "info\ninfo", kvs: nil, f: l.Error, }, { name: "multiline kvs", expected: "{\"level\":\"error\",\"msg\":\"info\",\"multiline\":\"info\\ninfo\"}\n", msg: "info", kvs: []interface{}{"multiline", "info\ninfo"}, f: l.Error, }, { name: "odd number of kvs", expected: "{\"baz\":\"missing value\",\"foo\":\"bar\",\"level\":\"error\",\"msg\":\"info\"}\n", msg: "info", kvs: []interface{}{"foo", "bar", "baz"}, f: l.Error, }, { name: "error field", expected: "{\"error\":\"error message\",\"level\":\"error\",\"msg\":\"info\"}\n", msg: "info", kvs: []interface{}{"error", errors.New("error message")}, f: l.Error, }, { name: "struct field", expected: "{\"level\":\"info\",\"msg\":\"info\",\"struct\":{}}\n", msg: "info", kvs: []interface{}{"struct", struct{ foo string }{foo: "bar"}}, f: l.Info, }, { name: "slice field", expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[1,2,3]}\n", msg: "info", kvs: []interface{}{"slice", []int{1, 2, 3}}, f: l.Info, }, { name: "slice of structs", expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n", msg: "info", kvs: []interface{}{"slice", []struct{ foo string }{{foo: "bar"}, {foo: "baz"}}}, f: l.Info, }, { name: "slice of strings", expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[\"foo\",\"bar\"]}\n", msg: "info", kvs: []interface{}{"slice", []string{"foo", "bar"}}, f: l.Info, }, { name: "slice of errors", expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n", msg: "info", kvs: []interface{}{"slice", []error{errors.New("error message1"), errors.New("error message2")}}, f: l.Info, }, { name: "map of strings", expected: "{\"level\":\"info\",\"map\":{\"a\":\"b\",\"foo\":\"bar\"},\"msg\":\"info\"}\n", msg: "info", kvs: []interface{}{"map", map[string]string{"a": "b", "foo": "bar"}}, f: l.Info, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) require.Equal(t, c.expected, buf.String()) }) } } func TestJsonCaller(t *testing.T) { var buf bytes.Buffer l := New(&buf) l.SetFormatter(JSONFormatter) l.SetReportCaller(true) l.SetLevel(DebugLevel) _, file, line, _ := runtime.Caller(0) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "simple caller", expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"level\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30), msg: "info", kvs: nil, f: l.Info, }, { name: "nested caller", expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"level\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30), msg: "info", kvs: nil, f: func(msg interface{}, kvs ...interface{}) { l.Helper() l.Info(msg, kvs...) }, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) require.Equal(t, c.expected, buf.String()) }) } } func TestJsonCustomKey(t *testing.T) { var buf bytes.Buffer oldTsKey := TimestampKey defer func() { TimestampKey = oldTsKey }() TimestampKey = "time" logger := New(&buf) logger.SetTimeFunction(_zeroTime) logger.SetFormatter(JSONFormatter) logger.SetReportTimestamp(true) logger.Info("info") require.Equal(t, "{\"level\":\"info\",\"msg\":\"info\",\"time\":\"0002/01/01 00:00:00\"}\n", buf.String()) } golang-github-charmbracelet-log-0.4.0/level.go000066400000000000000000000025541466315775400213010ustar00rootroot00000000000000package log import ( "errors" "fmt" "math" "strings" ) // Level is a logging level. type Level int32 const ( // DebugLevel is the debug level. DebugLevel Level = -4 // InfoLevel is the info level. InfoLevel Level = 0 // WarnLevel is the warn level. WarnLevel Level = 4 // ErrorLevel is the error level. ErrorLevel Level = 8 // FatalLevel is the fatal level. FatalLevel Level = 12 // noLevel is used with log.Print. noLevel Level = math.MaxInt32 ) // String returns the string representation of the level. func (l Level) String() string { switch l { case DebugLevel: return "debug" case InfoLevel: return "info" case WarnLevel: return "warn" case ErrorLevel: return "error" case FatalLevel: return "fatal" default: return "" } } // ErrInvalidLevel is an error returned when parsing an invalid level string. var ErrInvalidLevel = errors.New("invalid level") // ParseLevel converts level in string to Level type. Default level is InfoLevel. func ParseLevel(level string) (Level, error) { switch strings.ToLower(level) { case DebugLevel.String(): return DebugLevel, nil case InfoLevel.String(): return InfoLevel, nil case WarnLevel.String(): return WarnLevel, nil case ErrorLevel.String(): return ErrorLevel, nil case FatalLevel.String(): return FatalLevel, nil default: return 0, fmt.Errorf("%w: %q", ErrInvalidLevel, level) } } golang-github-charmbracelet-log-0.4.0/level_121.go000066400000000000000000000004671466315775400216650ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 package log import "log/slog" // fromSlogLevel converts slog.Level to log.Level. var fromSlogLevel = map[slog.Level]Level{ slog.LevelDebug: DebugLevel, slog.LevelInfo: InfoLevel, slog.LevelWarn: WarnLevel, slog.LevelError: ErrorLevel, slog.Level(12): FatalLevel, } golang-github-charmbracelet-log-0.4.0/level_no121.go000066400000000000000000000005061466315775400222140ustar00rootroot00000000000000//go:build !go1.21 // +build !go1.21 package log import "golang.org/x/exp/slog" // fromSlogLevel converts slog.Level to log.Level. var fromSlogLevel = map[slog.Level]Level{ slog.LevelDebug: DebugLevel, slog.LevelInfo: InfoLevel, slog.LevelWarn: WarnLevel, slog.LevelError: ErrorLevel, slog.Level(12): FatalLevel, } golang-github-charmbracelet-log-0.4.0/level_test.go000066400000000000000000000024251466315775400223350ustar00rootroot00000000000000package log import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestDefaultLevel(t *testing.T) { var level Level assert.Equal(t, InfoLevel, level) } func TestParseLevel(t *testing.T) { testCases := []struct { name string input string expected Level error error }{ { name: "Parse debug", input: "debug", expected: DebugLevel, error: nil, }, { name: "Parse info", input: "Info", expected: InfoLevel, error: nil, }, { name: "Parse warn", input: "WARN", expected: WarnLevel, error: nil, }, { name: "Parse error", input: "error", expected: ErrorLevel, error: nil, }, { name: "Parse fatal", input: "FATAL", expected: FatalLevel, error: nil, }, { name: "Default", input: "", expected: InfoLevel, error: fmt.Errorf("%w: %q", ErrInvalidLevel, ""), }, { name: "Wrong level, set INFO", input: "WRONG_LEVEL", expected: InfoLevel, error: fmt.Errorf("%w: %q", ErrInvalidLevel, "WRONG_LEVEL"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { lvl, err := ParseLevel(tc.input) assert.Equal(t, tc.expected, lvl) assert.Equal(t, tc.error, err) }) } } golang-github-charmbracelet-log-0.4.0/logfmt.go000066400000000000000000000013351466315775400214560ustar00rootroot00000000000000package log import ( "errors" "fmt" "time" "github.com/go-logfmt/logfmt" ) func (l *Logger) logfmtFormatter(keyvals ...interface{}) { e := logfmt.NewEncoder(&l.b) for i := 0; i < len(keyvals); i += 2 { switch keyvals[i] { case TimestampKey: if t, ok := keyvals[i+1].(time.Time); ok { keyvals[i+1] = t.Format(l.timeFormat) } default: if key := fmt.Sprint(keyvals[i]); key != "" { keyvals[i] = key } } err := e.EncodeKeyval(keyvals[i], keyvals[i+1]) if err != nil && errors.Is(err, logfmt.ErrUnsupportedValueType) { // If the value is not supported by logfmt, we try to convert it to a string. _ = e.EncodeKeyval(keyvals[i], fmt.Sprintf("%+v", keyvals[i+1])) } } _ = e.EndRecord() } golang-github-charmbracelet-log-0.4.0/logfmt_test.go000066400000000000000000000062601466315775400225170ustar00rootroot00000000000000package log import ( "bytes" "errors" "testing" "github.com/stretchr/testify/assert" ) func TestLogfmt(t *testing.T) { var buf bytes.Buffer l := New(&buf) l.SetFormatter(LogfmtFormatter) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "simple", expected: "level=info msg=info\n", msg: "info", kvs: nil, f: l.Info, }, { name: "ignored message", expected: "", msg: "info", kvs: nil, f: l.Debug, }, { name: "message with keyvals", expected: "level=info msg=info foo=bar\n", msg: "info", kvs: []interface{}{"foo", "bar"}, f: l.Info, }, { name: "message with multiline keyvals", expected: "level=info msg=info foo=\"bar\\nbaz\"\n", msg: "info", kvs: []interface{}{"foo", "bar\nbaz"}, f: l.Info, }, { name: "multiline message", expected: "level=info msg=\"info\\nfoo\"\n", msg: "info\nfoo", kvs: nil, f: l.Info, }, { name: "message with error", expected: "level=info msg=info err=\"foo: bar\"\n", msg: "info", kvs: []interface{}{"err", errors.New("foo: bar")}, f: l.Info, }, { name: "odd number of keyvals", expected: "level=info msg=info foo=bar baz=\"missing value\"\n", msg: "info", kvs: []interface{}{"foo", "bar", "baz"}, f: l.Info, }, { name: "struct field", expected: "level=info msg=info foo=\"{bar:foo bar}\"\n", msg: "info", kvs: []interface{}{"foo", struct{ bar string }{"foo bar"}}, f: l.Info, }, { name: "multiple struct fields", expected: "level=info msg=info foo={bar:baz} foo2={bar:baz}\n", msg: "info", kvs: []interface{}{"foo", struct{ bar string }{"baz"}, "foo2", struct{ bar string }{"baz"}}, f: l.Info, }, { name: "slice of structs", expected: "level=info msg=info foo=\"[{bar:baz} {bar:baz}]\"\n", msg: "info", kvs: []interface{}{"foo", []struct{ bar string }{{"baz"}, {"baz"}}}, f: l.Info, }, { name: "slice of strings", expected: "level=info msg=info foo=\"[bar baz]\"\n", msg: "info", kvs: []interface{}{"foo", []string{"bar", "baz"}}, f: l.Info, }, { name: "slice of errors", expected: "level=info msg=info foo=\"[error1 error2]\"\n", msg: "info", kvs: []interface{}{"foo", []error{errors.New("error1"), errors.New("error2")}}, f: l.Info, }, { name: "map of strings", expected: "level=info msg=info foo=map[bar:baz]\n", msg: "info", kvs: []interface{}{"foo", map[string]string{"bar": "baz"}}, f: l.Info, }, { name: "map with interface", expected: "level=info msg=info foo=map[bar:baz]\n", msg: "info", kvs: []interface{}{"foo", map[string]interface{}{"bar": "baz"}}, f: l.Info, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } golang-github-charmbracelet-log-0.4.0/logger.go000066400000000000000000000227541466315775400214550ustar00rootroot00000000000000package log import ( "bytes" "fmt" "io" "os" "runtime" "strings" "sync" "sync/atomic" "time" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" ) // ErrMissingValue is returned when a key is missing a value. var ErrMissingValue = fmt.Errorf("missing value") // LoggerOption is an option for a logger. type LoggerOption = func(*Logger) // Logger is a Logger that implements Logger. type Logger struct { w io.Writer b bytes.Buffer mu *sync.RWMutex re *lipgloss.Renderer isDiscard uint32 level int32 prefix string timeFunc TimeFunction timeFormat string callerOffset int callerFormatter CallerFormatter formatter Formatter reportCaller bool reportTimestamp bool fields []interface{} helpers *sync.Map styles *Styles } // Logf logs a message with formatting. func (l *Logger) Logf(level Level, format string, args ...interface{}) { l.Log(level, fmt.Sprintf(format, args...)) } // Log logs the given message with the given keyvals for the given level. func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) { if atomic.LoadUint32(&l.isDiscard) != 0 { return } // check if the level is allowed if atomic.LoadInt32(&l.level) > int32(level) { return } var frame runtime.Frame if l.reportCaller { // Skip log.log, the caller, and any offset added. frames := l.frames(l.callerOffset + 2) for { f, more := frames.Next() _, helper := l.helpers.Load(f.Function) if !helper || !more { // Found a frame that wasn't a helper function. // Or we ran out of frames to check. frame = f break } } } l.handle(level, l.timeFunc(time.Now()), []runtime.Frame{frame}, msg, keyvals...) } func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) { var kvs []interface{} if l.reportTimestamp && !ts.IsZero() { kvs = append(kvs, TimestampKey, ts) } _, ok := l.styles.Levels[level] if ok { kvs = append(kvs, LevelKey, level) } if l.reportCaller && len(frames) > 0 && frames[0].PC != 0 { file, line, fn := l.location(frames) if file != "" { caller := l.callerFormatter(file, line, fn) kvs = append(kvs, CallerKey, caller) } } if l.prefix != "" { kvs = append(kvs, PrefixKey, l.prefix) } if msg != nil { if m := fmt.Sprint(msg); m != "" { kvs = append(kvs, MessageKey, m) } } // append logger fields kvs = append(kvs, l.fields...) if len(l.fields)%2 != 0 { kvs = append(kvs, ErrMissingValue) } // append the rest kvs = append(kvs, keyvals...) if len(keyvals)%2 != 0 { kvs = append(kvs, ErrMissingValue) } l.mu.Lock() defer l.mu.Unlock() switch l.formatter { case LogfmtFormatter: l.logfmtFormatter(kvs...) case JSONFormatter: l.jsonFormatter(kvs...) default: l.textFormatter(kvs...) } // WriteTo will reset the buffer l.b.WriteTo(l.w) //nolint: errcheck } // Helper marks the calling function as a helper // and skips it for source location information. // It's the equivalent of testing.TB.Helper(). func (l *Logger) Helper() { l.helper(1) } func (l *Logger) helper(skip int) { var pcs [1]uintptr // Skip runtime.Callers, and l.helper n := runtime.Callers(skip+2, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) frame, _ := frames.Next() l.helpers.LoadOrStore(frame.Function, struct{}{}) } // frames returns the runtime.Frames for the caller. func (l *Logger) frames(skip int) *runtime.Frames { // Copied from testing.T const maxStackLen = 50 var pc [maxStackLen]uintptr // Skip runtime.Callers, and l.frame n := runtime.Callers(skip+2, pc[:]) frames := runtime.CallersFrames(pc[:n]) return frames } func (l *Logger) location(frames []runtime.Frame) (file string, line int, fn string) { if len(frames) == 0 { return "", 0, "" } f := frames[0] return f.File, f.Line, f.Function } // Cleanup a path by returning the last n segments of the path only. func trimCallerPath(path string, n int) string { // lovely borrowed from zap // nb. To make sure we trim the path correctly on Windows too, we // counter-intuitively need to use '/' and *not* os.PathSeparator here, // because the path given originates from Go stdlib, specifically // runtime.Caller() which (as of Mar/17) returns forward slashes even on // Windows. // // See https://github.com/golang/go/issues/3335 // and https://github.com/golang/go/issues/18151 // // for discussion on the issue on Go side. // Return the full path if n is 0. if n <= 0 { return path } // Find the last separator. idx := strings.LastIndexByte(path, '/') if idx == -1 { return path } for i := 0; i < n-1; i++ { // Find the penultimate separator. idx = strings.LastIndexByte(path[:idx], '/') if idx == -1 { return path } } return path[idx+1:] } // SetReportTimestamp sets whether the timestamp should be reported. func (l *Logger) SetReportTimestamp(report bool) { l.mu.Lock() defer l.mu.Unlock() l.reportTimestamp = report } // SetReportCaller sets whether the caller location should be reported. func (l *Logger) SetReportCaller(report bool) { l.mu.Lock() defer l.mu.Unlock() l.reportCaller = report } // GetLevel returns the current level. func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return Level(l.level) } // SetLevel sets the current level. func (l *Logger) SetLevel(level Level) { l.mu.Lock() defer l.mu.Unlock() atomic.StoreInt32(&l.level, int32(level)) } // GetPrefix returns the current prefix. func (l *Logger) GetPrefix() string { l.mu.RLock() defer l.mu.RUnlock() return l.prefix } // SetPrefix sets the current prefix. func (l *Logger) SetPrefix(prefix string) { l.mu.Lock() defer l.mu.Unlock() l.prefix = prefix } // SetTimeFormat sets the time format. func (l *Logger) SetTimeFormat(format string) { l.mu.Lock() defer l.mu.Unlock() l.timeFormat = format } // SetTimeFunction sets the time function. func (l *Logger) SetTimeFunction(f TimeFunction) { l.mu.Lock() defer l.mu.Unlock() l.timeFunc = f } // SetOutput sets the output destination. func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() if w == nil { w = os.Stderr } l.w = w var isDiscard uint32 if w == io.Discard { isDiscard = 1 } atomic.StoreUint32(&l.isDiscard, isDiscard) // Reuse cached renderers if v, ok := registry.Load(w); ok { l.re = v.(*lipgloss.Renderer) } else { l.re = lipgloss.NewRenderer(w, termenv.WithColorCache(true)) registry.Store(w, l.re) } } // SetFormatter sets the formatter. func (l *Logger) SetFormatter(f Formatter) { l.mu.Lock() defer l.mu.Unlock() l.formatter = f } // SetCallerFormatter sets the caller formatter. func (l *Logger) SetCallerFormatter(f CallerFormatter) { l.mu.Lock() defer l.mu.Unlock() l.callerFormatter = f } // SetCallerOffset sets the caller offset. func (l *Logger) SetCallerOffset(offset int) { l.mu.Lock() defer l.mu.Unlock() l.callerOffset = offset } // SetColorProfile force sets the underlying Lip Gloss renderer color profile // for the TextFormatter. func (l *Logger) SetColorProfile(profile termenv.Profile) { l.re.SetColorProfile(profile) } // SetStyles sets the logger styles for the TextFormatter. func (l *Logger) SetStyles(s *Styles) { if s == nil { s = DefaultStyles() } l.mu.Lock() defer l.mu.Unlock() l.styles = s } // With returns a new logger with the given keyvals added. func (l *Logger) With(keyvals ...interface{}) *Logger { var st Styles l.mu.Lock() sl := *l st = *l.styles l.mu.Unlock() sl.b = bytes.Buffer{} sl.mu = &sync.RWMutex{} sl.helpers = &sync.Map{} sl.fields = append(l.fields, keyvals...) sl.styles = &st return &sl } // WithPrefix returns a new logger with the given prefix. func (l *Logger) WithPrefix(prefix string) *Logger { sl := l.With() sl.SetPrefix(prefix) return sl } // Debug prints a debug message. func (l *Logger) Debug(msg interface{}, keyvals ...interface{}) { l.Log(DebugLevel, msg, keyvals...) } // Info prints an info message. func (l *Logger) Info(msg interface{}, keyvals ...interface{}) { l.Log(InfoLevel, msg, keyvals...) } // Warn prints a warning message. func (l *Logger) Warn(msg interface{}, keyvals ...interface{}) { l.Log(WarnLevel, msg, keyvals...) } // Error prints an error message. func (l *Logger) Error(msg interface{}, keyvals ...interface{}) { l.Log(ErrorLevel, msg, keyvals...) } // Fatal prints a fatal message and exits. func (l *Logger) Fatal(msg interface{}, keyvals ...interface{}) { l.Log(FatalLevel, msg, keyvals...) os.Exit(1) } // Print prints a message with no level. func (l *Logger) Print(msg interface{}, keyvals ...interface{}) { l.Log(noLevel, msg, keyvals...) } // Debugf prints a debug message with formatting. func (l *Logger) Debugf(format string, args ...interface{}) { l.Log(DebugLevel, fmt.Sprintf(format, args...)) } // Infof prints an info message with formatting. func (l *Logger) Infof(format string, args ...interface{}) { l.Log(InfoLevel, fmt.Sprintf(format, args...)) } // Warnf prints a warning message with formatting. func (l *Logger) Warnf(format string, args ...interface{}) { l.Log(WarnLevel, fmt.Sprintf(format, args...)) } // Errorf prints an error message with formatting. func (l *Logger) Errorf(format string, args ...interface{}) { l.Log(ErrorLevel, fmt.Sprintf(format, args...)) } // Fatalf prints a fatal message with formatting and exits. func (l *Logger) Fatalf(format string, args ...interface{}) { l.Log(FatalLevel, fmt.Sprintf(format, args...)) os.Exit(1) } // Printf prints a message with no level and formatting. func (l *Logger) Printf(format string, args ...interface{}) { l.Log(noLevel, fmt.Sprintf(format, args...)) } golang-github-charmbracelet-log-0.4.0/logger_121.go000066400000000000000000000031531466315775400220300ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 package log import ( "context" "log/slog" "runtime" "sync/atomic" ) // Enabled reports whether the logger is enabled for the given level. // // Implements slog.Handler. func (l *Logger) Enabled(_ context.Context, level slog.Level) bool { return atomic.LoadInt32(&l.level) <= int32(fromSlogLevel[level]) } // Handle handles the Record. It will only be called if Enabled returns true. // // Implements slog.Handler. func (l *Logger) Handle(ctx context.Context, record slog.Record) error { if !l.Enabled(ctx, record.Level) { return nil } fields := make([]interface{}, 0, record.NumAttrs()*2) record.Attrs(func(a slog.Attr) bool { fields = append(fields, a.Key, a.Value.String()) return true }) // Get the caller frame using the record's PC. frames := runtime.CallersFrames([]uintptr{record.PC}) frame, _ := frames.Next() l.handle(fromSlogLevel[record.Level], l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) return nil } // WithAttrs returns a new Handler with the given attributes added. // // Implements slog.Handler. func (l *Logger) WithAttrs(attrs []slog.Attr) slog.Handler { fields := make([]interface{}, 0, len(attrs)*2) for _, attr := range attrs { fields = append(fields, attr.Key, attr.Value) } return l.With(fields...) } // WithGroup returns a new Handler with the given group name prepended to the // current group name or prefix. // // Implements slog.Handler. func (l *Logger) WithGroup(name string) slog.Handler { if l.prefix != "" { name = l.prefix + "." + name } return l.WithPrefix(name) } var _ slog.Handler = (*Logger)(nil) golang-github-charmbracelet-log-0.4.0/logger_121_test.go000066400000000000000000000055071466315775400230740ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 package log import ( "bytes" "testing" "log/slog" "github.com/stretchr/testify/assert" ) func TestSlogSimple(t *testing.T) { var buf bytes.Buffer h := New(&buf) h.SetLevel(DebugLevel) l := slog.New(h) cases := []struct { name string expected string msg string attrs []any print func(string, ...any) }{ { name: "slog debug", expected: "DEBU slog debug\n", msg: "slog debug", print: l.Debug, attrs: nil, }, { name: "slog info", expected: "INFO slog info\n", msg: "slog info", print: l.Info, attrs: nil, }, { name: "slog warn", expected: "WARN slog warn\n", msg: "slog warn", print: l.Warn, attrs: nil, }, { name: "slog error", expected: "ERRO slog error\n", msg: "slog error", print: l.Error, attrs: nil, }, { name: "slog error attrs", expected: "ERRO slog error foo=bar\n", msg: "slog error", print: l.Error, attrs: []any{"foo", "bar"}, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.print(c.msg, c.attrs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestSlogWith(t *testing.T) { var buf bytes.Buffer h := New(&buf) h.SetLevel(DebugLevel) l := slog.New(h).With("a", "b") cases := []struct { name string expected string msg string attrs []any print func(string, ...any) }{ { name: "slog debug", expected: "DEBU slog debug a=b foo=bar\n", msg: "slog debug", print: l.Debug, attrs: []any{"foo", "bar"}, }, { name: "slog info", expected: "INFO slog info a=b foo=bar\n", msg: "slog info", print: l.Info, attrs: []any{"foo", "bar"}, }, { name: "slog warn", expected: "WARN slog warn a=b foo=bar\n", msg: "slog warn", print: l.Warn, attrs: []any{"foo", "bar"}, }, { name: "slog error", expected: "ERRO slog error a=b foo=bar\n", msg: "slog error", print: l.Error, attrs: []any{"foo", "bar"}, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.print(c.msg, c.attrs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestSlogWithGroup(t *testing.T) { var buf bytes.Buffer h := New(&buf) l := slog.New(h).WithGroup("charm").WithGroup("bracelet") cases := []struct { name string expected string msg string }{ { name: "simple", msg: "message", expected: "INFO charm.bracelet: message\n", }, { name: "empty", msg: "", expected: "INFO charm.bracelet:\n", }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l.Info(c.msg) assert.Equal(t, c.expected, buf.String()) }) } } golang-github-charmbracelet-log-0.4.0/logger_no121.go000066400000000000000000000031041466315775400223610ustar00rootroot00000000000000//go:build !go1.21 // +build !go1.21 package log import ( "context" "runtime" "sync/atomic" "golang.org/x/exp/slog" ) // Enabled reports whether the logger is enabled for the given level. // // Implements slog.Handler. func (l *Logger) Enabled(_ context.Context, level slog.Level) bool { return atomic.LoadInt32(&l.level) <= int32(fromSlogLevel[level]) } // Handle handles the Record. It will only be called if Enabled returns true. // // Implements slog.Handler. func (l *Logger) Handle(_ context.Context, record slog.Record) error { fields := make([]interface{}, 0, record.NumAttrs()*2) record.Attrs(func(a slog.Attr) bool { fields = append(fields, a.Key, a.Value.String()) return true }) // Get the caller frame using the record's PC. frames := runtime.CallersFrames([]uintptr{record.PC}) frame, _ := frames.Next() l.handle(fromSlogLevel[record.Level], l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) return nil } // WithAttrs returns a new Handler with the given attributes added. // // Implements slog.Handler. func (l *Logger) WithAttrs(attrs []slog.Attr) slog.Handler { fields := make([]interface{}, 0, len(attrs)*2) for _, attr := range attrs { fields = append(fields, attr.Key, attr.Value) } return l.With(fields...) } // WithGroup returns a new Handler with the given group name prepended to the // current group name or prefix. // // Implements slog.Handler. func (l *Logger) WithGroup(name string) slog.Handler { if l.prefix != "" { name = l.prefix + "." + name } return l.WithPrefix(name) } var _ slog.Handler = (*Logger)(nil) golang-github-charmbracelet-log-0.4.0/logger_test.go000066400000000000000000000121771466315775400225120ustar00rootroot00000000000000package log import ( "bytes" "io" "sync" "testing" "time" "github.com/stretchr/testify/assert" ) func TestSubLogger(t *testing.T) { var buf bytes.Buffer l := New(&buf) cases := []struct { name string expected string msg string fields []interface{} kvs []interface{} }{ { name: "sub logger nil fields", expected: "INFO info\n", msg: "info", fields: nil, kvs: nil, }, { name: "sub logger info", expected: "INFO info foo=bar\n", msg: "info", fields: []interface{}{"foo", "bar"}, kvs: nil, }, { name: "sub logger info with kvs", expected: "INFO info foo=bar foobar=baz\n", msg: "info", fields: []interface{}{"foo", "bar"}, kvs: []interface{}{"foobar", "baz"}, }, { name: "emoji", expected: "INFO ๐Ÿ‘ ๐Ÿฑ\n", msg: "๐Ÿ‘ ๐Ÿฑ", fields: nil, kvs: nil, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l.With(c.fields...).Info(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestWrongLevel(t *testing.T) { var buf bytes.Buffer cases := []struct { name string expected string level Level }{ { name: "wrong level", expected: "", level: Level(999), }, { name: "wrong level negative", expected: "INFO info\n", level: Level(-999), }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() l := New(&buf) l.SetLevel(c.level) l.Info("info") assert.Equal(t, c.expected, buf.String()) }) } } func TestLogFormatter(t *testing.T) { var buf bytes.Buffer l := New(&buf) l.SetLevel(DebugLevel) cases := []struct { name string format string args []interface{} fun func(string, ...interface{}) expected string }{ { name: "info format", format: "%s %s", args: []interface{}{"foo", "bar"}, fun: l.Infof, expected: "INFO foo bar\n", }, { name: "debug format", format: "%s %s", args: []interface{}{"foo", "bar"}, fun: l.Debugf, expected: "DEBU foo bar\n", }, { name: "warn format", format: "%s %s", args: []interface{}{"foo", "bar"}, fun: l.Warnf, expected: "WARN foo bar\n", }, { name: "error format", format: "%s %s", args: []interface{}{"foo", "bar"}, fun: l.Errorf, expected: "ERRO foo bar\n", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() c.fun(c.format, "foo", "bar") assert.Equal(t, c.expected, buf.String()) }) } } func TestEmptyMessage(t *testing.T) { var buf bytes.Buffer l := New(&buf) cases := []struct { name string expected string msg string fields []interface{} kvs []interface{} }{ { name: "empty message nil fields", expected: "INFO\n", msg: "", fields: nil, kvs: nil, }, { name: "empty message with fields", expected: "INFO foo=bar\n", msg: "", fields: []interface{}{"foo", "bar"}, kvs: nil, }, { name: "empty message with fields & kvs", expected: "INFO foo=bar foobar=baz\n", msg: "", fields: []interface{}{"foo", "bar"}, kvs: []interface{}{"foobar", "baz"}, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l.With(c.fields...).Info(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestLogWithPrefix(t *testing.T) { var buf bytes.Buffer cases := []struct { name string expected string prefix string msg string }{ { name: "with prefix", expected: "INFO prefix: info\n", prefix: "prefix", msg: "info", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() l := New(&buf) l.SetPrefix(c.prefix) l.Info(c.msg) assert.Equal(t, c.expected, buf.String()) }) } } func TestLogWithRaceCondition(t *testing.T) { var buf bytes.Buffer cases := []struct { name string }{ { name: "must be run with -race", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() l := New(&buf) var done sync.WaitGroup done.Add(2) go func() { l.With("arg1", "val1", "arg2", "val2") done.Done() }() go func() { l.Info("kinda long log message") done.Done() }() done.Wait() }) } } func TestRace(t *testing.T) { t.Parallel() w := io.Discard l := New(w) for i := 0; i < 100; i++ { t.Run("race", func(t *testing.T) { t.Parallel() s := l.StandardLog() l.Info("foo") l.GetLevel() l.Print("foo") s.Print("foo") s.Writer().Write([]byte("bar")) s.Output(1, "baz") l.SetOutput(w) l.Debug("foo") l.SetLevel(InfoLevel) l.GetPrefix() o := l.With("foo", "bar") o.Printf("foo %s", "bar") o.SetTimeFormat(time.Kitchen) o.Warn("foo") o.SetOutput(w) o.Error("foo") o.SetFormatter(JSONFormatter) }) } } func TestCustomLevel(t *testing.T) { var buf bytes.Buffer level500 := Level(500) l := New(&buf) l.SetLevel(level500) l.Logf(level500, "foo") assert.Equal(t, "foo\n", buf.String()) } golang-github-charmbracelet-log-0.4.0/options.go000066400000000000000000000040551466315775400216630ustar00rootroot00000000000000package log import ( "fmt" "time" ) // DefaultTimeFormat is the default time format. const DefaultTimeFormat = "2006/01/02 15:04:05" // TimeFunction is a function that returns a time.Time. type TimeFunction = func(time.Time) time.Time // NowUTC is a convenient function that returns the // current time in UTC timezone. // // This is to be used as a time function. // For example: // // log.SetTimeFunction(log.NowUTC) func NowUTC(t time.Time) time.Time { return t.UTC() } // CallerFormatter is the caller formatter. type CallerFormatter func(string, int, string) string // ShortCallerFormatter is a caller formatter that returns the last 2 levels of the path // and line number. func ShortCallerFormatter(file string, line int, _ string) string { return fmt.Sprintf("%s:%d", trimCallerPath(file, 2), line) } // LongCallerFormatter is a caller formatter that returns the full path and line number. func LongCallerFormatter(file string, line int, _ string) string { return fmt.Sprintf("%s:%d", file, line) } // Options is the options for the logger. type Options struct { // TimeFunction is the time function for the logger. The default is time.Now. TimeFunction TimeFunction // TimeFormat is the time format for the logger. The default is "2006/01/02 15:04:05". TimeFormat string // Level is the level for the logger. The default is InfoLevel. Level Level // Prefix is the prefix for the logger. The default is no prefix. Prefix string // ReportTimestamp is whether the logger should report the timestamp. The default is false. ReportTimestamp bool // ReportCaller is whether the logger should report the caller location. The default is false. ReportCaller bool // CallerFormatter is the caller format for the logger. The default is ShortCallerFormatter. CallerFormatter CallerFormatter // CallerOffset is the caller format for the logger. The default is 0. CallerOffset int // Fields is the fields for the logger. The default is no fields. Fields []interface{} // Formatter is the formatter for the logger. The default is TextFormatter. Formatter Formatter } golang-github-charmbracelet-log-0.4.0/options_test.go000066400000000000000000000034701466315775400227220ustar00rootroot00000000000000package log import ( "bytes" "fmt" "io" "testing" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { opts := Options{ Level: ErrorLevel, ReportCaller: true, Fields: []interface{}{"foo", "bar"}, } logger := NewWithOptions(io.Discard, opts) require.Equal(t, ErrorLevel, logger.GetLevel()) require.True(t, logger.reportCaller) require.False(t, logger.reportTimestamp) require.Equal(t, []interface{}{"foo", "bar"}, logger.fields) require.Equal(t, TextFormatter, logger.formatter) require.Equal(t, DefaultTimeFormat, logger.timeFormat) require.NotNil(t, logger.timeFunc) } func TestCallerFormatter(t *testing.T) { var buf bytes.Buffer l := NewWithOptions(&buf, Options{ReportCaller: true}) frames := l.frames(0) frame, _ := frames.Next() file, line, fn := frame.File, frame.Line, frame.Function hi := func() { l.Info("hi") } cases := []struct { name string expected string format CallerFormatter }{ { name: "short caller formatter", expected: fmt.Sprintf("INFO hi\n", line+3), format: ShortCallerFormatter, }, { name: "long caller formatter", expected: fmt.Sprintf("INFO <%s:%d> hi\n", file, line+3), format: LongCallerFormatter, }, { name: "foo caller formatter", expected: "INFO hi\n", format: func(file string, line int, fn string) string { return "foo" }, }, { name: "custom caller formatter", expected: fmt.Sprintf("INFO <%s:%d:%s.func1> hi\n", file, line+3, fn), format: func(file string, line int, fn string) string { return fmt.Sprintf("%s:%d:%s", file, line, fn) }, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() l.callerFormatter = c.format hi() require.Equal(t, c.expected, buf.String()) }) } } golang-github-charmbracelet-log-0.4.0/pkg.go000066400000000000000000000143151466315775400207510ustar00rootroot00000000000000package log import ( "bytes" "fmt" "io" "log" "os" "sync" "time" "github.com/muesli/termenv" ) var ( // registry is a map of all registered lipgloss renderers. registry = sync.Map{} // defaultLogger is the default global logger instance. defaultLoggerOnce sync.Once defaultLogger *Logger ) // Default returns the default logger. The default logger comes with timestamp enabled. func Default() *Logger { defaultLoggerOnce.Do(func() { if defaultLogger != nil { // already set via SetDefault. return } defaultLogger = NewWithOptions(os.Stderr, Options{ReportTimestamp: true}) }) return defaultLogger } // SetDefault sets the default global logger. func SetDefault(logger *Logger) { defaultLogger = logger } // New returns a new logger with the default options. func New(w io.Writer) *Logger { return NewWithOptions(w, Options{}) } // NewWithOptions returns a new logger using the provided options. func NewWithOptions(w io.Writer, o Options) *Logger { l := &Logger{ b: bytes.Buffer{}, mu: &sync.RWMutex{}, helpers: &sync.Map{}, level: int32(o.Level), reportTimestamp: o.ReportTimestamp, reportCaller: o.ReportCaller, prefix: o.Prefix, timeFunc: o.TimeFunction, timeFormat: o.TimeFormat, formatter: o.Formatter, fields: o.Fields, callerFormatter: o.CallerFormatter, callerOffset: o.CallerOffset, } l.SetOutput(w) l.SetLevel(Level(l.level)) l.SetStyles(DefaultStyles()) if l.callerFormatter == nil { l.callerFormatter = ShortCallerFormatter } if l.timeFunc == nil { l.timeFunc = func(t time.Time) time.Time { return t } } if l.timeFormat == "" { l.timeFormat = DefaultTimeFormat } return l } // SetReportTimestamp sets whether to report timestamp for the default logger. func SetReportTimestamp(report bool) { Default().SetReportTimestamp(report) } // SetReportCaller sets whether to report caller location for the default logger. func SetReportCaller(report bool) { Default().SetReportCaller(report) } // SetLevel sets the level for the default logger. func SetLevel(level Level) { Default().SetLevel(level) } // GetLevel returns the level for the default logger. func GetLevel() Level { return Default().GetLevel() } // SetTimeFormat sets the time format for the default logger. func SetTimeFormat(format string) { Default().SetTimeFormat(format) } // SetTimeFunction sets the time function for the default logger. func SetTimeFunction(f TimeFunction) { Default().SetTimeFunction(f) } // SetOutput sets the output for the default logger. func SetOutput(w io.Writer) { Default().SetOutput(w) } // SetFormatter sets the formatter for the default logger. func SetFormatter(f Formatter) { Default().SetFormatter(f) } // SetCallerFormatter sets the caller formatter for the default logger. func SetCallerFormatter(f CallerFormatter) { Default().SetCallerFormatter(f) } // SetCallerOffset sets the caller offset for the default logger. func SetCallerOffset(offset int) { Default().SetCallerOffset(offset) } // SetPrefix sets the prefix for the default logger. func SetPrefix(prefix string) { Default().SetPrefix(prefix) } // SetColorProfile force sets the underlying Lip Gloss renderer color profile // for the TextFormatter. func SetColorProfile(profile termenv.Profile) { Default().SetColorProfile(profile) } // SetStyles sets the logger styles for the TextFormatter. func SetStyles(s *Styles) { Default().SetStyles(s) } // GetPrefix returns the prefix for the default logger. func GetPrefix() string { return Default().GetPrefix() } // With returns a new logger with the given keyvals. func With(keyvals ...interface{}) *Logger { return Default().With(keyvals...) } // WithPrefix returns a new logger with the given prefix. func WithPrefix(prefix string) *Logger { return Default().WithPrefix(prefix) } // Helper marks the calling function as a helper // and skips it for source location information. // It's the equivalent of testing.TB.Helper(). func Helper() { Default().helper(1) } // Log logs a message with the given level. func Log(level Level, msg interface{}, keyvals ...interface{}) { Default().Log(level, msg, keyvals...) } // Debug logs a debug message. func Debug(msg interface{}, keyvals ...interface{}) { Default().Log(DebugLevel, msg, keyvals...) } // Info logs an info message. func Info(msg interface{}, keyvals ...interface{}) { Default().Log(InfoLevel, msg, keyvals...) } // Warn logs a warning message. func Warn(msg interface{}, keyvals ...interface{}) { Default().Log(WarnLevel, msg, keyvals...) } // Error logs an error message. func Error(msg interface{}, keyvals ...interface{}) { Default().Log(ErrorLevel, msg, keyvals...) } // Fatal logs a fatal message and exit. func Fatal(msg interface{}, keyvals ...interface{}) { Default().Log(FatalLevel, msg, keyvals...) os.Exit(1) } // Print logs a message with no level. func Print(msg interface{}, keyvals ...interface{}) { Default().Log(noLevel, msg, keyvals...) } // Logf logs a message with formatting and level. func Logf(level Level, format string, args ...interface{}) { Default().Logf(level, format, args...) } // Debugf logs a debug message with formatting. func Debugf(format string, args ...interface{}) { Default().Log(DebugLevel, fmt.Sprintf(format, args...)) } // Infof logs an info message with formatting. func Infof(format string, args ...interface{}) { Default().Log(InfoLevel, fmt.Sprintf(format, args...)) } // Warnf logs a warning message with formatting. func Warnf(format string, args ...interface{}) { Default().Log(WarnLevel, fmt.Sprintf(format, args...)) } // Errorf logs an error message with formatting. func Errorf(format string, args ...interface{}) { Default().Log(ErrorLevel, fmt.Sprintf(format, args...)) } // Fatalf logs a fatal message with formatting and exit. func Fatalf(format string, args ...interface{}) { Default().Log(FatalLevel, fmt.Sprintf(format, args...)) os.Exit(1) } // Printf logs a message with formatting and no level. func Printf(format string, args ...interface{}) { Default().Log(noLevel, fmt.Sprintf(format, args...)) } // StandardLog returns a standard logger from the default logger. func StandardLog(opts ...StandardLogOptions) *log.Logger { return Default().StandardLog(opts...) } golang-github-charmbracelet-log-0.4.0/pkg_test.go000066400000000000000000000121121466315775400220010ustar00rootroot00000000000000package log import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "runtime" "testing" "time" "github.com/muesli/termenv" "github.com/stretchr/testify/assert" ) func TestGlobal(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetTimeFunction(_zeroTime) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "default logger info with timestamp", expected: "0002/01/01 00:00:00 INFO info\n", msg: "info", kvs: nil, f: Info, }, { name: "default logger debug with timestamp", expected: "", msg: "info", kvs: nil, f: Debug, }, { name: "default logger error with timestamp", expected: "0002/01/01 00:00:00 ERRO info\n", msg: "info", kvs: nil, f: Error, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestPrint(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(FatalLevel) SetTimeFunction(_zeroTime) SetReportTimestamp(true) SetReportCaller(false) SetTimeFormat(DefaultTimeFormat) SetColorProfile(termenv.ANSI) Error("error") Print("print") assert.Equal(t, "0002/01/01 00:00:00 print\n", buf.String()) } func TestPrintf(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(FatalLevel) SetTimeFunction(_zeroTime) SetReportTimestamp(true) SetReportCaller(false) SetTimeFormat(DefaultTimeFormat) Errorf("error") Printf("print") assert.Equal(t, "0002/01/01 00:00:00 print\n", buf.String()) } func TestFatal(t *testing.T) { SetReportTimestamp(true) SetReportCaller(false) SetTimeFormat(DefaultTimeFormat) if os.Getenv("FATAL") == "1" { Fatal("i'm dead") return } cmd := exec.Command(os.Args[0], "-test.run=TestFatal") cmd.Env = append(os.Environ(), "FATAL=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Fatalf("process ran with err %v, want exit status 1", err) } func TestFatalf(t *testing.T) { SetReportTimestamp(true) SetReportCaller(false) SetTimeFormat(DefaultTimeFormat) if os.Getenv("FATAL") == "1" { Fatalf("i'm %s", "dead") return } cmd := exec.Command(os.Args[0], "-test.run=TestFatalf") cmd.Env = append(os.Environ(), "FATAL=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Fatalf("process ran with err %v, want exit status 1", err) } func TestDebugf(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(DebugLevel) SetTimeFunction(_zeroTime) SetReportTimestamp(true) SetReportCaller(true) SetTimeFormat(DefaultTimeFormat) _, file, line, _ := runtime.Caller(0) Debugf("debug %s", "foo") assert.Equal(t, fmt.Sprintf("0002/01/01 00:00:00 DEBU debug foo\n", filepath.Base(file), line+1), buf.String()) } func TestInfof(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(InfoLevel) SetReportTimestamp(false) SetReportCaller(false) SetTimeFormat(DefaultTimeFormat) Infof("info %s", "foo") assert.Equal(t, "INFO info foo\n", buf.String()) } func TestWarnf(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(WarnLevel) SetReportCaller(false) SetReportTimestamp(true) SetTimeFunction(_zeroTime) SetTimeFormat(DefaultTimeFormat) Warnf("warn %s", "foo") assert.Equal(t, "0002/01/01 00:00:00 WARN warn foo\n", buf.String()) } func TestErrorf(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(ErrorLevel) SetReportCaller(false) SetReportTimestamp(true) SetTimeFunction(_zeroTime) SetTimeFormat(time.Kitchen) Errorf("error %s", "foo") assert.Equal(t, "12:00AM ERRO error foo\n", buf.String()) } func TestWith(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(InfoLevel) SetReportCaller(false) SetReportTimestamp(true) SetTimeFunction(_zeroTime) SetTimeFormat(DefaultTimeFormat) With("foo", "bar").Info("info") assert.Equal(t, "0002/01/01 00:00:00 INFO info foo=bar\n", buf.String()) } func TestGetLevel(t *testing.T) { SetLevel(InfoLevel) assert.Equal(t, InfoLevel, GetLevel()) } func TestPrefix(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(WarnLevel) SetReportCaller(false) SetReportTimestamp(false) SetPrefix("prefix") Warn("info") assert.Equal(t, "WARN prefix: info\n", buf.String()) assert.Equal(t, "prefix", GetPrefix()) SetPrefix("") } func TestFormatter(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) SetLevel(InfoLevel) SetReportCaller(false) SetReportTimestamp(false) SetFormatter(JSONFormatter) Info("info") assert.Equal(t, "{\"level\":\"info\",\"msg\":\"info\"}\n", buf.String()) } func TestWithPrefix(t *testing.T) { l := WithPrefix("test") assert.Equal(t, "test", l.prefix) } func TestGlobalCustomLevel(t *testing.T) { var buf bytes.Buffer lvl := Level(-1) SetOutput(&buf) SetLevel(lvl) SetReportCaller(false) SetReportTimestamp(false) SetFormatter(JSONFormatter) Log(lvl, "info") Logf(lvl, "hey %s", "you") assert.Equal(t, "{\"msg\":\"info\"}\n{\"msg\":\"hey you\"}\n", buf.String()) } golang-github-charmbracelet-log-0.4.0/stdlog.go000066400000000000000000000027321466315775400214640ustar00rootroot00000000000000package log import ( "log" "strings" ) type stdLogWriter struct { l *Logger opt *StandardLogOptions } func (l *stdLogWriter) Write(p []byte) (n int, err error) { str := strings.TrimSuffix(string(p), "\n") if l.opt != nil { switch l.opt.ForceLevel { case DebugLevel: l.l.Debug(str) case InfoLevel: l.l.Info(str) case WarnLevel: l.l.Warn(str) case ErrorLevel: l.l.Error(str) } } else { switch { case strings.HasPrefix(str, "DEBUG"): l.l.Debug(strings.TrimSpace(str[5:])) case strings.HasPrefix(str, "INFO"): l.l.Info(strings.TrimSpace(str[4:])) case strings.HasPrefix(str, "WARN"): l.l.Warn(strings.TrimSpace(str[4:])) case strings.HasPrefix(str, "ERROR"): l.l.Error(strings.TrimSpace(str[5:])) case strings.HasPrefix(str, "ERR"): l.l.Error(strings.TrimSpace(str[3:])) default: l.l.Info(str) } } return len(p), nil } // StandardLogOptions can be used to configure the standard log adapter. type StandardLogOptions struct { ForceLevel Level } // StandardLog returns a standard logger from Logger. The returned logger // can infer log levels from message prefix. Expected prefixes are DEBUG, INFO, // WARN, ERROR, and ERR. func (l *Logger) StandardLog(opts ...StandardLogOptions) *log.Logger { nl := l.With() // The caller stack is // log.Printf() -> l.Output() -> l.out.Write(stdLogger.Write) nl.callerOffset += 3 sl := &stdLogWriter{ l: nl, } if len(opts) > 0 { sl.opt = &opts[0] } return log.New(sl, "", 0) } golang-github-charmbracelet-log-0.4.0/stdlog_test.go000066400000000000000000000045341466315775400225250ustar00rootroot00000000000000package log import ( "bytes" "fmt" "log" "path/filepath" "runtime" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStdLog(t *testing.T) { var buf bytes.Buffer l := New(&buf) cases := []struct { f func(l *log.Logger) name string expected string }{ { name: "simple", expected: "INFO info\n", f: func(l *log.Logger) { l.Print("info") }, }, { name: "without level", expected: "INFO coffee\n", f: func(l *log.Logger) { l.Print("coffee") }, }, { name: "error level", expected: "ERRO coffee\n", f: func(l *log.Logger) { l.Print("ERROR coffee") }, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l.SetOutput(&buf) l.SetTimeFunction(_zeroTime) c.f(l.StandardLog()) assert.Equal(t, c.expected, buf.String()) }) } } func TestStdLog_forceLevel(t *testing.T) { var buf bytes.Buffer logger := New(&buf) cases := []struct { name string expected string level Level }{ { name: "debug", expected: "", level: DebugLevel, }, { name: "info", expected: "INFO coffee\n", level: InfoLevel, }, { name: "error", expected: "ERRO coffee\n", level: ErrorLevel, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l := logger.StandardLog(StandardLogOptions{ForceLevel: c.level}) l.Print("coffee") assert.Equal(t, c.expected, buf.String()) }) } } func TestStdLog_writer(t *testing.T) { var buf bytes.Buffer logger := New(&buf) logger.SetReportCaller(true) _, file, line, ok := runtime.Caller(0) require.True(t, ok) cases := []struct { name string expected string level Level }{ { name: "debug", expected: "", level: DebugLevel, }, { name: "info", expected: fmt.Sprintf("INFO coffee\n", filepath.Base(file), line+27), level: InfoLevel, }, { name: "error", expected: fmt.Sprintf("ERRO coffee\n", filepath.Base(file), line+27), level: ErrorLevel, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { l := log.New(logger.StandardLog(StandardLogOptions{ForceLevel: c.level}).Writer(), "", 0) l.Print("coffee") assert.Equal(t, c.expected, buf.String()) }) } } golang-github-charmbracelet-log-0.4.0/styles.go000066400000000000000000000041371466315775400215140ustar00rootroot00000000000000package log import ( "strings" "github.com/charmbracelet/lipgloss" ) // Styles defines the styles for the text logger. type Styles struct { // Timestamp is the style for timestamps. Timestamp lipgloss.Style // Caller is the style for source caller. Caller lipgloss.Style // Prefix is the style for prefix. Prefix lipgloss.Style // Message is the style for messages. Message lipgloss.Style // Key is the style for keys. Key lipgloss.Style // Value is the style for values. Value lipgloss.Style // Separator is the style for separators. Separator lipgloss.Style // Levels are the styles for each level. Levels map[Level]lipgloss.Style // Keys overrides styles for specific keys. Keys map[string]lipgloss.Style // Values overrides value styles for specific keys. Values map[string]lipgloss.Style } // DefaultStyles returns the default styles. func DefaultStyles() *Styles { return &Styles{ Timestamp: lipgloss.NewStyle(), Caller: lipgloss.NewStyle().Faint(true), Prefix: lipgloss.NewStyle().Bold(true).Faint(true), Message: lipgloss.NewStyle(), Key: lipgloss.NewStyle().Faint(true), Value: lipgloss.NewStyle(), Separator: lipgloss.NewStyle().Faint(true), Levels: map[Level]lipgloss.Style{ DebugLevel: lipgloss.NewStyle(). SetString(strings.ToUpper(DebugLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("63")), InfoLevel: lipgloss.NewStyle(). SetString(strings.ToUpper(InfoLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("86")), WarnLevel: lipgloss.NewStyle(). SetString(strings.ToUpper(WarnLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("192")), ErrorLevel: lipgloss.NewStyle(). SetString(strings.ToUpper(ErrorLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("204")), FatalLevel: lipgloss.NewStyle(). SetString(strings.ToUpper(FatalLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("134")), }, Keys: map[string]lipgloss.Style{}, Values: map[string]lipgloss.Style{}, } } golang-github-charmbracelet-log-0.4.0/text.go000066400000000000000000000134561466315775400211610ustar00rootroot00000000000000package log import ( "fmt" "io" "strings" "sync" "time" "unicode" "unicode/utf8" ) const ( separator = "=" indentSeparator = " โ”‚ " ) func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) { st := l.styles // kindly borrowed from hclog for { nl := strings.IndexByte(str, '\n') if nl == -1 { if str != "" { _, _ = w.Write([]byte(indent)) val := escapeStringForOutput(str, false) if valueStyle, ok := st.Values[key]; ok { val = valueStyle.Renderer(l.re).Render(val) } else { val = st.Value.Renderer(l.re).Render(val) } _, _ = w.Write([]byte(val)) if newline { _, _ = w.Write([]byte{'\n'}) } } return } _, _ = w.Write([]byte(indent)) val := escapeStringForOutput(str[:nl], false) val = st.Value.Renderer(l.re).Render(val) _, _ = w.Write([]byte(val)) _, _ = w.Write([]byte{'\n'}) str = str[nl+1:] } } func needsEscaping(str string) bool { for _, b := range str { if !unicode.IsPrint(b) || b == '"' { return true } } return false } const ( lowerhex = "0123456789abcdef" ) var bufPool = sync.Pool{ New: func() interface{} { return new(strings.Builder) }, } func escapeStringForOutput(str string, escapeQuotes bool) string { // kindly borrowed from hclog if !needsEscaping(str) { return str } bb := bufPool.Get().(*strings.Builder) bb.Reset() defer bufPool.Put(bb) for _, r := range str { if escapeQuotes && r == '"' { bb.WriteString(`\"`) } else if unicode.IsPrint(r) { bb.WriteRune(r) } else { switch r { case '\a': bb.WriteString(`\a`) case '\b': bb.WriteString(`\b`) case '\f': bb.WriteString(`\f`) case '\n': bb.WriteString(`\n`) case '\r': bb.WriteString(`\r`) case '\t': bb.WriteString(`\t`) case '\v': bb.WriteString(`\v`) default: switch { case r < ' ': bb.WriteString(`\x`) bb.WriteByte(lowerhex[byte(r)>>4]) bb.WriteByte(lowerhex[byte(r)&0xF]) case !utf8.ValidRune(r): r = 0xFFFD fallthrough case r < 0x10000: bb.WriteString(`\u`) for s := 12; s >= 0; s -= 4 { bb.WriteByte(lowerhex[r>>uint(s)&0xF]) } default: bb.WriteString(`\U`) for s := 28; s >= 0; s -= 4 { bb.WriteByte(lowerhex[r>>uint(s)&0xF]) } } } } } return bb.String() } func needsQuoting(s string) bool { for i := 0; i < len(s); { b := s[i] if b < utf8.RuneSelf { if needsQuotingSet[b] { return true } i++ continue } r, size := utf8.DecodeRuneInString(s[i:]) if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { return true } i += size } return false } var needsQuotingSet = [utf8.RuneSelf]bool{ '"': true, '=': true, } func init() { for i := 0; i < utf8.RuneSelf; i++ { r := rune(i) if unicode.IsSpace(r) || !unicode.IsPrint(r) { needsQuotingSet[i] = true } } } func writeSpace(w io.Writer, first bool) { if !first { w.Write([]byte{' '}) //nolint: errcheck } } func (l *Logger) textFormatter(keyvals ...interface{}) { st := l.styles lenKeyvals := len(keyvals) for i := 0; i < lenKeyvals; i += 2 { firstKey := i == 0 moreKeys := i < lenKeyvals-2 switch keyvals[i] { case TimestampKey: if t, ok := keyvals[i+1].(time.Time); ok { ts := t.Format(l.timeFormat) ts = st.Timestamp.Renderer(l.re).Render(ts) writeSpace(&l.b, firstKey) l.b.WriteString(ts) } case LevelKey: if level, ok := keyvals[i+1].(Level); ok { var lvl string lvlStyle, ok := st.Levels[level] if !ok { continue } lvl = lvlStyle.Renderer(l.re).String() if lvl != "" { writeSpace(&l.b, firstKey) l.b.WriteString(lvl) } } case CallerKey: if caller, ok := keyvals[i+1].(string); ok { caller = fmt.Sprintf("<%s>", caller) caller = st.Caller.Renderer(l.re).Render(caller) writeSpace(&l.b, firstKey) l.b.WriteString(caller) } case PrefixKey: if prefix, ok := keyvals[i+1].(string); ok { prefix = st.Prefix.Renderer(l.re).Render(prefix + ":") writeSpace(&l.b, firstKey) l.b.WriteString(prefix) } case MessageKey: if msg := keyvals[i+1]; msg != nil { m := fmt.Sprint(msg) m = st.Message.Renderer(l.re).Render(m) writeSpace(&l.b, firstKey) l.b.WriteString(m) } default: sep := separator indentSep := indentSeparator sep = st.Separator.Renderer(l.re).Render(sep) indentSep = st.Separator.Renderer(l.re).Render(indentSep) key := fmt.Sprint(keyvals[i]) val := fmt.Sprintf("%+v", keyvals[i+1]) raw := val == "" if raw { val = `""` } if key == "" { continue } actualKey := key valueStyle := st.Value if vs, ok := st.Values[actualKey]; ok { valueStyle = vs } if keyStyle, ok := st.Keys[key]; ok { key = keyStyle.Renderer(l.re).Render(key) } else { key = st.Key.Renderer(l.re).Render(key) } // Values may contain multiple lines, and that format // is preserved, with each line prefixed with a " | " // to show it's part of a collection of lines. // // Values may also need quoting, if not all the runes // in the value string are "normal", like if they // contain ANSI escape sequences. if strings.Contains(val, "\n") { l.b.WriteString("\n ") l.b.WriteString(key) l.b.WriteString(sep + "\n") l.writeIndent(&l.b, val, indentSep, moreKeys, actualKey) } else if !raw && needsQuoting(val) { writeSpace(&l.b, firstKey) l.b.WriteString(key) l.b.WriteString(sep) l.b.WriteString(valueStyle.Renderer(l.re).Render(fmt.Sprintf(`"%s"`, escapeStringForOutput(val, true)))) } else { val = valueStyle.Renderer(l.re).Render(val) writeSpace(&l.b, firstKey) l.b.WriteString(key) l.b.WriteString(sep) l.b.WriteString(val) } } } // Add a newline to the end of the log message. l.b.WriteByte('\n') } golang-github-charmbracelet-log-0.4.0/text_test.go000066400000000000000000000266131466315775400222170ustar00rootroot00000000000000package log import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "testing" "time" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func _zeroTime(time.Time) time.Time { return time.Time{}.AddDate(1, 0, 0) } func TestNilStyles(t *testing.T) { st := DefaultStyles() l := New(io.Discard) l.SetStyles(nil) assert.Equal(t, st, l.styles) } func TestTextCaller(t *testing.T) { var buf bytes.Buffer logger := New(&buf) logger.SetReportCaller(true) // We calculate the caller offset based on the caller line number. _, file, line, _ := runtime.Caller(0) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "simple caller", expected: fmt.Sprintf("INFO info\n", filepath.Base(file), line+14), msg: "info", kvs: nil, f: func(msg interface{}, kvs ...interface{}) { logger.Info(msg, kvs...) }, }, { name: "helper caller", expected: fmt.Sprintf("INFO info\n", filepath.Base(file), line+58), msg: "info", kvs: nil, f: func(msg interface{}, kvs ...interface{}) { logger.Helper() logger.Info(msg, kvs...) }, }, { name: "nested helper caller", expected: fmt.Sprintf("INFO info\n", filepath.Base(file), line+37), msg: "info", kvs: nil, f: func(msg interface{}, kvs ...interface{}) { fun := func(msg interface{}, kvs ...interface{}) { logger.Helper() logger.Info(msg, kvs...) } fun(msg, kvs...) }, }, { name: "double nested helper caller", expected: fmt.Sprintf("INFO info\n", filepath.Base(file), line+58), msg: "info", kvs: nil, f: func(msg interface{}, kvs ...interface{}) { logger.Helper() fun := func(msg interface{}, kvs ...interface{}) { logger.Helper() logger.Info(msg, kvs...) } fun(msg, kvs...) }, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestTextLogger(t *testing.T) { var buf bytes.Buffer logger := New(&buf) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "simple message", expected: "INFO info\n", msg: "info", kvs: nil, f: logger.Info, }, { name: "ignored message", expected: "", msg: "this is a debug message", kvs: nil, f: logger.Debug, }, { name: "message with keyvals", expected: "INFO info key1=val1 key2=val2\n", msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, f: logger.Info, }, { name: "error message with keyvals", expected: "ERRO info key1=val1 key2=val2\n", msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, f: logger.Error, }, { name: "error message with multiline", expected: "ERRO info\n key1=\n โ”‚ val1\n โ”‚ val2\n", msg: "info", kvs: []interface{}{"key1", "val1\nval2"}, f: logger.Error, }, { name: "odd number of keyvals", expected: "ERRO info key1=val1 key2=val2 key3=\"missing value\"\n", msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2", "key3"}, f: logger.Error, }, { name: "error field", expected: "ERRO info key1=\"error value\"\n", msg: "info", kvs: []interface{}{"key1", errors.New("error value")}, f: logger.Error, }, { name: "struct field", expected: "ERRO info key1={foo:bar}\n", msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar"}}, f: logger.Error, }, { name: "struct field quoted", expected: "ERRO info key1=\"{foo:bar baz}\"\n", msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar baz"}}, f: logger.Error, }, { name: "slice of strings", expected: "ERRO info key1=\"[foo bar]\"\n", msg: "info", kvs: []interface{}{"key1", []string{"foo", "bar"}}, f: logger.Error, }, { name: "slice of structs", expected: "ERRO info key1=\"[{foo:bar} {foo:baz}]\"\n", msg: "info", kvs: []interface{}{"key1", []struct{ foo string }{{foo: "bar"}, {foo: "baz"}}}, f: logger.Error, }, { name: "slice of errors", expected: "ERRO info key1=\"[error value1 error value2]\"\n", msg: "info", kvs: []interface{}{"key1", []error{errors.New("error value1"), errors.New("error value2")}}, f: logger.Error, }, { name: "map of strings", expected: "ERRO info key1=\"map[baz:qux foo:bar]\"\n", msg: "info", kvs: []interface{}{"key1", map[string]string{"foo": "bar", "baz": "qux"}}, f: logger.Error, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestTextHelper(t *testing.T) { var buf bytes.Buffer logger := New(&buf) logger.SetReportCaller(true) helper := func() { logger.Helper() logger.Info("helper func") } helper() _, file, line, ok := runtime.Caller(0) require.True(t, ok) assert.Equal(t, fmt.Sprintf("INFO helper func\n", filepath.Base(file), line-1), buf.String()) } func TestTextFatal(t *testing.T) { var buf bytes.Buffer logger := New(&buf) logger.SetReportCaller(true) if os.Getenv("FATAL") == "1" { logger.Fatal("i'm dead") logger.Fatalf("bye %s", "bye") return } cmd := exec.Command(os.Args[0], "-test.run=TestTextFatal") cmd.Env = append(os.Environ(), "FATAL=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Fatalf("process ran with err %v, want exit status 1", err) } func TestTextValueStyles(t *testing.T) { var buf bytes.Buffer logger := New(&buf) logger.SetColorProfile(termenv.ANSI256) lipgloss.SetColorProfile(termenv.ANSI256) st := DefaultStyles() st.Value = lipgloss.NewStyle().Bold(true) st.Values["key3"] = st.Value.Copy().Underline(true) logger.SetStyles(st) cases := []struct { name string expected string msg string kvs []interface{} f func(msg interface{}, kvs ...interface{}) }{ { name: "simple message", expected: fmt.Sprintf("%s info\n", st.Levels[InfoLevel]), msg: "info", kvs: nil, f: logger.Info, }, { name: "ignored message", expected: "", msg: "this is a debug message", kvs: nil, f: logger.Debug, }, { name: "message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", st.Levels[InfoLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, f: logger.Info, }, { name: "error message with multiline", expected: fmt.Sprintf( "%s info\n %s%s\n%s%s\n%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Separator.Render(indentSeparator), st.Value.Render("val1"), st.Separator.Render(indentSeparator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1\nval2"}, f: logger.Error, }, { name: "error message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, f: logger.Error, }, { name: "odd number of keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), st.Key.Render("key3"), st.Separator.Render(separator), st.Values["key3"].Render(`"missing value"`), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2", "key3"}, f: logger.Error, }, { name: "error field", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"error value"`), ), msg: "info", kvs: []interface{}{"key1", errors.New("error value")}, f: logger.Error, }, { name: "struct field", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[InfoLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("{foo:bar}"), ), msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar"}}, f: logger.Info, }, { name: "struct field quoted", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[InfoLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"{foo:bar baz}"`), ), msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar baz"}}, f: logger.Info, }, { name: "slice of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[foo bar]"`), ), msg: "info", kvs: []interface{}{"key1", []string{"foo", "bar"}}, f: logger.Error, }, { name: "slice of structs", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[{foo:bar} {foo:baz}]"`), ), msg: "info", kvs: []interface{}{"key1", []struct{ foo string }{{foo: "bar"}, {foo: "baz"}}}, f: logger.Error, }, { name: "slice of errors", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[error value1 error value2]"`), ), msg: "info", kvs: []interface{}{"key1", []error{errors.New("error value1"), errors.New("error value2")}}, f: logger.Error, }, { name: "map of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", st.Levels[ErrorLevel], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"map[baz:qux foo:bar]"`), ), msg: "info", kvs: []interface{}{"key1", map[string]string{"foo": "bar", "baz": "qux"}}, f: logger.Error, }, } for _, c := range cases { buf.Reset() t.Run(c.name, func(t *testing.T) { c.f(c.msg, c.kvs...) assert.Equal(t, c.expected, buf.String()) }) } } func TestColorProfile(t *testing.T) { cases := []termenv.Profile{ termenv.Ascii, termenv.ANSI, termenv.ANSI256, termenv.TrueColor, } l := New(io.Discard) for _, p := range cases { l.SetColorProfile(p) assert.Equal(t, p, l.re.ColorProfile()) } } func TestCustomLevelStyle(t *testing.T) { var buf bytes.Buffer l := New(&buf) st := DefaultStyles() lvl := Level(1234) st.Levels[lvl] = lipgloss.NewStyle().Bold(true).SetString("FUNKY") l.SetStyles(st) l.SetLevel(lvl) l.Log(lvl, "foobar") assert.Equal(t, "FUNKY foobar\n", buf.String()) }