pulumi/cmd/logs.go
Luke Hoban 75f7416524 Collect logs in parallel
Parallelize collection of logs at each layer of the resource tree walk.  Since almost all cost of log collection is at leaf nodes, this should allow the total time for log collection to be close to the time taken for the single longest leaf node, instead of the sum of all leaf nodes.
2017-11-22 21:33:36 -08:00

128 lines
3.5 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newLogsCmd() *cobra.Command {
var stack string
var follow bool
var since string
var resource string
logsCmd := &cobra.Command{
Use: "logs",
Short: "Show aggregated logs for a project",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
stackName, err := explicitOrCurrent(stack, backend)
if err != nil {
return err
}
startTime := parseRelativeDuration(since)
var resourceFilter *operations.ResourceFilter
if resource != "" {
var rf = operations.ResourceFilter(resource)
resourceFilter = &rf
}
// IDEA: This map will grow forever as new log entries are found. We may need to do a more approximate
// approach here to ensure we don't grow memory unboundedly while following logs.
//
// Note: Just tracking latest log date is not sufficient - as stale logs may show up which should have been
// displayed before previously rendered log entries, but weren't available at the time, so still need to be
// rendered now even though they are technically out of order.
shown := map[operations.LogEntry]bool{}
for {
logs, err := backend.GetLogs(stackName, operations.LogQuery{
StartTime: startTime,
ResourceFilter: resourceFilter,
})
if err != nil {
return err
}
for _, logEntry := range logs {
if _, shownAlready := shown[logEntry]; !shownAlready {
eventTime := time.Unix(0, logEntry.Timestamp*1000000)
fmt.Printf("%30.30s[%30.30s] %v\n", eventTime.Format(time.RFC3339Nano), logEntry.ID, logEntry.Message)
shown[logEntry] = true
}
}
if !follow {
return nil
}
time.Sleep(time.Second)
}
}),
}
logsCmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"List configuration for a different stack than the currently selected stack")
logsCmd.PersistentFlags().BoolVarP(
&follow, "follow", "f", false,
"Follow the log stream in real time (like tail -f)")
logsCmd.PersistentFlags().StringVar(
&since, "since", "",
"Only return logs newer than a relative duration ('5s', '2m', '3h'). Defaults to returning all logs.")
logsCmd.PersistentFlags().StringVarP(
&resource, "resource", "r", "",
"Only return logs for the requested resource ('name', 'type::name' or full URN). Defaults to returning all logs.")
return logsCmd
}
var durationRegexp = regexp.MustCompile(`(\d+)([y|w|d|h|m|s])`)
// parseRelativeDuration extracts a time.Time previous to now by the a relative duration in the format '5s', '2m', '3h'.
func parseRelativeDuration(duration string) *time.Time {
now := time.Now()
if duration == "" {
return nil
}
parts := durationRegexp.FindStringSubmatch(duration)
if parts == nil {
fmt.Printf("Warning: duration could not be parsed: '%v'\n", duration)
return nil
}
num, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
fmt.Printf("Warning: duration could not be parsed: '%v'\n", duration)
return nil
}
d := time.Duration(-num)
switch parts[2] {
case "y":
d *= time.Hour * 24 * 365
case "w":
d *= time.Hour * 24 * 7
case "d":
d *= time.Hour * 24
case "h":
d *= time.Hour
case "m":
d *= time.Minute
case "s":
d *= time.Second
default:
fmt.Printf("Warning: duration could not be parsed: '%v'\n", duration)
return nil
}
ret := now.Add(d)
return &ret
}