Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions cmd/skpr/logs/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/spf13/cobra"

"github.com/skpr/cli/cmd/skpr/logs/list"
"github.com/skpr/cli/cmd/skpr/logs/query"
"github.com/skpr/cli/cmd/skpr/logs/summarise"
"github.com/skpr/cli/cmd/skpr/logs/tail"
skprcommand "github.com/skpr/cli/internal/command"
)
Expand All @@ -24,6 +26,8 @@ func NewCommand() *cobra.Command {
}

cmd.AddCommand(list.NewCommand())
cmd.AddCommand(query.NewCommand())
cmd.AddCommand(summarise.NewCommand())
cmd.AddCommand(tail.NewCommand())

return cmd
Expand Down
54 changes: 54 additions & 0 deletions cmd/skpr/logs/query/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package query

import (
"github.com/spf13/cobra"

"github.com/skpr/cli/internal/command/logs/filter"
v1query "github.com/skpr/cli/internal/command/logs/query"
)

var (
cmdLong = `Run a bounded query over the logs of a running application.`

cmdExample = `
# Query the last hour of the default streams.
skpr logs query dev

# Query specific streams over the last 24 hours.
skpr logs query dev nginx fpm --timeframe 24h

# Query an absolute time range.
skpr logs query dev --from "2 days ago" --to now

# Query for events containing a substring, excluding another.
skpr logs query dev --contains error --exclude healthcheck`
)

// NewCommand creates a new cobra.Command for 'query' sub command
func NewCommand() *cobra.Command {
command := v1query.Command{}

cmd := &cobra.Command{
Use: "query <environment> <stream> <stream>",
Args: cobra.MinimumNArgs(1),
DisableFlagsInUseLine: true,
Short: "Run a bounded query over the logs of a running application",
Long: cmdLong,
Example: cmdExample,
RunE: func(cmd *cobra.Command, args []string) error {
command.Environment = args[0]

if len(args) > 1 {
command.Streams = args[1:]
}

return command.Run(cmd.Context())
},
}

filter.AddFlags(cmd.Flags(), &command.Options)
cmd.Flags().Int32Var(&command.Limit, "limit", 0, "Cap on returned events (0 for server default)")
cmd.Flags().BoolVar(&command.Indent, "indent", false, "Enable indenting for pretty printed logs")

return cmd
}
51 changes: 51 additions & 0 deletions cmd/skpr/logs/summarise/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package summarise

import (
"github.com/spf13/cobra"

"github.com/skpr/cli/internal/command/logs/filter"
v1summarise "github.com/skpr/cli/internal/command/logs/summarise"
)

var (
cmdLong = `Summarise a window of an application's logs using a natural-language prompt.`

cmdExample = `
# Summarise the last hour of the default streams.
skpr logs summarise dev

# Summarise specific streams over the last 24 hours.
skpr logs summarise dev nginx fpm --timeframe 24h

# Ask a specific question of the summariser.
skpr logs summarise dev --prompt "What caused the 500 errors?"`
)

