linkerd2/cli/cmd/logs.go

276 lines
8.1 KiB
Go

package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"regexp"
"text/template"
"time"
"github.com/fatih/color"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/spf13/cobra"
"github.com/wercker/stern/stern"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
//This code replicates most of the functionality in https://github.com/wercker/stern/blob/master/cmd/cli.go
type logCmdConfig struct {
clientset kubernetes.Interface
*stern.Config
}
type logsOptions struct {
container string
controlPlaneComponent string
noColor bool
sinceSeconds time.Duration
tail int64
timestamps bool
}
func newLogsOptions() *logsOptions {
return &logsOptions{
container: "",
controlPlaneComponent: "",
noColor: false,
sinceSeconds: 48 * time.Hour,
tail: -1,
timestamps: false,
}
}
func (o *logsOptions) toSternConfig(controlPlaneComponents, availableContainers []string) (*stern.Config, error) {
config := &stern.Config{}
if o.controlPlaneComponent == "" {
config.LabelSelector = labels.Everything()
} else {
var podExists string
for _, p := range controlPlaneComponents {
if p == o.controlPlaneComponent {
podExists = p
break
}
}
if podExists == "" {
return nil, fmt.Errorf("control plane component [%s] does not exist. Must be one of %v", o.controlPlaneComponent, controlPlaneComponents)
}
selector, err := labels.Parse(fmt.Sprintf("linkerd.io/control-plane-component=%s", o.controlPlaneComponent))
if err != nil {
return nil, err
}
config.LabelSelector = selector
}
if o.container != "" {
var matchingContainer string
for _, c := range availableContainers {
if o.container == c {
matchingContainer = c
break
}
}
if matchingContainer == "" {
return nil, fmt.Errorf("container [%s] does not exist in control plane [%s]", o.container, controlPlaneNamespace)
}
}
containerFilterRgx, err := regexp.Compile(o.container)
if err != nil {
return nil, err
}
config.ContainerQuery = containerFilterRgx
if o.tail != -1 {
config.TailLines = &o.tail
}
// Do not use regex to filter pods. Instead, we provide the list of all control plane components and use
// the label selector to filter logs.
podFilterRgx, err := regexp.Compile("")
if err != nil {
return nil, err
}
// Based on stern/cmd/cli.go
t := "{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{.Message}}"
if o.noColor {
t = "{{.PodName}} {{.ContainerName}} {{.Message}}"
}
funs := map[string]interface{}{
"json": func(in interface{}) (string, error) {
b, err := json.Marshal(in)
if err != nil {
return "", err
}
return string(b), nil
},
"color": func(color color.Color, text string) string {
return color.SprintFunc()(text)
},
}
template, err := template.New("log").Funcs(funs).Parse(t)
if err != nil {
return nil, err
}
config.PodQuery = podFilterRgx
config.Since = o.sinceSeconds
config.Timestamps = o.timestamps
config.Namespace = controlPlaneNamespace
config.ContainerState = stern.RUNNING
config.ExcludeContainerQuery = nil
config.Template = template
return config, nil
}
func getControlPlaneComponentsAndContainers(pods *corev1.PodList) ([]string, []string) {
var controlPlaneComponents, containers []string
for _, pod := range pods.Items {
controlPlaneComponents = append(controlPlaneComponents, pod.Labels["linkerd.io/control-plane-component"])
for _, container := range pod.Spec.Containers {
containers = append(containers, container.Name)
}
}
return controlPlaneComponents, containers
}
func newLogCmdConfig(options *logsOptions, kubeconfigPath, kubeContext, impersonate string, impersonateGroup []string) (*logCmdConfig, error) {
kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
if err != nil {
return nil, err
}
podList, err := kubeAPI.CoreV1().Pods(controlPlaneNamespace).List(metav1.ListOptions{})
if err != nil {
return nil, err
}
components, containers := getControlPlaneComponentsAndContainers(podList)
c, err := options.toSternConfig(components, containers)
if err != nil {
return nil, err
}
return &logCmdConfig{
kubeAPI,
c,
}, nil
}
func newCmdLogs() *cobra.Command {
options := newLogsOptions()
cmd := &cobra.Command{
Use: "logs [flags]",
Short: "Tail logs from containers in the Linkerd control plane",
Long: `Tail logs from containers in the Linkerd control plane.`,
Example: ` # Tail logs from all containers in the prometheus control plane component
linkerd logs --control-plane-component prometheus
# Tail logs from the linkerd-proxy container in the grafana control plane component
linkerd logs --control-plane-component grafana --container linkerd-proxy
# Tail logs from the linkerd-proxy container in the controller component beginning with the last two lines
linkerd logs --control-plane-component controller --container linkerd-proxy --tail 2
# Tail logs from the linkerd-proxy container in the controller component showing timestamps for each line
linkerd logs --control-plane-component controller --container linkerd-proxy --timestamps
`,
RunE: func(cmd *cobra.Command, args []string) error {
color.NoColor = options.noColor
opts, err := newLogCmdConfig(options, kubeconfigPath, kubeContext, impersonate, impersonateGroup)
if err != nil {
return err
}
return runLogOutput(opts)
},
}
cmd.PersistentFlags().StringVarP(&options.container, "container", "c", options.container, "Tail logs from the specified container. Options are 'public-api', 'destination', 'tap', 'prometheus', 'grafana' or 'linkerd-proxy'")
cmd.PersistentFlags().StringVar(&options.controlPlaneComponent, "control-plane-component", options.controlPlaneComponent, "Tail logs from the specified control plane component. Default value (empty string) causes this command to tail logs from all resources marked with the 'linkerd.io/control-plane-component' label selector")
cmd.PersistentFlags().BoolVarP(&options.noColor, "no-color", "n", options.noColor, "Disable colorized output") // needed until at least https://github.com/wercker/stern/issues/69 is resolved
cmd.PersistentFlags().DurationVarP(&options.sinceSeconds, "since", "s", options.sinceSeconds, "Duration of how far back logs should be retrieved")
cmd.PersistentFlags().Int64Var(&options.tail, "tail", options.tail, "Last number of log lines to show for a given container. -1 does not show previous log lines")
cmd.PersistentFlags().BoolVarP(&options.timestamps, "timestamps", "t", options.timestamps, "Print timestamps for each given log line")
return cmd
}
func runLogOutput(opts *logCmdConfig) error {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, os.Kill)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
podInterface := opts.clientset.CoreV1().Pods(opts.Namespace)
tails := make(map[string]*stern.Tail)
// This channel serializes all log output.
// It is intended to workaround https://github.com/wercker/stern/issues/96,
// and is based on
// https://github.com/oandrew/stern/commit/8723308e46b408e239ce369ced12706d01479532
logC := make(chan string, 1024)
go func() {
for {
select {
case str := <-logC:
fmt.Fprintf(os.Stdout, str)
case <-ctx.Done():
break
}
}
}()
added, _, err := stern.Watch(
ctx,
podInterface,
opts.PodQuery,
opts.ContainerQuery,
opts.ExcludeContainerQuery,
opts.ContainerState,
opts.LabelSelector,
)
if err != nil {
return err
}
go func() {
for a := range added {
tailOpts := &stern.TailOptions{
SinceSeconds: int64(opts.Since.Seconds()),
Timestamps: opts.Timestamps,
TailLines: opts.TailLines,
Exclude: opts.Exclude,
Include: opts.Include,
Namespace: true,
}
newTail := stern.NewTail(a.Namespace, a.Pod, a.Container, opts.Template, tailOpts)
if _, ok := tails[a.GetID()]; !ok {
tails[a.GetID()] = newTail
}
newTail.Start(ctx, podInterface, logC)
}
}()
<-sigCh
return nil
}