github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/experiment/tracer/trace.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "sort" 26 "strconv" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 33 "k8s.io/test-infra/prow/github" 34 "k8s.io/test-infra/prow/kube" 35 ) 36 37 type lineData struct { 38 TimeStr string `json:"time"` 39 time time.Time 40 PrNum int `json:"pr"` 41 Repo string `json:"repo"` 42 Org string `json:"org"` 43 EventGUID string `json:"event-GUID"` 44 URL string `json:"url"` 45 } 46 47 type podLine struct { 48 actual []byte 49 unmarshaled lineData 50 } 51 52 // linesByTimestamp sorts pod lines by timestamp. 53 // Useful when collecting logs across multiple pods. 54 type linesByTimestamp []podLine 55 56 // linesByTimestamp implements the sort.Interface interface. 57 var _ sort.Interface = linesByTimestamp{} 58 59 func (l linesByTimestamp) Len() int { return len(l) } 60 func (l linesByTimestamp) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 61 func (l linesByTimestamp) Less(i, j int) bool { 62 return l[i].unmarshaled.time.Before(l[j].unmarshaled.time) 63 } 64 65 // linesByTimestamp implements the fmt.Stringer interface. 66 var _ fmt.Stringer = linesByTimestamp{} 67 68 // Return valid json. 69 func (l linesByTimestamp) String() string { 70 sort.Sort(l) 71 72 var log string 73 for i, line := range l { 74 switch i { 75 case len(l) - 1: 76 log += string(line.actual) 77 default: 78 // buf.ReadBytes('\n') does not remove the newline 79 log += fmt.Sprintf("%s,\n", strings.TrimSuffix(string(line.actual), "\n")) 80 } 81 } 82 83 return fmt.Sprintf("[%s]", log) 84 } 85 86 // ic is the prefix of a URL fragment for a Github comment. 87 // The URL fragment has the following format: 88 // 89 // issuecomment-#id 90 // 91 // We use #id in order to figure out the event-GUID for a 92 // comment and trace comments across prow. 93 const ic = "issuecomment" 94 95 func handleTrace(selector string, kc *kube.Client) http.HandlerFunc { 96 return func(w http.ResponseWriter, r *http.Request) { 97 logrus.Info("Started handling request") 98 start := time.Now() 99 w.Header().Set("Cache-Control", "no-cache") 100 w.Header().Set("Access-Control-Allow-Origin", "*") 101 102 if err := validateTraceRequest(r); err != nil { 103 logrus.Infof("Invalid request: %v", err) 104 http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest) 105 return 106 } 107 108 targets, err := getPods(selector, kc) 109 if err != nil { 110 http.Error(w, err.Error(), http.StatusBadGateway) 111 return 112 } 113 if len(targets) == 0 { 114 logrus.Info("No targets found.") 115 fmt.Fprint(w, "[]") 116 return 117 } 118 119 // Return the logs from the found targets. 120 log := getPodLogs(kc, targets, r) 121 // Filter further if issuecomment has been provided. 122 fmt.Fprint(w, postFilter(log, r.URL.Query().Get(ic))) 123 logrus.Info("Finished handling request") 124 logrus.Infof("Sync time: %v", time.Since(start)) 125 } 126 } 127 128 func validateTraceRequest(r *http.Request) error { 129 icID := r.URL.Query().Get(ic) 130 pr := r.URL.Query().Get(github.PrLogField) 131 repo := r.URL.Query().Get(github.RepoLogField) 132 org := r.URL.Query().Get(github.OrgLogField) 133 eventGUID := r.URL.Query().Get(github.EventGUID) 134 135 if (pr == "" || repo == "" || org == "") && eventGUID == "" && icID == "" { 136 return fmt.Errorf("need either %q, %q, and %q, or %q, or %q to be specified", 137 github.PrLogField, github.RepoLogField, github.OrgLogField, github.EventGUID, ic) 138 } 139 if icID != "" && eventGUID != "" { 140 return fmt.Errorf("cannot specify both %s (%s) and %s (%s)", ic, icID, github.EventGUID, eventGUID) 141 } 142 var prNum int 143 if pr != "" { 144 var err error 145 prNum, err = strconv.Atoi(pr) 146 if err != nil { 147 return fmt.Errorf("invalid pr query %q: %v", pr, err) 148 } 149 if prNum < 1 { 150 return fmt.Errorf("invalid pr query %q: needs to be a positive number", pr) 151 } 152 } 153 return nil 154 } 155 156 func getPods(selector string, kc *kube.Client) ([]kube.Pod, error) { 157 pods, err := kc.ListPods(selector) 158 if err != nil { 159 return nil, fmt.Errorf("Cannot list pods with selector %q: %v", selector, err) 160 } 161 var targets []kube.Pod 162 for _, pod := range pods { 163 if pod.Status.Phase != kube.PodRunning { 164 logrus.Warnf("Ignoring pod %q: not in %s phase (phase: %s, reason: %s)", 165 pod.ObjectMeta.Name, kube.PodRunning, pod.Status.Phase, pod.Status.Reason) 166 continue 167 } 168 targets = append(targets, pod) 169 } 170 return targets, nil 171 } 172 173 func getPodLogs(kc *kube.Client, targets []kube.Pod, r *http.Request) linesByTimestamp { 174 var lock sync.Mutex 175 log := make(linesByTimestamp, 0) 176 wg := sync.WaitGroup{} 177 wg.Add(len(targets)) 178 for _, pod := range targets { 179 go func(podName string) { 180 defer wg.Done() 181 podLog, err := getPodLog(podName, kc, r) 182 if err != nil { 183 logrus.Warnf("cannot get logs from %q: %v", podName, err) 184 return 185 } 186 lock.Lock() 187 log = append(log, podLog...) 188 lock.Unlock() 189 }(pod.ObjectMeta.Name) 190 } 191 wg.Wait() 192 return log 193 } 194 195 func getPodLog(podName string, kc *kube.Client, r *http.Request) (linesByTimestamp, error) { 196 pr := r.URL.Query().Get(github.PrLogField) 197 // Error already checked in validateTraceRequest 198 prNum, _ := strconv.Atoi(pr) 199 repo := r.URL.Query().Get(github.RepoLogField) 200 org := r.URL.Query().Get(github.OrgLogField) 201 eventGUID := r.URL.Query().Get(github.EventGUID) 202 203 podLog, err := kc.GetLog(podName) 204 if err != nil { 205 return nil, err 206 } 207 buf := bytes.NewBuffer(podLog) 208 209 log := make(linesByTimestamp, 0) 210 for { 211 line, err := buf.ReadBytes('\n') 212 if err == io.EOF { 213 break 214 } 215 if err != nil { 216 logrus.Debugf("error while reading log line from %q: %v", podName, err) 217 continue 218 } 219 220 var jsonLine lineData 221 if err := json.Unmarshal(line, &jsonLine); err != nil { 222 logrus.Debugf("cannot unmarshal log line from %q (%s): %v", podName, string(line), err) 223 continue 224 } 225 if eventGUID != "" && jsonLine.EventGUID != eventGUID { 226 continue 227 } 228 if pr != "" && jsonLine.PrNum != prNum { 229 continue 230 } 231 if repo != "" && jsonLine.Repo != repo { 232 continue 233 } 234 if org != "" && jsonLine.Org != org { 235 continue 236 } 237 jsonLine.time, err = time.Parse(time.RFC3339, jsonLine.TimeStr) 238 if err != nil { 239 logrus.Debugf("could not parse time format: %v", err) 240 // Continue including this in the output at the expense 241 // of not having it sorted. 242 } 243 log = append(log, podLine{actual: line, unmarshaled: jsonLine}) 244 } 245 return log, nil 246 } 247 248 func postFilter(log linesByTimestamp, icID string) linesByTimestamp { 249 if icID == "" { 250 return log 251 } 252 var icEventGUID string 253 for _, l := range log { 254 if strings.HasSuffix(l.unmarshaled.URL, fmt.Sprintf("%s-%s", ic, icID)) { 255 icEventGUID = l.unmarshaled.EventGUID 256 break 257 } 258 } 259 // No event-GUID was found for the provided issuecomment. 260 // No logs should be returned. 261 if icEventGUID == "" { 262 return linesByTimestamp{} 263 } 264 265 filtered := make(linesByTimestamp, 0) 266 for _, l := range log { 267 if l.unmarshaled.EventGUID != icEventGUID { 268 continue 269 } 270 filtered = append(filtered, l) 271 } 272 return filtered 273 }