github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/log.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"regexp"
    10  
    11  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/heroku-go"
    12  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/mgutz/ansi"
    13  )
    14  
    15  var (
    16  	lines  int
    17  	source string
    18  	dyno   string
    19  )
    20  
    21  var cmdLog = &Command{
    22  	Run:      runLog,
    23  	Usage:    "log [-n <lines>] [-s <source>] [-d <dyno>]",
    24  	NeedsApp: true,
    25  	Category: "app",
    26  	Short:    "stream app log lines",
    27  	Long: `
    28  Log prints the streaming application log.
    29  
    30  Options:
    31  
    32      -n <N>       print at most N log lines
    33      -s <source>  filter log source
    34      -d <dyno>    filter dyno or process type
    35  
    36  Examples:
    37  
    38      $ hk log
    39      2013-10-17T00:17:35.066089+00:00 app[web.1]: Completed 302 Found in 0ms
    40      2013-10-17T00:17:35.079095+00:00 heroku[router]: at=info method=GET path=/ host=www.heroku.com fwd="1.2.3.4" dyno=web.1 connect=1ms service=6ms status=302 bytes=95
    41      2013-10-17T00:17:35.505389+00:00 heroku[nginx]: 1.2.3.4 - - [17/Oct/2013:00:17:35 +0000] "GET / HTTP/1.1" 301 5 "-" "Amazon Route 53 Health Check Service" www.heroku.com
    42      ...
    43  
    44      $ hk log -n 2 -s app -d web
    45      2013-10-17T00:17:34.288521+00:00 app[web.1]: Completed 200 OK in 10ms (Views: 10.0ms)
    46      2013-10-17T00:17:33.918946+00:00 heroku[web.5]: Started GET "/" for 1.2.3.4 at 2013-10-17 00:17:32 +0000
    47      2013-10-17T00:17:34.667654+00:00 heroku[router]: at=info method=GET path=/ host=www.heroku.com fwd="1.2.3.4" dyno=web.5 connect=3ms service=8ms status=301 bytes=0
    48      2013-10-17T00:17:35.079095+00:00 heroku[router]: at=info method=GET path=/ host=www.heroku.com fwd="1.2.3.4" dyno=web.1 connect=1ms service=6ms status=302 bytes=95
    49      ...
    50  
    51      $ hk log -d web.5
    52      2013-10-17T00:17:33.918946+00:00 app[web.5]: Started GET "/" for 1.2.3.4 at 2013-10-17 00:17:32 +0000
    53      2013-10-17T00:17:33.918658+00:00 app[web.5]: Processing by PagesController#root as HTML
    54      ...
    55  `,
    56  }
    57  
    58  func init() {
    59  	cmdLog.Flag.IntVarP(&lines, "number", "n", -1, "max number of log lines to request")
    60  	cmdLog.Flag.StringVarP(&source, "source", "s", "", "only display logs from the given source")
    61  	cmdLog.Flag.StringVarP(&dyno, "dyno", "d", "", "only display logs from the given dyno or process type")
    62  }
    63  
    64  func runLog(cmd *Command, args []string) {
    65  	if len(args) != 0 {
    66  		cmd.PrintUsage()
    67  		os.Exit(2)
    68  	}
    69  
    70  	opts := heroku.LogSessionCreateOpts{}
    71  	if dyno != "" {
    72  		opts.Dyno = &dyno
    73  	}
    74  	if source != "" {
    75  		opts.Source = &source
    76  	}
    77  
    78  	if lines != -1 {
    79  		opts.Lines = &lines
    80  	} else {
    81  		tailopt := true
    82  		lineopt := 10
    83  		opts.Tail = &tailopt
    84  		opts.Lines = &lineopt
    85  	}
    86  
    87  	session, err := client.LogSessionCreate(mustApp(), &opts)
    88  	if err != nil {
    89  		printFatal(err.Error())
    90  	}
    91  	resp, err := http.Get(session.LogplexURL)
    92  	if err != nil {
    93  		printFatal(err.Error())
    94  	}
    95  	if warning := resp.Header.Get("X-Heroku-Warning"); warning != "" {
    96  		printWarning(warning)
    97  	}
    98  	if resp.StatusCode/100 != 2 {
    99  		if resp.StatusCode/100 == 4 {
   100  			printFatal("Unauthorized")
   101  		} else {
   102  			printFatal("Unexpected error: " + resp.Status)
   103  		}
   104  	}
   105  
   106  	// colors are disabled globally in main() depending on term.IsTerminal()
   107  	writer := newColorizer(os.Stdout)
   108  
   109  	scanner := bufio.NewScanner(resp.Body)
   110  	scanner.Split(bufio.ScanLines)
   111  
   112  	for scanner.Scan() {
   113  		_, err = writer.Writeln(scanner.Text())
   114  		must(err)
   115  	}
   116  
   117  	resp.Body.Close()
   118  }
   119  
   120  type colorizer struct {
   121  	colors      map[string]string
   122  	colorScheme []string
   123  	filter      *regexp.Regexp
   124  	writer      io.Writer
   125  }
   126  
   127  func newColorizer(writer io.Writer) *colorizer {
   128  	return &colorizer{
   129  		colors: make(map[string]string),
   130  		colorScheme: []string{
   131  			"cyan",
   132  			"yellow",
   133  			"green",
   134  			"magenta",
   135  			"red",
   136  		},
   137  		filter: regexp.MustCompile(`(?s)^(.*?\[([\w-]+)(?:[\d\.]+)?\]:)(.*)?$`),
   138  		writer: writer,
   139  	}
   140  }
   141  
   142  func (c *colorizer) resolve(p string) string {
   143  	if color, ok := c.colors[p]; ok {
   144  		return color
   145  	}
   146  
   147  	color := c.colorScheme[len(c.colors)%len(c.colorScheme)]
   148  	c.colors[p] = color
   149  	return color
   150  }
   151  
   152  func (c *colorizer) Writeln(p string) (n int, err error) {
   153  	if c.filter.MatchString(p) {
   154  		submatches := c.filter.FindStringSubmatch(p)
   155  		return fmt.Fprintln(c.writer, ansi.Color(submatches[1], c.resolve(submatches[2]))+ansi.ColorCode("reset")+submatches[3])
   156  	}
   157  
   158  	return fmt.Fprintln(c.writer, p)
   159  }