Skip to content
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

feat: enable job logs command #3794

Merged
merged 22 commits into from Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
54bbf43
chore: update cloudwatchlogs to allow limiting log streams fetched an…
bvtujo Jul 13, 2022
9912674
chore: add logic for fetching state machine logs to workload
bvtujo Jul 13, 2022
2fa22ef
chore: add unit tests for workload logging pkg
bvtujo Jul 13, 2022
a283acf
chore: make things work for ci
bvtujo Jul 13, 2022
c7ab051
chore: address feedback
bvtujo Jul 15, 2022
270014e
feat: add job logs logic
bvtujo Jul 14, 2022
00128b6
chore: add logic in underlying libraries for job logs (#3757)
bvtujo Jul 15, 2022
649ea99
fix logic bugs in log stream sorting
bvtujo Jul 18, 2022
acaa910
chore: make IncludeStateMachine a logging option, not a struct member
bvtujo Jul 18, 2022
b1e2c17
feat: enable job logs command and add tests
bvtujo Jul 18, 2022
f7ae13f
docs: add job logs docs
bvtujo Jul 22, 2022
6def373
chore: change default number of executions to show
bvtujo Jul 22, 2022
086ddf4
nit: don't reverse log stream order
bvtujo Aug 9, 2022
0b6af44
chore: move state machine logic into logging package
bvtujo Aug 10, 2022
cc2d3f1
chore: fix tests
bvtujo Aug 10, 2022
6641bad
docs: fix nits in docs
bvtujo Aug 10, 2022
b4896ac
Merge branch 'mainline' into job-logs
bvtujo Aug 10, 2022
d883342
Update site/content/docs/concepts/jobs.en.md
bvtujo Aug 11, 2022
28c61e4
Update internal/pkg/cli/job_logs.go
bvtujo Aug 11, 2022
8a52b10
Update site/content/docs/commands/job-logs.en.md
bvtujo Aug 11, 2022
53b1990
test: add additional param checks and unit test to validate limit and…
bvtujo Aug 11, 2022
2c8989e
Merge branch 'mainline' into job-logs
mergify[bot] Aug 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 18 additions & 5 deletions internal/pkg/aws/cloudwatchlogs/cloudwatchlogs_test.go
Expand Up @@ -283,7 +283,7 @@ func TestLogEvents(t *testing.T) {
"should limit log streams fetched": {
logGroupName: "mockLogGroup",
logStreamLimit: 2,
limit: aws.Int64(1),
limit: aws.Int64(2),
logStream: []string{"copilot/"},
mockcloudwatchlogsClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{
Expand All @@ -296,20 +296,24 @@ func TestLogEvents(t *testing.T) {
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream"),
LastEventTimestamp: aws.Int64(5),
},
{
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream2"),
LastEventTimestamp: aws.Int64(4),
},
{
LogStreamName: aws.String("states/abcde"),
LastEventTimestamp: aws.Int64(3),
},
{
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream2"),
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream3"),
LastEventTimestamp: aws.Int64(1),
},
},
}, nil)
m.EXPECT().GetLogEvents(&cloudwatchlogs.GetLogEventsInput{
LogGroupName: aws.String("mockLogGroup"),
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream"),
Limit: aws.Int64(1),
Limit: aws.Int64(2),
}).Return(&cloudwatchlogs.GetLogEventsOutput{
Events: []*cloudwatchlogs.OutputLogEvent{
{
Expand All @@ -321,13 +325,17 @@ func TestLogEvents(t *testing.T) {
m.EXPECT().GetLogEvents(&cloudwatchlogs.GetLogEventsInput{
LogGroupName: aws.String("mockLogGroup"),
LogStreamName: aws.String("copilot/mockLogGroup/mockLogStream2"),
Limit: aws.Int64(1),
Limit: aws.Int64(2),
}).Return(&cloudwatchlogs.GetLogEventsOutput{
Events: []*cloudwatchlogs.OutputLogEvent{
{
Message: aws.String("other log"),
Timestamp: aws.Int64(1),
},
{
Message: aws.String("important log"),
Timestamp: aws.Int64(4),
},
},
}, nil)
},
Expand All @@ -337,10 +345,15 @@ func TestLogEvents(t *testing.T) {
Timestamp: 5,
Message: "some log",
},
{
LogStreamName: "copilot/mockLogGroup/mockLogStream2",
Timestamp: 4,
Message: "important log",
},
},
wantLastEventTime: map[string]int64{
"copilot/mockLogGroup/mockLogStream": 5,
"copilot/mockLogGroup/mockLogStream2": 1,
"copilot/mockLogGroup/mockLogStream2": 4,
},
wantErr: nil,
},
Expand Down
4 changes: 2 additions & 2 deletions internal/pkg/cli/cli.go
Expand Up @@ -23,8 +23,8 @@ import (
)

const (
svcAppNamePrompt = "Which application does your service belong to?"
svcAppNameHelpPrompt = "An application groups all of your services and jobs together."
bvtujo marked this conversation as resolved.
Show resolved Hide resolved
svcAppNamePrompt = "Which application does your service belong to?"
wkldAppNameHelpPrompt = "An application groups all of your services and jobs together."
)

// tryReadingAppName retrieves the application's name from the workspace if it exists and returns it.
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/cli/flag.go
Expand Up @@ -36,6 +36,7 @@ const (
stackOutputDirFlag = "output-dir"
uploadAssetsFlag = "upload-assets"
limitFlag = "limit"
lastFlag = "last"
followFlag = "follow"
sinceFlag = "since"
startTimeFlag = "start-time"
Expand Down Expand Up @@ -227,6 +228,8 @@ Uploaded asset locations are filled in the template configuration.`

limitFlagDescription = `Optional. The maximum number of log events returned. Default is 10
unless any time filtering flags are set.`
lastFlagDescription = `Optional. The number of executions of the scheduled job for which
logs should be shown.`
followFlagDescription = "Optional. Specifies if the logs should be streamed."
sinceFlagDescription = `Optional. Only return logs newer than a relative duration like 5s, 2m, or 3h.
Defaults to all logs. Only one of start-time / since may be used.`
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/job_list.go
Expand Up @@ -74,7 +74,7 @@ func (o *listJobOpts) Ask() error {
return nil
}

name, err := o.sel.Application(jobListAppNamePrompt, svcAppNameHelpPrompt)
name, err := o.sel.Application(jobListAppNamePrompt, wkldAppNameHelpPrompt)
if err != nil {
return fmt.Errorf("select application name: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/job_list_test.go
Expand Up @@ -76,7 +76,7 @@ func TestListJobOpts_Ask(t *testing.T) {
}{
"with no flags set": {
mockSel: func(m *mocks.MockappSelector) {
m.EXPECT().Application(jobListAppNamePrompt, svcAppNameHelpPrompt).Return("myapp", nil)
m.EXPECT().Application(jobListAppNamePrompt, wkldAppNameHelpPrompt).Return("myapp", nil)
},
wantedApp: "myapp",
},
Expand Down
125 changes: 71 additions & 54 deletions internal/pkg/cli/job_logs.go
Expand Up @@ -26,12 +26,15 @@ const (

jobLogNamePrompt = "Which job's logs would you like to show?"
jobLogNameHelpPrompt = "The logs of the indicated deployed job will be shown."

defaultJobLogExecutionLimit = 1
)

type jobLogsVars struct {
wkldLogsVars

includeStateMachineLogs bool // Whether to include the logs from the state machine log streams
last int // The number of previous executions of the state machine to show.
}

type jobLogsOpts struct {
Expand Down Expand Up @@ -64,7 +67,7 @@ func newJobLogOpts(vars jobLogsVars) (*jobLogsOpts, error) {
},
}
opts.initLogsSvc = func() error {
env, err := opts.configStore.GetEnvironment(opts.appName, opts.envName)
env, err := opts.getTargetEnv()
if err != nil {
return fmt.Errorf("get environment: %w", err)
}
Expand All @@ -73,10 +76,13 @@ func newJobLogOpts(vars jobLogsVars) (*jobLogsOpts, error) {
return err
}
opts.logsSvc, err = logging.NewWorkloadClient(&logging.NewWorkloadLogsConfig{
Sess: sess,
App: opts.appName,
Env: opts.envName,
Name: opts.name,
Sess: sess,
App: opts.appName,
Env: opts.envName,
Name: opts.name,
LogGroup: opts.logGroup,
TaskIDs: opts.taskIDs,
ConfigStore: configStore,
})
if err != nil {
return err
Expand Down Expand Up @@ -151,38 +157,39 @@ func (o *jobLogsOpts) Ask() error {
return o.validateAndAskJobEnvName()
}

func (o *jobLogsOpts) validateOrAskApp() error {
if o.appName != "" {
_, err := o.configStore.GetApplication(o.appName)
// Execute outputs logs of the job.
func (o *jobLogsOpts) Execute() error {
if err := o.initLogsSvc(); err != nil {
return err
}
app, err := o.sel.Application(jobAppNamePrompt, svcAppNameHelpPrompt)
if err != nil {
return fmt.Errorf("select application: %w", err)
eventsWriter := logging.WriteHumanLogs
if o.shouldOutputJSON {
eventsWriter = logging.WriteJSONLogs
}
o.appName = app
return nil
}

func (o *jobLogsOpts) validateAndAskJobEnvName() error {
if o.envName != "" {
if _, err := o.getTargetEnv(); err != nil {
return err
}
var limit *int64
if o.limit != 0 {
limit = aws.Int64(int64(o.limit))
}

if o.name != "" {
if _, err := o.configStore.GetJob(o.appName, o.name); err != nil {
return err
}
// By default, only display the logs of the last execution of the job.
logStreamLimit := defaultJobLogExecutionLimit
if o.last != 0 {
logStreamLimit = o.last
}

deployedJob, err := o.sel.DeployedJob(jobLogNamePrompt, jobLogNameHelpPrompt, o.appName, selector.WithEnv(o.envName), selector.WithName(o.name))
err := o.logsSvc.WriteLogEvents(logging.WriteLogEventsOpts{
Follow: o.follow,
Limit: limit,
EndTime: o.endTime,
StartTime: o.startTime,
TaskIDs: o.taskIDs,
OnEvents: eventsWriter,
LogStreamLimit: logStreamLimit,
bvtujo marked this conversation as resolved.
Show resolved Hide resolved
IncludeStateMachineLogs: o.includeStateMachineLogs,
})
if err != nil {
return fmt.Errorf("select deployed jobs for application %s: %w", o.appName, err)
return fmt.Errorf("write log events for job %s: %w", o.name, err)
}
o.name = deployedJob.Name
o.envName = deployedJob.Env
return nil
}

Expand All @@ -198,54 +205,58 @@ func (o *jobLogsOpts) getTargetEnv() (*config.Environment, error) {
return o.targetEnv, nil
}

// Execute outputs logs of the job.
func (o *jobLogsOpts) Execute() error {
if err := o.initLogsSvc(); err != nil {
func (o *jobLogsOpts) validateOrAskApp() error {
if o.appName != "" {
_, err := o.configStore.GetApplication(o.appName)
return err
}
eventsWriter := logging.WriteHumanLogs
if o.shouldOutputJSON {
eventsWriter = logging.WriteJSONLogs
app, err := o.sel.Application(jobAppNamePrompt, wkldAppNameHelpPrompt)
if err != nil {
return fmt.Errorf("select application: %w", err)
}
o.appName = app
return nil
}

var limit *int64
if o.limit != 0 {
limit = aws.Int64(int64(o.limit))
func (o *jobLogsOpts) validateAndAskJobEnvName() error {
if o.envName != "" {
if _, err := o.getTargetEnv(); err != nil {
return err
}
}
err := o.logsSvc.WriteLogEvents(logging.WriteLogEventsOpts{
Follow: o.follow,
Limit: limit,
EndTime: o.endTime,
StartTime: o.startTime,
TaskIDs: o.taskIDs,
OnEvents: eventsWriter,
})
if o.name != "" {
if _, err := o.configStore.GetJob(o.appName, o.name); err != nil {
return err
}
}
deployedJob, err := o.sel.DeployedJob(jobLogNamePrompt, jobLogNameHelpPrompt, o.appName, selector.WithEnv(o.envName), selector.WithName(o.name))
if err != nil {
return fmt.Errorf("write log events for job %s: %w", o.name, err)
return fmt.Errorf("select deployed jobs for application %s: %w", o.appName, err)
}
o.name = deployedJob.Name
o.envName = deployedJob.Env
return nil
}

// buildJobLogsCmd builds the command for displaying job logs in an application.
func buildJobLogsCmd() *cobra.Command {
vars := jobLogsVars{}
cmd := &cobra.Command{
Use: "logs",
Short: "Displays logs of a deployed job.",
Hidden: true,
Use: "logs",
Short: "Displays logs of a deployed job.",
Example: `
Displays logs of the job "my-job" in environment "test".
/code $ copilot job logs -n my-job -e test
Displays logs in the last hour.
/code $ copilot job logs --since 1h
Displays logs from 2006-01-02T15:04:05 to 2006-01-02T15:05:05.
/code $ copilot job logs --start-time 2006-01-02T15:04:05+00:00 --end-time 2006-01-02T15:05:05+00:00
Displays logs from specific task IDs.
/code $ copilot job logs --tasks 709c7eae05f947f6861b150372ddc443,1de57fd63c6a4920ac416d02add891b9
Displays logs from the last execution of the job.
/code $ copilot job logs --last 1
Displays logs from specific task IDs.
/code $ copilot job logs --tasks 709c7ea,1de57fd
Displays logs in real time.
/code $ copilot job logs --follow
Displays container logs and state machine execution logs from the last execution.
/code $ copilot job logs --include-state-machine`,
/code $ copilot job logs --include-state-machine --last 1`,
RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
opts, err := newJobLogOpts(vars)
if err != nil {
Expand All @@ -263,7 +274,13 @@ Displays logs from specific task IDs.
cmd.Flags().BoolVar(&vars.follow, followFlag, false, followFlagDescription)
cmd.Flags().DurationVar(&vars.since, sinceFlag, 0, sinceFlagDescription)
cmd.Flags().IntVar(&vars.limit, limitFlag, 0, limitFlagDescription)
cmd.Flags().IntVar(&vars.last, lastFlag, 1, lastFlagDescription)
cmd.Flags().StringSliceVar(&vars.taskIDs, tasksFlag, nil, tasksLogsFlagDescription)
cmd.Flags().BoolVar(&vars.includeStateMachineLogs, includeStateMachineLogsFlag, false, includeStateMachineLogsFlagDescription)

// There's no way to associate a specific execution with a task without parsing the logs of every state machine invocation.
cmd.MarkFlagsMutuallyExclusive(includeStateMachineLogsFlag, tasksFlag)
cmd.MarkFlagsMutuallyExclusive(followFlag, lastFlag)

return cmd
}