github.com/franciscocpg/up@v0.1.10/platform/lambda/logs.go (about)

     1  package lambda
     2  
     3  import (
     4  	"encoding/json"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/apex/log"
    11  	jsonlog "github.com/apex/log/handlers/json"
    12  
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/aws/session"
    15  	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
    16  	"github.com/mattn/go-isatty"
    17  
    18  	"github.com/apex/up/internal/logs/parser"
    19  	"github.com/apex/up/internal/logs/text"
    20  	"github.com/apex/up/internal/util"
    21  	"github.com/apex/up/platform"
    22  	"github.com/apex/up/platform/lambda/logs"
    23  )
    24  
    25  // TODO: move formatting logic outside of platform, reader interface
    26  // TODO: optionally expand fields
    27  
    28  // Logs implementation.
    29  type Logs struct {
    30  	platform *Platform
    31  	region   string
    32  	query    string
    33  	follow   bool
    34  	since    time.Time
    35  	w        io.WriteCloser
    36  	io.Reader
    37  }
    38  
    39  // NewLogs returns a new logs tailer.
    40  func NewLogs(p *Platform, region, query string) platform.Logs {
    41  	r, w := io.Pipe()
    42  
    43  	query, err := parseQuery(query)
    44  	if err != nil {
    45  		w.CloseWithError(err)
    46  	}
    47  
    48  	l := &Logs{
    49  		platform: p,
    50  		region:   region,
    51  		query:    query,
    52  		Reader:   r,
    53  		w:        w,
    54  	}
    55  
    56  	go l.start()
    57  
    58  	return l
    59  }
    60  
    61  // Since implementation.
    62  func (l *Logs) Since(t time.Time) {
    63  	l.since = t
    64  }
    65  
    66  // Follow implementation.
    67  func (l *Logs) Follow() {
    68  	log.Debug("follow")
    69  	l.follow = true
    70  }
    71  
    72  // start fetching logs.
    73  func (l *Logs) start() {
    74  	// TODO: flag to override and allow querying other groups
    75  	// TODO: apply backoff instead of PollInterval
    76  	group := "/aws/lambda/" + l.platform.config.Name
    77  
    78  	config := logs.Config{
    79  		Service:       cloudwatchlogs.New(session.New(aws.NewConfig().WithRegion(l.region))),
    80  		StartTime:     l.since,
    81  		PollInterval:  2 * time.Second,
    82  		Follow:        l.follow,
    83  		FilterPattern: l.query,
    84  	}
    85  
    86  	tailer := &logs.Logs{
    87  		Config:     config,
    88  		GroupNames: []string{group},
    89  	}
    90  
    91  	// TODO: delegate isatty stuff...
    92  
    93  	var handler log.Handler
    94  
    95  	if isatty.IsTerminal(os.Stdout.Fd()) {
    96  		handler = text.New(os.Stdout)
    97  	} else {
    98  		handler = jsonlog.New(os.Stdout)
    99  	}
   100  
   101  	// TODO: transform to reader of nl-delimited json, move to apex/log?
   102  	for l := range tailer.Start() {
   103  		line := strings.TrimSpace(l.Message)
   104  
   105  		if !util.IsJSON(line) {
   106  			// fmt.Fprint(l.w, e.Message) // TODO: ignore? json-ify?
   107  			continue
   108  		}
   109  
   110  		var e log.Entry
   111  		err := json.Unmarshal([]byte(line), &e)
   112  		if err != nil {
   113  			log.Fatalf("error parsing json: %s", err)
   114  		}
   115  
   116  		handler.HandleLog(&e)
   117  	}
   118  
   119  	// TODO: refactor interface to delegate
   120  	if err := tailer.Err(); err != nil {
   121  		panic(err)
   122  	}
   123  
   124  	l.w.Close()
   125  }
   126  
   127  // parseQuery parses and converts the query to a CW friendly syntax.
   128  func parseQuery(s string) (string, error) {
   129  	if s == "" {
   130  		return s, nil
   131  	}
   132  
   133  	n, err := parser.Parse(s)
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  
   138  	return n.String(), nil
   139  }