// NewCommand creates a new cobra.Command for 'summarise' sub command
func NewCommand() *cobra.Command {
command := v1summarise.Command{}

cmd := &cobra.Command{
Use: "summarise <environment> <stream> <stream>",
Aliases: []string{"summarize"},
Args: cobra.MinimumNArgs(1),
DisableFlagsInUseLine: true,
Short: "Summarise a window of an application's logs",
Long: cmdLong,
Example: cmdExample,
RunE: func(cmd *cobra.Command, args []string) error {
command.Environment = args[0]

if len(args) > 1 {
command.Streams = args[1:]
}

return command.Run(cmd.Context())
},
}

filter.AddFlags(cmd.Flags(), &command.Options)
cmd.Flags().StringVar(&command.Prompt, "prompt", "", "Natural-language question for the summariser")

return cmd
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ require (
github.com/moby/patternmatcher v0.6.1
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
github.com/pkg/errors v0.9.1
github.com/skpr/api v1.6.0
github.com/skpr/api v1.7.0
github.com/skpr/compass/tracing v0.0.0-20251208094547-dafe383c3926
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.10.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skpr/api v1.6.0 h1:LzbNm97wXvnWAcm+iyV1r36YKGMFWNn65y+EHhBxgqo=
github.com/skpr/api v1.6.0/go.mod h1:LMyIte2bWjGuO2e8XOA6FKNFUX10itF2qYVTckM7dwg=
github.com/skpr/api v1.7.0 h1:J9ZRTKDyUlSHXUGbkppB22fic/gC40g1+LjlWuxRijs=
github.com/skpr/api v1.7.0/go.mod h1:HWzGU9hY392K8N4I7xdk+anSRLoouJM2NtSjnYfIj1s=
github.com/skpr/compass/tracing v0.0.0-20251208094547-dafe383c3926 h1:g8m8qqehB6/GMeM81BaIXQOkD2LZbCQE0HPRGear9Jw=
github.com/skpr/compass/tracing v0.0.0-20251208094547-dafe383c3926/go.mod h1:Rokt2mnHteBaBmsSgiNk4P7laV0E8/BLQsSLyGUJLtk=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
Expand Down
104 changes: 104 additions & 0 deletions internal/command/logs/filter/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package filter

import (
"fmt"
"time"

"github.com/skpr/api/pb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"

skprtime "github.com/skpr/cli/internal/time"
)

// Options holds the shared filter inputs used by bounded log operations such as
// query and summarise.
type Options struct {
Environment string
Streams []string
Timeframe time.Duration
From string
To string
Contains []string
Exclude []string
}

// Build converts the options into a pb.LogFilter. An absolute time range
// (--from/--to) takes precedence over the relative --timeframe.
func (o Options) Build() (*pb.LogFilter, error) {
filter := &pb.LogFilter{
Environment: o.Environment,
Streams: o.Streams,
}

// Build the substring filters. An event must satisfy every entry.
for _, value := range o.Contains {
filter.Contains = append(filter.Contains, &pb.LogContainsFilter{
Value: value,
})
}

for _, value := range o.Exclude {
filter.Contains = append(filter.Contains, &pb.LogContainsFilter{
Value: value,
Exclude: true,
})
}

if o.From != "" || o.To != "" {
window, err := buildTimeRange(o.From, o.To)
if err != nil {
return nil, err
}

filter.Window = window
} else {
filter.Window = &pb.LogFilter_Timeframe{
Timeframe: durationpb.New(o.Timeframe),
}
}

return filter, nil
}

// buildTimeRange constructs an absolute time range window from the provided
// --from and --to values. Either bound may be omitted.
func buildTimeRange(from, to string) (*pb.LogFilter_TimeRange, error) {
timeRange := &pb.LogTimeRange{}

if from != "" {
t, err := skprtime.ParseString(from)
if err != nil {
return nil, fmt.Errorf("failed to parse --from: %w", err)
}

timeRange.From = timestamppb.New(t)
}

if to != "" {
t, err := skprtime.ParseString(to)
if err != nil {
return nil, fmt.Errorf("failed to parse --to: %w", err)
}

timeRange.To = timestamppb.New(t)
}

return &pb.LogFilter_TimeRange{TimeRange: timeRange}, nil
}

// AddFlags registers the shared filter flags on a command's flag set.
func AddFlags(flags flagSet, o *Options) {
flags.DurationVar(&o.Timeframe, "timeframe", time.Hour, "Window duration relative to now (ignored when --from or --to is set)")
flags.StringVar(&o.From, "from", "", "Start of an absolute time range (e.g. \"2 days ago\", RFC3339 timestamp)")
flags.StringVar(&o.To, "to", "", "End of an absolute time range (e.g. \"now\", RFC3339 timestamp)")
flags.StringArrayVar(&o.Contains, "contains", nil, "Only include events whose message contains this substring (repeatable)")
flags.StringArrayVar(&o.Exclude, "exclude", nil, "Exclude events whose message contains this substring (repeatable)")
}

// flagSet is the subset of *pflag.FlagSet used to register filter flags.
type flagSet interface {
DurationVar(p *time.Duration, name string, value time.Duration, usage string)
StringVar(p *string, name, value, usage string)
StringArrayVar(p *[]string, name string, value []string, usage string)
}
27 changes: 18 additions & 9 deletions internal/command/logs/list/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import (
"context"
"fmt"
"os"

"github.com/skpr/api/pb"
"strings"

"github.com/skpr/cli/internal/client"
"github.com/skpr/cli/internal/command/logs/streams"
"github.com/skpr/cli/internal/components/tooltip"
skprtable "github.com/skpr/cli/internal/table"
)

// Helpful text provided by the tooltip.
const tooltipText = `To view logs for a specific stream, use the tail command.
const tooltipText = `To tail a stream in real-time, use the tail command.

$ skpr logs tail ENVIRONMENT STREAM STREAM

To run a bounded query over a time window, use the query command.

$ skpr logs tail ENVIRONMENT STREAM STREAM`
$ skpr logs query ENVIRONMENT STREAM STREAM`

// Command to list all log sources.
type Command struct {
Expand All @@ -29,21 +33,26 @@ func (cmd *Command) Run(ctx context.Context) error {
return err
}

resp, err := client.Logs().ListStreams(ctx, &pb.LogListStreamsRequest{
Environment: cmd.Environment,
})
resp, err := streams.List(ctx, client.Logs(), cmd.Environment)
if err != nil {
return fmt.Errorf("failed to list streams: %w", err)
}

header := []string{
"Streams",
"Stream",
"Capabilities",
}

var rows [][]string

for _, stream := range resp.Streams {
rows = append(rows, []string{stream})
var capabilities []string

for _, streamType := range stream.Types {
capabilities = append(capabilities, streamType.String())
}

rows = append(rows, []string{stream.Name, strings.Join(capabilities, ", ")})
}

err = skprtable.Print(os.Stdout, header, rows)
Expand Down
Loading
Loading