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  }