Skip to content

Commit

Permalink
enhance(logging): add flag to allow logging in ecs format, map custom…
Browse files Browse the repository at this point in the history
… fields to ecs fields (#971)
  • Loading branch information
KellyMerrick authored Sep 27, 2023
1 parent dff9633 commit f3acd78
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 7 deletions.
9 changes: 6 additions & 3 deletions cmd/vela-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ func main() {
Usage: "set log level - options: (trace|debug|info|warn|error|fatal|panic)",
Value: "info",
},
&cli.StringFlag{
EnvVars: []string{"VELA_LOG_FORMATTER", "LOG_FORMATTER"},
Name: "log-formatter",
Usage: "set log formatter - options: (json|ecs)",
Value: "json",
},
&cli.StringFlag{
EnvVars: []string{"VELA_ADDR", "VELA_HOST"},
Name: "server-addr",
Expand Down Expand Up @@ -247,9 +253,6 @@ func main() {
// Add Source Flags
app.Flags = append(app.Flags, scm.Flags...)

// set logrus to log in JSON format
logrus.SetFormatter(&logrus.JSONFormatter{})

if err = app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/vela-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ import (
)

func server(c *cli.Context) error {
// set log formatter
switch c.String("log-formatter") {
case "json":
// set logrus to log in JSON format
logrus.SetFormatter(&logrus.JSONFormatter{})
case "ecs":
// set logrus to log in Elasticsearch Common Schema (ecs) format
logrus.SetFormatter(&middleware.ECSFormatter{
DataKey: "labels.vela",
})
}

// validate all input
err := validate(c)
if err != nil {
Expand Down
73 changes: 72 additions & 1 deletion router/middleware/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ import (
"github.com/sirupsen/logrus"
)

// This file, in part, reproduces portions of
// https://github.com/elastic/ecs-logging-go-logrus/blob/v1.0.0/formatter.go
// to handle our custom fields in Format().

// ECSFormatter holds ECS parameter information for logging.
type ECSFormatter struct {
// DataKey allows users to put all the log entry parameters into a
// nested dictionary at a given key.
//
// DataKey is ignored for well-defined fields, such as "error",
// which will instead be stored under the appropriate ECS fields.
DataKey string
}

// Logger returns a gin.HandlerFunc (middleware) that logs requests using logrus.
//
// Requests with errors are logged using logrus.Error().
Expand Down Expand Up @@ -98,7 +112,7 @@ func Logger(logger *logrus.Logger, timeFormat string) gin.HandlerFunc {
// Append error field if this is an erroneous request.
entry.Error(c.Errors.String())
} else {
entry.Info()
entry.Infof("%v %v %v %s %s", fields["status"], fields["latency"], fields["ip"], fields["method"], fields["path"])
}
}
}
Expand All @@ -114,3 +128,60 @@ func sanitize(body interface{}) interface{} {

return body
}

// Format formats logrus.Entry as ECS-compliant JSON,
// mapping our custom fields to ECS fields.
func (f *ECSFormatter) Format(e *logrus.Entry) ([]byte, error) {
datahint := len(e.Data)
if f.DataKey != "" {
datahint = 2
}
data := make(logrus.Fields, datahint)
if len(e.Data) > 0 {
extraData := data
if f.DataKey != "" {
extraData = make(logrus.Fields, len(e.Data))
}
for k, v := range e.Data {
switch k {
case "ip":
data["client.ip"] = v
case "latency":
data["event.duration"] = v
case "method":
data["http.request.method"] = v
case "path":
data["url.path"] = v
case "status":
data["http.response.status_code"] = v
case "user-agent":
data["user_agent.name"] = v
case "version":
data["user_agent.version"] = v
default:
extraData[k] = v
}
}
if f.DataKey != "" && len(extraData) > 0 {
data[f.DataKey] = extraData
}
}

// ecsVersion holds the version of ECS with which the formatter is compatible.
data["ecs.version"] = "1.6.0"
ecopy := *e
ecopy.Data = data
e = &ecopy

ecsFieldMap := logrus.FieldMap{
logrus.FieldKeyTime: "@timestamp",
logrus.FieldKeyMsg: "message",
logrus.FieldKeyLevel: "log.level",
}

jf := logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z0700",
FieldMap: ecsFieldMap,
}
return jf.Format(e)
}
64 changes: 61 additions & 3 deletions router/middleware/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ func TestMiddleware_Logger(t *testing.T) {

payload, _ := json.Marshal(`{"foo": "bar"}`)
wantLevel := logrus.InfoLevel
wantMessage := ""

logger, hook := test.NewNullLogger()
defer hook.Reset()
Expand Down Expand Up @@ -106,8 +105,16 @@ func TestMiddleware_Logger(t *testing.T) {
t.Errorf("Logger Level is %v, want %v", gotLevel, wantLevel)
}

if !reflect.DeepEqual(gotMessage, wantMessage) {
t.Errorf("Logger Message is %v, want %v", gotMessage, wantMessage)
if gotMessage == "" {
t.Errorf("Logger Message is %v, want non-empty string", gotMessage)
}

if strings.Contains(gotMessage, "GET") {
t.Errorf("Logger Message is %v, want message to contain GET", gotMessage)
}

if !strings.Contains(gotMessage, "POST") {
t.Errorf("Logger Message is %v, message shouldn't contain POST", gotMessage)
}
}

Expand Down Expand Up @@ -217,3 +224,54 @@ func TestMiddleware_Logger_Sanitize(t *testing.T) {
}
}
}

func TestMiddleware_Format(t *testing.T) {

wantLabels := "labels.vela"

// setup data, fields, and logger
formatter := &ECSFormatter{
DataKey: wantLabels,
}

fields := logrus.Fields{
"ip": "123.4.5.6",
"method": http.MethodGet,
"path": "/foobar",
"latency": 0,
"status": http.StatusOK,
"user-agent": "foobar",
"version": "v1.0.0",
"org": "foo",
}

logger := logrus.NewEntry(logrus.StandardLogger())
entry := logger.WithFields(fields)

got, err := formatter.Format(entry)

fmt.Println("got:", string(got))
// run test
gotLabels := string(formatter.DataKey)

if err != nil {
t.Errorf("Format returned err: %v", err)
}

if got == nil {
t.Errorf("Format returned nothing, want a log")
}

if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("Logger returned %v, want %v", gotLabels, wantLabels)
}

if !strings.Contains(string(got), "GET") {
t.Errorf("Format returned %v, want to contain GET", string(got))
}

if !strings.Contains(string(got), "/foobar") {
t.Errorf("Format returned %v, want to contain GET", string(got))
}

}

0 comments on commit f3acd78

Please sign in to comment.