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 }