Skip to content

Improved support for visualizing logs #1195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/auth0_logs_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ auth0 logs list [flags]
-f, --filter string Filter in Lucene query syntax. See https://auth0.com/docs/logs/log-search-query-syntax for more details.
--json Output in json format.
-n, --number int Number of log entries to show. Minimum 1, maximum 1000. (default 100)
-p, --picker Allows to toggle from list of logs and view a selected log in detail
```


Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/auth0/go-auth0 v1.20.0
github.com/briandowns/spinner v1.23.2
github.com/charmbracelet/glamour v0.10.0
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/fsnotify/fsnotify v1.9.0
github.com/getsentry/sentry-go v0.32.0
github.com/golang/mock v1.6.0
Expand All @@ -24,7 +25,9 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lestrrat-go/jwx v1.2.30
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-tty v0.0.7
github.com/mholt/archiver/v3 v3.5.1
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down Expand Up @@ -178,6 +184,8 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
Expand All @@ -189,6 +197,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
Expand Down Expand Up @@ -295,6 +305,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
38 changes: 36 additions & 2 deletions internal/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"sort"
"time"

"github.com/auth0/auth0-cli/internal/auth0"

"github.com/auth0/go-auth0/management"
"github.com/spf13/cobra"
"golang.org/x/net/context"
Expand All @@ -29,6 +31,13 @@ var (
ShortForm: "n",
Help: "Number of log entries to show. Minimum 1, maximum 1000.",
}

enableLogPicker = Flag{
Name: "Interactive picker option on rendered logs",
LongForm: "picker",
ShortForm: "p",
Help: "Allows to toggle from list of logs and view a selected log in detail",
}
)

func logsCmd(cli *cli) *cobra.Command {
Expand All @@ -50,6 +59,7 @@ func listLogsCmd(cli *cli) *cobra.Command {
var inputs struct {
Filter string
Num int
Picker bool
}

cmd := &cobra.Command{
Expand All @@ -72,19 +82,43 @@ func listLogsCmd(cli *cli) *cobra.Command {
if inputs.Num < 1 || inputs.Num > 1000 {
return fmt.Errorf("number flag invalid, please pass a number between 1 and 1000")
}
list, err := getLatestLogs(cmd.Context(), cli, inputs.Num, inputs.Filter)
logs, err := getLatestLogs(cmd.Context(), cli, inputs.Num, inputs.Filter)
if err != nil {
return fmt.Errorf("failed to list logs: %w", err)
}

hasFilter := inputs.Filter != ""
cli.renderer.LogList(list, !cli.debug, hasFilter)
if !inputs.Picker {
cli.renderer.LogList(logs, !cli.debug, hasFilter)
} else {
var (
selectedLogID string
currentIndex = auth0.Int(0)
)
for {
selectedLogID = cli.renderer.LogPrompt(logs, hasFilter, currentIndex)

logDetail, err := cli.api.Log.Read(cmd.Context(), selectedLogID)
if err != nil {
fmt.Println("Failed to fetch details:", err)
continue
}

fmt.Println("\nDetailed Log:")
cli.renderer.JSONResult(logDetail)

if cli.renderer.QuitPrompt() {
break
}
}
}
return nil
},
}

logsFilter.RegisterString(cmd, &inputs.Filter, "")
logsNum.RegisterInt(cmd, &inputs.Num, defaultPageSize)
enableLogPicker.RegisterBool(cmd, &inputs.Picker, false)

cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.")
cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.")
Expand Down
141 changes: 127 additions & 14 deletions internal/display/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"fmt"
"strings"

"github.com/auth0/go-auth0/management"

"github.com/auth0/auth0-cli/internal/ansi"
"github.com/auth0/auth0-cli/internal/auth0"

"github.com/auth0/go-auth0/management"
"github.com/chzyer/readline"
"github.com/manifoldco/promptui"
"github.com/mattn/go-tty"
"gopkg.in/yaml.v2"
)

Expand All @@ -18,23 +22,28 @@ const (
logCategoryWarning
logCategoryFailure
logCategoryUnknown
colWidthType = 20
colWidthDesc = 40
colWidthDate = 25
colWidthConnection = 20
colWidthClient = 30
)

type logCategory int

var _ View = &logView{}
var _ View = &LogView{}

type logView struct {
type LogView struct {
silent bool
*management.Log
raw interface{}
}

func (v *logView) AsTableHeader() []string {
func (v *LogView) AsTableHeader() []string {
return []string{"Type", "Description", "Date", "Connection", "Client"}
}

func (v *logView) getConnection() string {
func (v *LogView) getConnection() string {
if v.Details["prompts"] == nil {
return notApplicable
}
Expand All @@ -54,7 +63,31 @@ func (v *logView) getConnection() string {
return notApplicable
}

func (v *logView) AsTableRow() []string {
func (v *LogView) AsTableRowString() string {
row := v.AsTableRow()
return fmt.Sprintf(
"%-*s %-*s %-*s %-*s %-*s",
colWidthType, row[0],
colWidthDesc, row[1],
colWidthDate, row[2],
colWidthConnection, row[3],
colWidthClient, row[4],
)
}

func (v *LogView) AsTableHeaderString() string {
row := v.AsTableHeader()
return fmt.Sprintf(
" "+"\033[4m%-*s %-*s %-*s %-*s %-*s\033[0m",
colWidthType+3, row[0],
colWidthDesc+14, row[1],
colWidthDate, row[2],
colWidthConnection, row[3],
colWidthClient, row[4],
)
}

func (v *LogView) AsTableRow() []string {
typ, desc := v.typeDesc()

clientName := v.GetClientName()
Expand All @@ -78,11 +111,11 @@ func (v *logView) AsTableRow() []string {
}
}

func (v *logView) Object() interface{} {
func (v *LogView) Object() interface{} {
return v.raw
}

func (v *logView) Extras() []string {
func (v *LogView) Extras() []string {
if v.silent {
return nil
}
Expand All @@ -97,9 +130,9 @@ func (v *logView) Extras() []string {
return []string{ansi.Faint(indent(string(raw), "\t"))}
}

func (v *logView) category() logCategory {
func (v *LogView) category() logCategory {
switch logType := v.GetType(); {
case strings.HasPrefix(logType, "s"):
case strings.HasPrefix(logType, "s") || strings.HasPrefix(logType, "m"):
return logCategorySuccess
case strings.HasPrefix(logType, "w"):
return logCategoryWarning
Expand All @@ -110,7 +143,7 @@ func (v *logView) category() logCategory {
}
}

func (v *logView) typeDesc() (typ, desc string) {
func (v *LogView) typeDesc() (typ, desc string) {
chunks := strings.Split(v.TypeName(), "(")

// NOTE(cyx): Some logs don't have a typ at all -- for those we'll
Expand Down Expand Up @@ -142,6 +175,55 @@ func (v *logView) typeDesc() (typ, desc string) {
return typ, desc
}

func (r *Renderer) LogPrompt(logs []*management.Log, hasFilter bool, currentIndex *int) string {
resource := "logs"

r.Heading(resource)
if len(logs) == 0 {
if hasFilter {
if r.Format == OutputFormatJSON {
r.JSONResult([]interface{}{})
return ""
}
r.Warnf("No logs available matching filter criteria.\n")
} else {
r.EmptyState(resource, "To generate logs, run a test command like 'auth0 test login' or 'auth0 test token'")
}

return ""
}

view := LogView{Log: logs[0]}
label := view.AsTableHeaderString()
var rows []string

// Recursively append each log from logs list.
for _, l := range logs {
view := LogView{Log: l}
rows = append(rows, view.AsTableRowString())
}

promptui.IconInitial = promptui.Styler()("")
prompt := promptui.Select{
Label: label,
Items: rows,
Size: 10,
HideHelp: true,
Stdout: &noBellStdout{},
Templates: &promptui.SelectTemplates{
Label: "{{ . }}",
},
}
var err error
*currentIndex, _, err = prompt.RunCursorAt(*currentIndex, *currentIndex)
if err != nil {
r.Errorf("failed to select a log: %w", err)
}

// Return the ID of the select log.
return logs[*currentIndex].GetLogID()
}

func (r *Renderer) LogList(logs []*management.Log, silent, hasFilter bool) {
resource := "logs"

Expand All @@ -163,7 +245,7 @@ func (r *Renderer) LogList(logs []*management.Log, silent, hasFilter bool) {

var res []View
for _, l := range logs {
res = append(res, &logView{Log: l, silent: silent, raw: l})
res = append(res, &LogView{Log: l, silent: silent, raw: l})
}

r.Results(res)
Expand All @@ -174,7 +256,7 @@ func (r *Renderer) LogTail(logs []*management.Log, ch <-chan []*management.Log,

var res []View
for _, l := range logs {
res = append(res, &logView{Log: l, silent: silent, raw: l})
res = append(res, &LogView{Log: l, silent: silent, raw: l})
}

viewChan := make(chan View)
Expand All @@ -184,10 +266,41 @@ func (r *Renderer) LogTail(logs []*management.Log, ch <-chan []*management.Log,

for list := range ch {
for _, l := range list {
viewChan <- &logView{Log: l, silent: silent, raw: l}
viewChan <- &LogView{Log: l, silent: silent, raw: l}
}
}
}()

r.Stream(res, viewChan)
}

// Using below code to avoid the Bell sound
// when toggling up/down on prompt.
type noBellStdout struct{}

func (n *noBellStdout) Write(p []byte) (int, error) {
if len(p) == 1 && p[0] == readline.CharBell {
return 0, nil
}
return readline.Stdout.Write(p)
}

func (n *noBellStdout) Close() error {
return readline.Stdout.Close()
}

func (r *Renderer) QuitPrompt() bool {
fmt.Print("\nPress 'q' to quit or any other key to continue...\n")

ContTty, _ := tty.Open()
defer func(ContTty *tty.TTY) {
_ = ContTty.Close()
}(ContTty)

rn, err := ContTty.ReadRune()
if err != nil {
panic(err)
}

return rn == 'q' || rn == 'Q'
}
4 changes: 2 additions & 2 deletions internal/display/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestStream(t *testing.T) {
}

results := []View{
&logView{
&LogView{
Log: &management.Log{
LogID: auth0.String("354234"),
Type: auth0.String("sapi"),
Expand Down Expand Up @@ -51,7 +51,7 @@ API Operation Update branding settings
wg.Add(1)
go func() {
defer wg.Done()
viewChan <- &logView{
viewChan <- &LogView{
Log: &management.Log{
LogID: auth0.String("354236"),
Type: auth0.String("sapi"),
Expand Down
Loading