github.com/docker/compose-on-kubernetes@v0.5.0/internal/registry/logstreamer.go (about)

     1  package registry
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"regexp"
     9  	"strconv"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  	apiv1 "k8s.io/api/core/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/runtime/schema"
    18  	"k8s.io/apimachinery/pkg/watch"
    19  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    20  	restclient "k8s.io/client-go/rest"
    21  )
    22  
    23  type chanStream struct {
    24  	name     string
    25  	c        chan []byte
    26  	closer   *signaler
    27  	watching map[string]bool
    28  	nWatch   int64
    29  }
    30  
    31  type logStreamer struct {
    32  	config    *restclient.Config
    33  	namespace string
    34  	name      string
    35  }
    36  
    37  func (s *logStreamer) GetObjectKind() schema.ObjectKind {
    38  	return schema.EmptyObjectKind
    39  }
    40  
    41  func (s *logStreamer) DeepCopyObject() runtime.Object {
    42  	panic("DeepCopyObject not implemented for logStreamer")
    43  }
    44  
    45  // getPods stream logs in non-follow mode for existing pods of the stack
    46  func (s *logStreamer) getPods(cs *chanStream, core corev1.PodsGetter, tail *int64) error {
    47  	pods, err := core.Pods(s.namespace).List(
    48  		metav1.ListOptions{
    49  			LabelSelector: "com.docker.stack.namespace=" + s.name,
    50  		})
    51  	if err != nil {
    52  		return err
    53  	}
    54  	for _, pod := range pods.Items {
    55  		s.streamLogs(cs, core, pod.Name, false, tail)
    56  	}
    57  	return nil
    58  }
    59  
    60  // watchPods stream logs for pods of the stack, watching for new pods
    61  func (s *logStreamer) watchPods(cs *chanStream, podWatcher watch.Interface, core corev1.PodsGetter, tail *int64) {
    62  	abort := cs.closer.Channel()
    63  	for {
    64  		select {
    65  		case <-abort:
    66  			log.Debugf("Stopping pod watcher for log stream of %s/%s", s.namespace, s.name)
    67  			podWatcher.Stop()
    68  			return
    69  		case ev := <-podWatcher.ResultChan():
    70  			if ev.Type != watch.Added && ev.Type != watch.Modified {
    71  				continue
    72  			}
    73  			pod := ev.Object.(*apiv1.Pod)
    74  			if pod.Status.Phase != apiv1.PodRunning {
    75  				continue
    76  			}
    77  			if cs.watching[pod.Name] {
    78  				continue
    79  			}
    80  			log.Debugf("Adding pod %s to log stream of %s/%s", pod.Name, s.namespace, s.name)
    81  			s.streamLogs(cs, core, pod.Name, true, tail)
    82  		}
    83  	}
    84  }
    85  
    86  func (s *logStreamer) streamLogs(cs *chanStream, core corev1.PodsGetter, podName string, follow bool, tail *int64) {
    87  	logreader, err := core.Pods(s.namespace).GetLogs(podName, &apiv1.PodLogOptions{
    88  		Follow:    follow,
    89  		TailLines: tail,
    90  	}).Stream()
    91  	if err != nil {
    92  		log.Errorf("Failed to get pod %s/%s logs: %s", s.namespace, podName, err)
    93  		return
    94  	}
    95  	cs.watching[podName] = true
    96  	atomic.AddInt64(&cs.nWatch, 1)
    97  	go func() {
    98  		log.Debugf("Entering log reader goroutine for %s", podName)
    99  		id := cs.closer.Register(func() { logreader.Close() })
   100  		defer cs.closer.Unregister(id)
   101  		in := bufio.NewReader(logreader)
   102  		for {
   103  			input, err := in.ReadSlice('\n')
   104  			if err != nil {
   105  				log.Debugf("Read error on pod logs %s: %s", podName, err)
   106  				break
   107  			}
   108  			cs.c <- append([]byte(podName+" "), input...)
   109  		}
   110  		log.Debugf("Exiting log reader goroutine for %s", podName)
   111  		atomic.AddInt64(&cs.nWatch, -1)
   112  		cs.c <- nil // wake up forwarder goroutine so that it can close the stream if we were the last
   113  	}()
   114  }
   115  
   116  func parseArgs(in *http.Request) (bool, *int64, *regexp.Regexp, error) {
   117  	sFollow := in.FormValue("follow")
   118  	sTail := in.FormValue("tail")
   119  	sFilter := in.FormValue("filter")
   120  	follow := sFollow != "" && sFollow != "0" && sFollow != "false"
   121  	var tail *int64
   122  	if sTail != "" {
   123  		itail, err := strconv.Atoi(sTail)
   124  		if err != nil {
   125  			return false, nil, nil, err
   126  		}
   127  		i64tail := int64(itail)
   128  		tail = &i64tail
   129  	}
   130  	if sFilter != "" {
   131  		re, err := regexp.Compile(sFilter)
   132  		return follow, tail, re, err
   133  	}
   134  	return follow, tail, nil, nil
   135  }
   136  
   137  func forwardLogs(cs *chanStream, follow bool, filter *regexp.Regexp, out io.Writer) { //nolint:gocyclo
   138  	onClose := out.(http.CloseNotifier).CloseNotify()
   139  	needFlush := false
   140  	// time.After() is only GCed after timer expiration, so don't create one every iteration
   141  	var flusher <-chan time.Time
   142  loop:
   143  	for {
   144  		if flusher == nil && needFlush {
   145  			flusher = time.After(2 * time.Second)
   146  		}
   147  		select {
   148  		case <-flusher: // reading from a nil chan is legal
   149  			if streamFlusher, ok := out.(http.Flusher); ok {
   150  				streamFlusher.Flush()
   151  			}
   152  			needFlush = false
   153  			flusher = nil
   154  		case <-onClose:
   155  			log.Debugf("EOF on log output stream for %s, terminating", cs.name)
   156  			cs.closer.Signal()
   157  			break loop
   158  		case line := <-cs.c:
   159  			if line != nil && (filter == nil || filter.Match(line)) {
   160  				_, err := out.Write(line)
   161  				if err != nil {
   162  					log.Debugf("Write error on log output stream for %s, terminating: %s", cs.name, err)
   163  					cs.closer.Signal()
   164  					break loop
   165  				}
   166  				if follow {
   167  					needFlush = true
   168  				}
   169  			}
   170  			if !follow && atomic.LoadInt64(&cs.nWatch) == 0 {
   171  				// drain before exiting
   172  				for {
   173  					select {
   174  					case line := <-cs.c:
   175  						if line != nil && (filter == nil || filter.Match(line)) {
   176  							out.Write(line)
   177  						}
   178  					default:
   179  						return
   180  					}
   181  				}
   182  			}
   183  		}
   184  	}
   185  }
   186  
   187  func (s *logStreamer) ServeHTTP(out http.ResponseWriter, in *http.Request) {
   188  	follow, tail, filter, err := parseArgs(in)
   189  	if err != nil {
   190  		fmt.Fprintf(out, "Argument parse error: %s\n", err)
   191  		return
   192  	}
   193  	log.Infof("Processing log request for %s/%s follow=%v tail=%v", s.namespace, s.name, follow, tail)
   194  	c := make(chan []byte, 100)
   195  	cs := &chanStream{fmt.Sprintf("%s/%s", s.namespace, s.name), c, newSignaler(), make(map[string]bool), 0}
   196  	core, err := corev1.NewForConfig(s.config)
   197  	if err != nil {
   198  		fmt.Fprintf(out, "Failed to get corev1 interface: %s\n", err)
   199  		return
   200  	}
   201  	if follow {
   202  		podWatcher, err := core.Pods(s.namespace).Watch(
   203  			metav1.ListOptions{
   204  				LabelSelector: "com.docker.stack.namespace=" + s.name,
   205  			})
   206  		if err != nil {
   207  			fmt.Fprintf(out, "Failed to watch pods: %s\n", err)
   208  			return
   209  		}
   210  		go s.watchPods(cs, podWatcher, core, tail)
   211  	} else {
   212  		err := s.getPods(cs, core, tail)
   213  		if err != nil {
   214  			fmt.Fprintf(out, "Failed to get pods: %s\n", err)
   215  			return
   216  		}
   217  		if atomic.LoadInt64(&cs.nWatch) == 0 {
   218  			fmt.Fprintf(out, "Stack %s/%s does not exist or has no running pods\n", s.namespace, s.name)
   219  			return
   220  		}
   221  	}
   222  	forwardLogs(cs, follow, filter, out)
   223  }