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 }