Skip to content

Commit

Permalink
Merge pull request #628 from iamhopaul123/app-log/init
Browse files Browse the repository at this point in the history
feat(app-logs): add `app logs` subcommand
  • Loading branch information
iamhopaul123 authored Feb 5, 2020
2 parents 53e2629 + 0ca893f commit 04910d8
Show file tree
Hide file tree
Showing 17 changed files with 1,906 additions and 13 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,5 @@ gen-mocks: tools
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_describe.go -source=./internal/pkg/describe/webapp.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ecr/mocks/mock_ecr.go -source=./internal/pkg/aws/ecr/ecr.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ecs/mocks/mock_ecs.go -source=./internal/pkg/aws/ecs/ecs.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/cloudwatchlogs/mocks/mock_cloudwatchlogs.go -source=./internal/pkg/aws/cloudwatchlogs/cloudwatchlogs.go
${GOBIN}/mockgen -source=./internal/pkg/build/docker/docker.go -package=mocks -destination=./internal/pkg/build/docker/mocks/mock_docker.go
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ require (
github.com/spf13/viper v1.6.2
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e // indirect
golang.org/x/text v0.3.2 // indirect
gopkg.in/ini.v1 v1.52.0
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packd v0.4.0 h1:0jj6ImCqgBbqx874P0qFeowoN3mGilNVY//YXV5SokU=
github.com/gobuffalo/packd v0.4.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down
177 changes: 177 additions & 0 deletions internal/pkg/aws/cloudwatchlogs/cloudwatchlogs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package cloudwatchlogs contains utility functions for Cloudwatch Logs client.
package cloudwatchlogs

import (
"fmt"
"sort"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
)

const (
// SleepDuration is the sleep time for making the next request for log events.
SleepDuration = 1 * time.Second
)

var (
fatalCodes = []string{"FATA", "FATAL", "fatal", "ERR", "ERROR", "error"}
warningCodes = []string{"WARN", "warn", "WARNING", "warning"}
)

type cloudwatchlogsClient interface {
DescribeLogStreams(input *cloudwatchlogs.DescribeLogStreamsInput) (*cloudwatchlogs.DescribeLogStreamsOutput, error)
GetLogEvents(input *cloudwatchlogs.GetLogEventsInput) (*cloudwatchlogs.GetLogEventsOutput, error)
}

// Service wraps an AWS Cloudwatch Logs client.
type Service struct {
cwlogs cloudwatchlogsClient
}

// GetLogEventsOpts sets up optional parameters for LogEvents function.
type GetLogEventsOpts func(*cloudwatchlogs.GetLogEventsInput)

// WithLimit sets up limit for GetLogEventsInput
func WithLimit(limit int) GetLogEventsOpts {
return func(in *cloudwatchlogs.GetLogEventsInput) {
in.Limit = aws.Int64(int64(limit))
}
}

// WithStartTime sets up startTime for GetLogEventsInput
func WithStartTime(startTime int64) GetLogEventsOpts {
return func(in *cloudwatchlogs.GetLogEventsInput) {
in.StartTime = aws.Int64(startTime)
}
}

// WithEndTime sets up endTime for GetLogEventsInput
func WithEndTime(endTime int64) GetLogEventsOpts {
return func(in *cloudwatchlogs.GetLogEventsInput) {
in.EndTime = aws.Int64(endTime)
}
}

// LogEventsOutput contains the output for LogEvents
type LogEventsOutput struct {
// Retrieved log events.
Events []*Event
// Next tokens for each log stream.
NextTokens map[string]*string
}

// New returns a Service configured against the input session.
func New(s *session.Session) *Service {
return &Service{
cwlogs: cloudwatchlogs.New(s),
}
}

// logStreams returns all name of the log streams in a log group.
func (s *Service) logStreams(logGroupName string) ([]*string, error) {
resp, err := s.cwlogs.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{
LogGroupName: aws.String(logGroupName),
Descending: aws.Bool(true),
OrderBy: aws.String(cloudwatchlogs.OrderByLastEventTime),
})
if err != nil {
return nil, fmt.Errorf("describe log streams of log group %s: %w", logGroupName, err)
}
if len(resp.LogStreams) == 0 {
return nil, fmt.Errorf("no log stream found in log group %s", logGroupName)
}
logStreamNames := make([]*string, len(resp.LogStreams))
for ind, logStream := range resp.LogStreams {
logStreamNames[ind] = logStream.LogStreamName
}
return logStreamNames, nil
}

// TaskLogEvents returns an array of Cloudwatch Logs events.
func (s *Service) TaskLogEvents(logGroupName string, streamTokens map[string]*string, opts ...GetLogEventsOpts) (*LogEventsOutput, error) {
var events []*Event
var in *cloudwatchlogs.GetLogEventsInput
nextForwardTokens := make(map[string]*string)
logStreamNames, err := s.logStreams(logGroupName)
if err != nil {
return nil, err
}
for _, logStreamName := range logStreamNames {
in = &cloudwatchlogs.GetLogEventsInput{
LogGroupName: aws.String(logGroupName),
LogStreamName: logStreamName,
Limit: aws.Int64(10), // default to be 10
NextToken: streamTokens[*logStreamName],
}
for _, opt := range opts {
opt(in)
}
// TODO: https://github.com/aws/amazon-ecs-cli-v2/pull/628#discussion_r374291068 and https://github.com/aws/amazon-ecs-cli-v2/pull/628#discussion_r374294362
resp, err := s.cwlogs.GetLogEvents(in)
if err != nil {
return nil, fmt.Errorf("get log events of %s/%s: %w", logGroupName, *logStreamName, err)
}

for _, event := range resp.Events {
taskID, err := parseTaskID(*logStreamName)
if err != nil {
return nil, err
}
log := &Event{
TaskID: taskID,
IngestionTime: aws.Int64Value(event.IngestionTime),
Message: aws.StringValue(event.Message),
Timestamp: aws.Int64Value(event.Timestamp),
}
events = append(events, log)
}

nextForwardTokens[*logStreamName] = resp.NextForwardToken
}
sort.SliceStable(events, func(i, j int) bool { return events[i].Timestamp < events[j].Timestamp })
var truncatedEvents []*Event
if len(events) >= int(*in.Limit) {
truncatedEvents = events[len(events)-int(*in.Limit):]
} else {
truncatedEvents = events
}
return &LogEventsOutput{
Events: truncatedEvents,
NextTokens: nextForwardTokens,
}, nil
}

// LogGroupExists returns if a log group exists.
func (s *Service) LogGroupExists(logGroupName string) (bool, error) {
_, err := s.cwlogs.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{
LogGroupName: aws.String(logGroupName),
})
if err == nil {
return true, nil
}
aerr, ok := err.(awserr.Error)
if !ok {
return false, err
}
if aerr.Code() != cloudwatchlogs.ErrCodeResourceNotFoundException {
return false, err
}
return false, nil
}

func parseTaskID(logStreamName string) (string, error) {
// logStreamName example: ecs/appName/1cc0685ad01d4d0f8e4e2c00d1775c56
logStreamNameSplit := strings.Split(logStreamName, "/")
if len(logStreamNameSplit) != 3 {
return "", fmt.Errorf("cannot parse task ID from log stream name: %s", logStreamName)
}
return logStreamNameSplit[2], nil
}
Loading

0 comments on commit 04910d8

Please sign in to comment.