// Package kubernetes taken from https://github.com/micro/go-micro/blob/master/debug/log/kubernetes/kubernetes.go // There are some modifications compared to the other files as // this package doesn't provide write functionality. // With the write functionality gone, structured logs also go away. package kubernetes import ( "bufio" "strconv" "time" "github.com/asim/go-micro/v3/runtime" "github.com/asim/go-micro/v3/util/kubernetes/client" "github.com/asim/go-micro/v3/util/log" ) type klog struct { client client.Client serviceName string options runtime.LogsOptions } func (k *klog) podLogStream(podName string, stream *kubeStream) error { p := make(map[string]string) p["follow"] = "true" opts := []client.LogOption{ client.LogParams(p), client.LogNamespace(k.options.Namespace), } // get the logs for the pod body, err := k.client.Log(&client.Resource{ Name: podName, Kind: "pod", }, opts...) if err != nil { stream.err = err stream.Stop() return err } s := bufio.NewScanner(body) defer body.Close() for { select { case <-stream.stop: return stream.Error() default: if s.Scan() { record := runtime.LogRecord{ Message: s.Text(), } stream.stream <- record } else { // TODO: is there a blocking call // rather than a sleep loop? time.Sleep(time.Second) } } } } func (k *klog) getMatchingPods() ([]string, error) { r := &client.Resource{ Kind: "pod", Value: new(client.PodList), } l := make(map[string]string) l["name"] = client.Format(k.serviceName) // TODO: specify micro:service // l["micro"] = "service" opts := []client.GetOption{ client.GetLabels(l), client.GetNamespace(k.options.Namespace), } if err := k.client.Get(r, opts...); err != nil { return nil, err } var matches []string for _, p := range r.Value.(*client.PodList).Items { // find labels that match the name if p.Metadata.Labels["name"] == client.Format(k.serviceName) { matches = append(matches, p.Metadata.Name) } } return matches, nil } func (k *klog) Read() ([]runtime.LogRecord, error) { pods, err := k.getMatchingPods() if err != nil { return nil, err } var records []runtime.LogRecord for _, pod := range pods { logParams := make(map[string]string) //if !opts.Since.Equal(time.Time{}) { // logParams["sinceSeconds"] = strconv.Itoa(int(time.Since(opts.Since).Seconds())) //} if k.options.Count != 0 { logParams["tailLines"] = strconv.Itoa(int(k.options.Count)) } if k.options.Stream { logParams["follow"] = "true" } opts := []client.LogOption{ client.LogParams(logParams), client.LogNamespace(k.options.Namespace), } logs, err := k.client.Log(&client.Resource{ Name: pod, Kind: "pod", }, opts...) if err != nil { return nil, err } defer logs.Close() s := bufio.NewScanner(logs) for s.Scan() { record := runtime.LogRecord{ Message: s.Text(), } // record.Metadata["pod"] = pod records = append(records, record) } } // sort the records // sort.Slice(records, func(i, j int) bool { return records[i].Timestamp.Before(records[j].Timestamp) }) return records, nil } func (k *klog) Stream() (runtime.LogStream, error) { // find the matching pods pods, err := k.getMatchingPods() if err != nil { return nil, err } stream := &kubeStream{ stream: make(chan runtime.LogRecord), stop: make(chan bool), } // stream from the individual pods for _, pod := range pods { go func(podName string) { err := k.podLogStream(podName, stream) if err != nil { log.Errorf("Error streaming from pod: %v", err) } }(pod) } return stream, nil } // NewLog returns a configured Kubernetes logger func newLog(c client.Client, serviceName string, opts ...runtime.LogsOption) *klog { options := runtime.LogsOptions{ Namespace: client.DefaultNamespace, } for _, o := range opts { o(&options) } klog := &klog{ serviceName: serviceName, client: c, options: options, } return klog }