|
1 | 1 | package logs
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bytes" |
| 5 | + "fmt" |
4 | 6 | "log"
|
| 7 | + "log/slog" |
5 | 8 | "os"
|
| 9 | + "strings" |
| 10 | + |
| 11 | + "github.com/spf13/pflag" |
| 12 | + "k8s.io/apimachinery/pkg/util/runtime" |
| 13 | + "k8s.io/apimachinery/pkg/util/sets" |
| 14 | + "k8s.io/component-base/featuregate" |
| 15 | + "k8s.io/component-base/logs" |
| 16 | + logsapi "k8s.io/component-base/logs/api/v1" |
| 17 | + _ "k8s.io/component-base/logs/json/register" |
| 18 | +) |
| 19 | + |
| 20 | +// venafi-kubernetes-agent follows [Kubernetes Logging Conventions] and writes |
| 21 | +// logs in [Kubernetes text logging format] by default. It does not support |
| 22 | +// named levels (aka. severity), instead it uses arbitrary levels. Errors and |
| 23 | +// warnings are logged to stderr and Info messages to stdout, because that is |
| 24 | +// how some cloud logging systems (notably Google Cloud Logs Explorer) assign a |
| 25 | +// severity (INFO or ERROR) in the UI. The agent's and vcert's logs are written |
| 26 | +// logged as Info messages with level=0. |
| 27 | +// |
| 28 | +// Further reading: |
| 29 | +// - [Kubernetes logging conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md) |
| 30 | +// - [Kubernetes text logging format](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#text-logging-format) |
| 31 | +// - [Why not named levels, like Info/Warning/Error?](https://github.com/go-logr/logr?tab=readme-ov-file#why-not-named-levels-like-infowarningerror) |
| 32 | +// - [GKE logs best practices](https://cloud.google.com/kubernetes-engine/docs/concepts/about-logs#best_practices) |
| 33 | +// - [Structured Logging KEP](https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/1602-structured-logging/README.md) |
| 34 | +// - [Examples of using k8s.io/component-base/logs](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/component-base/logs/example), |
| 35 | +// upon which this code was based. |
| 36 | + |
| 37 | +var ( |
| 38 | + // This is the Agent's logger. For now, it is still a *log.Logger, but we |
| 39 | + // mean to migrate everything to slog with the klog backend. We avoid using |
| 40 | + // log.Default because log.Default is already used by the VCert library, and |
| 41 | + // we need to keep the agent's logger from the VCert's logger to be able to |
| 42 | + // remove the `vCert: ` prefix from the VCert logs. |
| 43 | + Log *log.Logger |
| 44 | + |
| 45 | + // All but the essential logging flags will be hidden to avoid overwhelming |
| 46 | + // the user. The hidden flags can still be used. For example if a user does |
| 47 | + // not like the split-stream behavior and a Venafi field engineer can |
| 48 | + // instruct them to patch --log-json-split-stream=false on to the Deployment |
| 49 | + // arguments. |
| 50 | + visibleFlagNames = sets.New[string]("v", "vmodule", "logging-format") |
| 51 | + // This default logging configuration will be updated with values from the |
| 52 | + // logging flags, even those that are hidden. |
| 53 | + configuration = logsapi.NewLoggingConfiguration() |
| 54 | + // Logging features will be added to this feature gate, but the |
| 55 | + // feature-gates flag will be hidden from the user. |
| 56 | + features = featuregate.NewFeatureGate() |
6 | 57 | )
|
7 | 58 |
|
8 |
| -var Log = log.New(os.Stderr, "", log.LstdFlags) |
| 59 | +func init() { |
| 60 | + runtime.Must(logsapi.AddFeatureGates(features)) |
| 61 | + // Turn on ALPHA options to enable the split-stream logging options. |
| 62 | + runtime.Must(features.OverrideDefault(logsapi.LoggingAlphaOptions, true)) |
| 63 | +} |
| 64 | + |
| 65 | +// AddFlags adds log related flags to the supplied flag set. |
| 66 | +// |
| 67 | +// The split-stream options are enabled by default, so that errors are logged to |
| 68 | +// stderr and info to stdout, allowing cloud logging systems to assign a |
| 69 | +// severity INFO or ERROR to the messages. |
| 70 | +func AddFlags(fs *pflag.FlagSet) { |
| 71 | + var tfs pflag.FlagSet |
| 72 | + logsapi.AddFlags(configuration, &tfs) |
| 73 | + features.AddFlag(&tfs) |
| 74 | + tfs.VisitAll(func(f *pflag.Flag) { |
| 75 | + if !visibleFlagNames.Has(f.Name) { |
| 76 | + tfs.MarkHidden(f.Name) |
| 77 | + } |
| 78 | + |
| 79 | + // The original usage string includes details about how |
| 80 | + // JSON logging is only available when BETA logging features are |
| 81 | + // enabled, but that's not relevant here because the feature is enabled |
| 82 | + // by default. |
| 83 | + if f.Name == "logging-format" { |
| 84 | + f.Usage = `Sets the log format. Permitted formats: "json", "text".` |
| 85 | + } |
| 86 | + if f.Name == "log-text-split-stream" { |
| 87 | + f.DefValue = "true" |
| 88 | + runtime.Must(f.Value.Set("true")) |
| 89 | + } |
| 90 | + if f.Name == "log-json-split-stream" { |
| 91 | + f.DefValue = "true" |
| 92 | + runtime.Must(f.Value.Set("true")) |
| 93 | + } |
| 94 | + |
| 95 | + // Since `--v` (which is the long form of `-v`) isn't the standard in |
| 96 | + // our projects (it only exists in cert-manager, webhook, and such), |
| 97 | + // let's rename it to the more common `--log-level`, which appears in |
| 98 | + // openshift-routes, csi-driver, trust-manager, and approver-policy. |
| 99 | + // More details at: |
| 100 | + // https://github.com/jetstack/jetstack-secure/pull/596#issuecomment-2421708181 |
| 101 | + if f.Name == "v" { |
| 102 | + f.Name = "log-level" |
| 103 | + f.Shorthand = "v" |
| 104 | + } |
| 105 | + }) |
| 106 | + fs.AddFlagSet(&tfs) |
| 107 | +} |
| 108 | + |
| 109 | +// Initialize uses k8s.io/component-base/logs, to configure the following global |
| 110 | +// loggers: log, slog, and klog. All are configured to write in the same format. |
| 111 | +func Initialize() { |
| 112 | + // This configures the global logger in klog *and* slog, if compiled with Go |
| 113 | + // >= 1.21. |
| 114 | + logs.InitLogs() |
| 115 | + if err := logsapi.ValidateAndApply(configuration, features); err != nil { |
| 116 | + fmt.Fprintf(os.Stderr, "Error in logging configuration: %v\n", err) |
| 117 | + os.Exit(2) |
| 118 | + } |
| 119 | + |
| 120 | + // Thanks to logs.InitLogs, slog.Default now uses klog as its backend. Thus, |
| 121 | + // the client-go library, which relies on klog.Info, has the same logger as |
| 122 | + // the agent, which still uses log.Printf. |
| 123 | + slog := slog.Default() |
| 124 | + |
| 125 | + Log = &log.Logger{} |
| 126 | + Log.SetOutput(LogToSlogWriter{Slog: slog, Source: "agent"}) |
| 127 | + |
| 128 | + // Let's make sure the VCert library, which is the only library we import to |
| 129 | + // be using the global log.Default, also uses the common slog logger. |
| 130 | + vcertLog := log.Default() |
| 131 | + vcertLog.SetOutput(LogToSlogWriter{Slog: slog, Source: "vcert"}) |
| 132 | + // This is a work around for a bug in vcert where it adds a `vCert: ` prefix |
| 133 | + // to the global log logger. It can be removed when this is fixed upstream |
| 134 | + // in vcert: https://github.com/Venafi/vcert/pull/512 |
| 135 | + vcertLog.SetPrefix("") |
| 136 | +} |
| 137 | + |
| 138 | +type LogToSlogWriter struct { |
| 139 | + Slog *slog.Logger |
| 140 | + Source string |
| 141 | +} |
| 142 | + |
| 143 | +func (w LogToSlogWriter) Write(p []byte) (n int, err error) { |
| 144 | + // log.Printf writes a newline at the end of the message, so we need to trim |
| 145 | + // it. |
| 146 | + p = bytes.TrimSuffix(p, []byte("\n")) |
| 147 | + |
| 148 | + message := string(p) |
| 149 | + if strings.Contains(message, "error") || |
| 150 | + strings.Contains(message, "failed") || |
| 151 | + strings.Contains(message, "fatal") || |
| 152 | + strings.Contains(message, "Failed") || |
| 153 | + strings.Contains(message, "While evaluating configuration") || |
| 154 | + strings.Contains(message, "data-path override present") || |
| 155 | + strings.Contains(message, "Cannot marshal readings") { |
| 156 | + w.Slog.With("source", w.Source).Error(message) |
| 157 | + } else { |
| 158 | + w.Slog.With("source", w.Source).Info(message) |
| 159 | + } |
| 160 | + return len(p), nil |
| 161 | +} |
0 commit comments