From c8cea8050f4cba9226749d809c2f5da870ce89d1 Mon Sep 17 00:00:00 2001 From: Marcel Ludwig Date: Thu, 1 Apr 2021 18:41:51 +0200 Subject: [PATCH] Add: json log options: pretty print and parent key Change: access and upstream parent keys can be configured with the new global parent key --- DOCKER.md | 16 +++++++------ config/settings.go | 1 + docs/README.md | 1 + logging/access_log.go | 7 +----- logging/json_color_formatter.go | 40 +++++++++++++++++++++++++++++++++ logging/upstream_log.go | 7 +----- main.go | 32 +++++++++++++++++--------- 7 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 logging/json_color_formatter.go diff --git a/DOCKER.md b/DOCKER.md index 11ec967e3..a654f7cda 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -20,22 +20,24 @@ docker run --rm -p 8080:8080 -v `pwd`:/htdocs avenga/couper | Variable | Default | Description | |--- |--- |--- | | COUPER_CONFIG_FILE | `couper.hcl` | Path to the configuration file. | -| COUPER_LOG_FORMAT | `common` | Can be set to `json` output which is the _container default_. | | COUPER_DEFAULT_PORT | `8080` | Sets the default port to the given value and does not override explicit `[host:port]` configurations from file. | | COUPER_XFH | `false` | Global configurations which uses the `Forwarded-Host` header instead of the request host. | | COUPER_HEALTH_PATH | `/healthz` | Path for health-check requests for all servers and ports. | | COUPER_NO_PROXY_FROM_ENV | `false` | Disables the connect hop to configured [proxy via environment](https://godoc.org/golang.org/x/net/http/httpproxy). | | COUPER_REQUEST_ID_FORMAT | `common` | If set to `uuid4` a rfc4122 uuid is used for `req.id` and related log fields. | | COUPER_SECURE_COOKIES | `""` | If set to `"strip"`, the `Secure` flag is removed from all `Set-Cookie` HTTP header fields. | -| COUPER_ACCESS_LOG_PARENT_FIELD | `""` | An option for `json` log format to add all log fields as child properties. | +| COUPER_TIMING_IDLE_TIMEOUT | `60s` | The maximum amount of time to wait for the next request on client connections when keep-alives are enabled. | +| COUPER_TIMING_READ_HEADER_TIMEOUT | `10s` | The amount of time allowed to read client request headers. | +| COUPER_TIMING_SHUTDOWN_DELAY | `0` | The amount of time the server is marked as unhealthy until calling server close finally. | +| COUPER_TIMING_SHUTDOWN_TIMEOUT | `0` | The maximum amount of time allowed to close the server with all running connections. | +| | | | +| COUPER_LOG_FORMAT | `common` | Can be set to `json` output which is the _container default_. | +| COUPER_LOG_PRETTY | `false` | Global option for `json` log format which pretty prints with basic key coloring. | +| COUPER_LOG_PARENT_FIELD | `""` | An option for `json` log format to add all log fields as child properties. | +| COUPER_LOG_TYPE_VALUE | `couper_daemon` | Value for the runtime log field `type`. | | COUPER_ACCESS_LOG_TYPE_VALUE | `couper_access` | Value for the log field `type`. | | COUPER_ACCESS_LOG_REQUEST_HEADERS | `User-Agent, Accept, Referer` | A comma separated list of header names whose values should be logged. | | COUPER_ACCESS_LOG_RESPONSE_HEADERS | `Cache-Control, Content-Encoding, Content-Type, Location` | A comma separated list of header names whose values should be logged. | -| COUPER_BACKEND_LOG_PARENT_FIELD | `""` | An option for `json` log format to add all log fields as child properties. | | COUPER_BACKEND_LOG_TYPE_VALUE | `couper_backend` | Value for the log field `type`. | | COUPER_BACKEND_LOG_REQUEST_HEADERS | `User-Agent, Accept, Referer` | A comma separated list of header names whose values should be logged. | | COUPER_BACKEND_LOG_RESPONSE_HEADERS | `Cache-Control, Content-Encoding, Content-Type, Location` | A comma separated list of header names whose values should be logged. | -| COUPER_TIMING_IDLE_TIMEOUT | `60s` | The maximum amount of time to wait for the next request on client connections when keep-alives are enabled. | -| COUPER_TIMING_READ_HEADER_TIMEOUT | `10s` | The amount of time allowed to read client request headers. | -| COUPER_TIMING_SHUTDOWN_DELAY | `0` | The amount of time the server is marked as unhealthy until calling server close finally. | -| COUPER_TIMING_SHUTDOWN_TIMEOUT | `0` | The maximum amount of time allowed to close the server with all running connections. | diff --git a/config/settings.go b/config/settings.go index 90cc258ff..9845a71fc 100644 --- a/config/settings.go +++ b/config/settings.go @@ -15,6 +15,7 @@ type Settings struct { DefaultPort int `hcl:"default_port,optional"` HealthPath string `hcl:"health_path,optional"` LogFormat string `hcl:"log_format,optional"` + LogPretty bool `hcl:"log_pretty,optional"` NoProxyFromEnv bool `hcl:"no_proxy_from_env,optional"` RequestIDFormat string `hcl:"request_id_format,optional"` SecureCookies string `hcl:"secure_cookies,optional"` diff --git a/docs/README.md b/docs/README.md index 5345f1821..0459c6fc6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -840,6 +840,7 @@ gateway instance. | `no_proxy_from_env` | Disables the connect hop to configured [proxy via environment](https://godoc.org/golang.org/x/net/http/httpproxy). | `false` | | `default_port` | Port which will be used if not explicitly specified per host within the [`hosts`](#server-block) list | `8080` | | `log_format` | Switch for tab/field based colored view or json log lines | `common` | +| `log_pretty` | Global option for `json` log format which pretty prints with basic key coloring | `false` | | `xfh` | Option to use the `X-Forwarded-Host` header as the request host | `false` | | `request_id_format` | If set to `uuid4` a rfc4122 uuid is used for `req.id` and related log fields | `common` | | `secure_cookies` | If set to `"strip"`, the `Secure` flag is removed from all `Set-Cookie` HTTP header fields. | `""` | diff --git a/logging/access_log.go b/logging/access_log.go index 6d1355b70..3f3edb576 100644 --- a/logging/access_log.go +++ b/logging/access_log.go @@ -136,12 +136,7 @@ func (log *AccessLog) ServeHTTP(rw http.ResponseWriter, req *http.Request, nextH fields["code"] = i } - var entry *logrus.Entry - if log.conf.ParentFieldKey != "" { - entry = log.logger.WithField(log.conf.ParentFieldKey, fields) - } else { - entry = log.logger.WithFields(logrus.Fields(fields)) - } + entry := log.logger.WithFields(logrus.Fields(fields)) entry.Time = startTime if statusRecorder.status == http.StatusInternalServerError || err != nil { diff --git a/logging/json_color_formatter.go b/logging/json_color_formatter.go new file mode 100644 index 000000000..c44d37abe --- /dev/null +++ b/logging/json_color_formatter.go @@ -0,0 +1,40 @@ +package logging + +import ( + "regexp" + + "github.com/fatih/color" + "github.com/sirupsen/logrus" +) + +type JSONColorFormatter struct { + inner *logrus.JSONFormatter +} + +func NewJSONColorFormatter(parent string, pretty bool) logrus.Formatter { + return &JSONColorFormatter{ + inner: &logrus.JSONFormatter{ + DataKey: parent, + FieldMap: logrus.FieldMap{ + logrus.FieldKeyTime: "timestamp", + logrus.FieldKeyMsg: "message", + }, + PrettyPrint: pretty, + }, + } +} + +var keyRegex = regexp.MustCompile(`"(\w+)":`) + +func (jcf *JSONColorFormatter) Format(entry *logrus.Entry) ([]byte, error) { + b, err := jcf.inner.Format(entry) + if !jcf.inner.PrettyPrint || err != nil { + return b, err + } + + result := keyRegex.ReplaceAllFunc(b, func(needle []byte) []byte { + return []byte(color.HiGreenString("%s", string(needle))) + }) + + return result, err +} diff --git a/logging/upstream_log.go b/logging/upstream_log.go index b8b7c324a..470ee0d07 100644 --- a/logging/upstream_log.go +++ b/logging/upstream_log.go @@ -146,12 +146,7 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) { fields["timings"] = timingResults //timings["ttlb"] = roundMS(rtDone.Sub(timeTTFB)) // TODO: depends on stream or buffer - var entry *logrus.Entry - if u.config.ParentFieldKey != "" { - entry = u.log.WithField(u.config.ParentFieldKey, fields) - } else { - entry = u.log.WithFields(logrus.Fields(fields)) - } + entry := u.log.WithFields(logrus.Fields(fields)) entry.Time = startTime if (beresp != nil && beresp.StatusCode == http.StatusInternalServerError) || err != nil { diff --git a/main.go b/main.go index 598befd89..8bb22f97d 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "github.com/avenga/couper/config/configload" "github.com/avenga/couper/config/env" "github.com/avenga/couper/config/runtime" + "github.com/avenga/couper/logging" ) var ( fields = logrus.Fields{ - "type": "couper_daemon", "build": runtime.BuildName, "version": runtime.VersionName, } @@ -46,18 +46,20 @@ func realmain(arguments []string) int { } var filePath, logFormat string + var logPretty bool set := flag.NewFlagSet("global", flag.ContinueOnError) set.StringVar(&filePath, "f", config.DefaultFilename, "-f ./couper.hcl") set.StringVar(&logFormat, "log-format", config.DefaultSettings.LogFormat, "-log-format=common") + set.BoolVar(&logPretty, "log-pretty", config.DefaultSettings.LogPretty, "-log-pretty") err := set.Parse(args.Filter(set)) if err != nil { - newLogger(logFormat).WithFields(fields).Error(err) + newLogger(logFormat, logPretty).WithFields(fields).Error(err) return 1 } confFile, err := configload.LoadFile(filePath) if err != nil { - newLogger(logFormat).WithFields(fields).Error(err) + newLogger(logFormat, logPretty).WithFields(fields).Error(err) return 1 } @@ -66,7 +68,10 @@ func realmain(arguments []string) int { if logFormat != config.DefaultSettings.LogFormat { confFile.Settings.LogFormat = logFormat } - logger := newLogger(confFile.Settings.LogFormat).WithFields(fields) + if logPretty != config.DefaultSettings.LogPretty { + confFile.Settings.LogPretty = logPretty + } + logger := newLogger(confFile.Settings.LogFormat, confFile.Settings.LogPretty).WithFields(fields) wd, err := os.Getwd() if err != nil { @@ -85,7 +90,7 @@ func realmain(arguments []string) int { // newLogger creates a log instance with the configured formatter. // Since the format option may required to be correct in early states // we parse the env configuration on every call. -func newLogger(format string) logrus.FieldLogger { +func newLogger(format string, pretty bool) logrus.FieldLogger { logger := logrus.New() logger.Out = os.Stdout if hook != nil { @@ -93,15 +98,20 @@ func newLogger(format string) logrus.FieldLogger { logger.Out = ioutil.Discard } - settings := &config.Settings{LogFormat: format} + settings := &config.Settings{ + LogFormat: format, + LogPretty: pretty, + } env.Decode(settings) + logConf := &logging.Config{ + TypeFieldKey: "couper_daemon", + } + env.Decode(logConf) + if settings.LogFormat == "json" { - logger.Formatter = &logrus.JSONFormatter{FieldMap: logrus.FieldMap{ - logrus.FieldKeyTime: "timestamp", - logrus.FieldKeyMsg: "message", - }} + logger.SetFormatter(logging.NewJSONColorFormatter(logConf.ParentFieldKey, settings.LogPretty)) } logger.Level = logrus.DebugLevel - return logger + return logger.WithField("type", logConf.TypeFieldKey) }