github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/logs/logs.go (about)

     1  package logs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/url"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/hashicorp/errwrap"
    16  	"github.com/henvic/wedeploycli/apihelper"
    17  	"github.com/henvic/wedeploycli/color"
    18  	"github.com/henvic/wedeploycli/colorwheel"
    19  	"github.com/henvic/wedeploycli/config"
    20  	"github.com/henvic/wedeploycli/errorhandler"
    21  	"github.com/henvic/wedeploycli/logs/internal/timelog"
    22  	"github.com/henvic/wedeploycli/verbose"
    23  )
    24  
    25  // Client for the services
    26  type Client struct {
    27  	*apihelper.Client
    28  }
    29  
    30  // New Client
    31  func New(wectx config.Context) *Client {
    32  	return &Client{
    33  		apihelper.New(wectx),
    34  	}
    35  }
    36  
    37  // Log structure
    38  type Log struct {
    39  	InsertID      string                  `json:"insertId"`
    40  	ServiceID     string                  `json:"serviceId,omitempty"`
    41  	ContainerUID  string                  `json:"containerUid,omitempty"`
    42  	Build         bool                    `json:"build,omitempty"`
    43  	BuildGroupUID string                  `json:"buildGroupUid,omitempty"`
    44  	DeployUID     string                  `json:"deployUid,omitempty"`
    45  	ProjectID     string                  `json:"projectId,omitempty"`
    46  	Level         string                  `json:"level,omitempty"`
    47  	Message       string                  `json:"message,omitempty"`
    48  	Timestamp     timelog.TimeStackDriver `json:"timestamp"`
    49  }
    50  
    51  // Filter structure
    52  type Filter struct {
    53  	Project       string   `json:"-"`
    54  	Services      []string `json:"services,omitempty"`
    55  	Instance      string   `json:"containerUid,omitempty"`
    56  	Level         string   `json:"level,omitempty"`
    57  	Since         string   `json:"start,omitempty"`
    58  	AfterInsertID string   `json:"afterInsertId,omitempty"`
    59  }
    60  
    61  // Watcher structure
    62  type Watcher struct {
    63  	Client          *Client
    64  	PoolingInterval time.Duration
    65  
    66  	Filter      *Filter
    67  	filterMutex sync.Mutex
    68  
    69  	ctx context.Context
    70  }
    71  
    72  // PoolingInterval is the default time between retries.
    73  var PoolingInterval = 5 * time.Second
    74  
    75  var instancesWheel = colorwheel.New(color.TextPalette)
    76  
    77  var errStream io.Writer = os.Stderr
    78  var errStreamMutex sync.Mutex
    79  
    80  var outStream io.Writer = os.Stdout
    81  var outStreamMutex sync.Mutex
    82  
    83  // GetList logs
    84  func (c *Client) GetList(ctx context.Context, f *Filter) ([]Log, error) {
    85  	var list []Log
    86  
    87  	var params = []string{
    88  		"/projects",
    89  		url.PathEscape(f.Project),
    90  	}
    91  
    92  	params = append(params, "/logs")
    93  
    94  	var req = c.Client.URL(ctx, params...)
    95  
    96  	c.Client.Auth(req)
    97  
    98  	if len(f.Services) == 1 && f.Services[0] != "" {
    99  		// avoid getting all logs unnecessarily
   100  		// CAUTION: see filter function below: on changes here, update it.
   101  		req.Param("serviceId", f.Services[0])
   102  	}
   103  
   104  	if f.Level != "" {
   105  		req.Param("level", f.Level)
   106  	}
   107  
   108  	if f.AfterInsertID != "" {
   109  		req.Param("afterInsertId", f.AfterInsertID)
   110  	}
   111  
   112  	if f.Since != "" {
   113  		req.Param("start", f.Since)
   114  	}
   115  
   116  	// it relies on wedeploy/data, which currently has a hard limit of 9999 results
   117  	req.Param("limit", "9999")
   118  
   119  	var err = apihelper.Validate(req, req.Get())
   120  
   121  	if err != nil {
   122  		return list, err
   123  	}
   124  
   125  	err = apihelper.DecodeJSON(req, &list)
   126  
   127  	if err != nil {
   128  		return list, errwrap.Wrapf("can't decode logs JSON: {{err}}", err)
   129  	}
   130  
   131  	return filter(list, f.Services, f.Instance), nil
   132  }
   133  
   134  func filter(list []Log, services []string, instance string) []Log {
   135  	// CAUTION: see optimization call to ?serviceId=:serviceID above: on changes here, update it.
   136  	if instance == "" && len(services) <= 1 {
   137  		return list
   138  	}
   139  
   140  	var l = []Log{}
   141  
   142  	for _, il := range list {
   143  		if isService(il.ServiceID, services) && isContainer(il.ContainerUID, instance) {
   144  			l = append(l, il)
   145  		}
   146  	}
   147  
   148  	return l
   149  }
   150  
   151  func isContainer(s, prefix string) bool {
   152  	if prefix == "" {
   153  		return true
   154  	}
   155  
   156  	return strings.HasPrefix(s, prefix)
   157  }
   158  
   159  func isService(current string, ss []string) bool {
   160  	for _, s := range ss {
   161  		if current == s {
   162  			return true
   163  		}
   164  	}
   165  
   166  	return len(ss) == 0
   167  }
   168  
   169  // List logs
   170  func (c *Client) List(ctx context.Context, filter *Filter) error {
   171  	var list, err = c.GetList(ctx, filter)
   172  
   173  	if err == nil {
   174  		printList(list)
   175  	}
   176  
   177  	return err
   178  }
   179  
   180  // Watch logs. If no pooling interval is set it uses the default value.
   181  func (w *Watcher) Watch(ctx context.Context, wectx config.Context) {
   182  	w.ctx = ctx
   183  	w.Client = New(wectx)
   184  
   185  	if w.PoolingInterval == 0 {
   186  		w.PoolingInterval = PoolingInterval
   187  	}
   188  
   189  	w.watch()
   190  }
   191  
   192  func (w *Watcher) watch() {
   193  	_, _ = fmt.Fprintf(outStream, "Logs shown on your current timezone: %s\n", time.Now().Format("-07:00"))
   194  	w.pool()
   195  
   196  	ticker := time.NewTicker(w.PoolingInterval)
   197  
   198  	for {
   199  		select {
   200  		case <-w.ctx.Done():
   201  			ticker.Stop()
   202  			return
   203  		case <-ticker.C:
   204  			w.pool()
   205  		}
   206  	}
   207  }
   208  
   209  func addHeader(log Log) (m string) {
   210  	switch {
   211  	case log.ServiceID == "":
   212  		return "[" + log.ProjectID + "]"
   213  	case log.ContainerUID != "":
   214  		return "[" + log.ServiceID + "-" + trim(log.ContainerUID, 12) + "]"
   215  	case log.Build:
   216  		return "build-" + log.BuildGroupUID + " " + log.ServiceID + "[building]"
   217  	case log.BuildGroupUID != "":
   218  		return "build-" + log.BuildGroupUID + " [" + log.ProjectID + "]"
   219  	}
   220  
   221  	return "[" + log.ProjectID + "]"
   222  }
   223  
   224  func printList(list []Log) {
   225  	for _, log := range list {
   226  		iw := instancesWheel.Get(log.ProjectID + "-" + log.ContainerUID)
   227  		fd := color.Format(iw, addHeader(log))
   228  		ts := color.Format(color.FgWhite, getLocalTimestamp(log.Timestamp))
   229  
   230  		outStreamMutex.Lock()
   231  		_, _ = fmt.Fprintf(outStream, "%v %v %v\n", ts, fd, strings.TrimSpace(log.Message))
   232  		outStreamMutex.Unlock()
   233  	}
   234  }
   235  
   236  func getLocalTimestamp(t timelog.TimeStackDriver) string {
   237  	v := time.Time(t)
   238  	l := v.Local()
   239  	return l.Format("Jan 02 15:04:05.000")
   240  }
   241  
   242  func (w *Watcher) pool() {
   243  	var ctx, cancel = context.WithTimeout(w.ctx, 10*time.Second)
   244  	defer cancel()
   245  
   246  	var list, err = w.Client.GetList(ctx, w.Filter)
   247  	cancel()
   248  
   249  	if err != nil && w.ctx.Err() == nil {
   250  		errStreamMutex.Lock()
   251  		defer errStreamMutex.Unlock()
   252  		_, _ = fmt.Fprintf(errStream, "%v\n", errorhandler.Handle(err))
   253  		return
   254  	}
   255  
   256  	if len(list) == 0 {
   257  		w.filterMutex.Lock()
   258  		defer w.filterMutex.Unlock()
   259  		verbose.Debug("No new log since " + w.Filter.Since)
   260  		return
   261  	}
   262  
   263  	printList(list)
   264  
   265  	if err := w.prepareNext(list); err != nil {
   266  		errStreamMutex.Lock()
   267  		defer errStreamMutex.Unlock()
   268  		_, _ = fmt.Fprintf(errStream, "%v\n", errorhandler.Handle(err))
   269  		return
   270  	}
   271  }
   272  
   273  func (w *Watcher) prepareNext(list []Log) error {
   274  	var last = list[len(list)-1]
   275  
   276  	w.Filter.AfterInsertID = last.InsertID
   277  	verbose.Debug("Next logs after log insertId = " + last.InsertID)
   278  
   279  	var next = time.Time(last.Timestamp)
   280  
   281  	if next.IsZero() {
   282  		return errors.New("invalid timestamp value on log line")
   283  	}
   284  
   285  	w.filterMutex.Lock()
   286  	defer w.filterMutex.Unlock()
   287  	w.Filter.Since = fmt.Sprintf("%v", next.Add(time.Nanosecond).Format(time.RFC3339Nano))
   288  	verbose.Debug("Next --since parameter value = " + w.Filter.Since)
   289  	return nil
   290  }
   291  
   292  // GetUnixTimestamp gets the Unix timestamp in seconds from a friendly string.
   293  // Be aware that the dashboard is using ms, not s.
   294  func GetUnixTimestamp(since string) (int64, error) {
   295  	if num, err := strconv.ParseInt(since, 10, 0); err == nil {
   296  		return num, err
   297  	}
   298  
   299  	var now = time.Now()
   300  
   301  	since = strings.Replace(since, "min", "m", -1)
   302  
   303  	var pds, err = time.ParseDuration(since)
   304  
   305  	if err != nil {
   306  		return 0, err
   307  	}
   308  
   309  	return now.Add(-pds).Unix(), err
   310  }
   311  
   312  func trim(s string, max int) string {
   313  	runes := []rune(s)
   314  
   315  	if len(runes) > max {
   316  		return string(runes[:max])
   317  	}
   318  
   319  	return s
   320  }