Skip to content

Commit c5bde23

Browse files
authored
[7.x] Ensure ECS compliant logging when enabled. (#3829) (#4536)
If `logging.ecs` is set log data in ECS compliant way. closes #3796
1 parent 535cb41 commit c5bde23

File tree

13 files changed

+149
-180
lines changed

13 files changed

+149
-180
lines changed

_meta/beat.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,10 @@ output.elasticsearch:
10581058
# Unix epoch. Defaults to disabled.
10591059
#interval: 0
10601060

1061-
# Set to true to log messages in json format.
1062-
#logging.json: false
1063-
1064-
# Set to true to log with minimal Elastic Common Schema (ECS) fields set.
1065-
# It is recommended to set `logging.json=true` when enabling ECS logging.
1066-
# Defaults to false.
1067-
#logging.ecs: false
1061+
# Set to true, to log messages with minimal required Elastic Common Schema (ECS)
1062+
# information. Recommended to use in combination with `logging.json=true`
1063+
# Defaults to true.
1064+
#logging.ecs: true
10681065

10691066

10701067
#=============================== HTTP Endpoint ===============================

apm-server.docker.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,10 @@ output.elasticsearch:
10581058
# Unix epoch. Defaults to disabled.
10591059
#interval: 0
10601060

1061-
# Set to true to log messages in json format.
1062-
#logging.json: false
1063-
1064-
# Set to true to log with minimal Elastic Common Schema (ECS) fields set.
1065-
# It is recommended to set `logging.json=true` when enabling ECS logging.
1066-
# Defaults to false.
1067-
#logging.ecs: false
1061+
# Set to true, to log messages with minimal required Elastic Common Schema (ECS)
1062+
# information. Recommended to use in combination with `logging.json=true`
1063+
# Defaults to true.
1064+
#logging.ecs: true
10681065

10691066

10701067
#=============================== HTTP Endpoint ===============================

apm-server.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,10 @@ output.elasticsearch:
10581058
# Unix epoch. Defaults to disabled.
10591059
#interval: 0
10601060

1061-
# Set to true to log messages in json format.
1062-
#logging.json: false
1063-
1064-
# Set to true to log with minimal Elastic Common Schema (ECS) fields set.
1065-
# It is recommended to set `logging.json=true` when enabling ECS logging.
1066-
# Defaults to false.
1067-
#logging.ecs: false
1061+
# Set to true, to log messages with minimal required Elastic Common Schema (ECS)
1062+
# information. Recommended to use in combination with `logging.json=true`
1063+
# Defaults to true.
1064+
#logging.ecs: true
10681065

10691066

10701067
#=============================== HTTP Endpoint ===============================

beater/middleware/log_middleware.go

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -34,73 +34,76 @@ import (
3434

3535
// LogMiddleware returns a middleware taking care of logging processing a request in the middleware and the request handler
3636
func LogMiddleware() Middleware {
37-
logger := logp.NewLogger(logs.Request)
3837
return func(h request.Handler) (request.Handler, error) {
39-
4038
return func(c *request.Context) {
41-
var reqID, transactionID, traceID string
4239
start := time.Now()
43-
tx := apm.TransactionFromContext(c.Request.Context())
44-
if tx != nil {
45-
// This request is being traced, grab its IDs to add to logs.
46-
traceContext := tx.TraceContext()
47-
transactionID = traceContext.Span.String()
48-
traceID = traceContext.Trace.String()
49-
reqID = transactionID
50-
} else {
51-
uuid, err := uuid.NewV4()
52-
if err != nil {
53-
id := request.IDResponseErrorsInternal
54-
logger.Errorw(request.MapResultIDToStatus[id].Keyword, "error", err)
55-
c.Result.SetWithError(id, err)
56-
c.Write()
57-
return
58-
}
59-
reqID = uuid.String()
60-
}
61-
62-
reqLogger := logger.With(
63-
"request_id", reqID,
64-
"method", c.Request.Method,
65-
"URL", c.Request.URL,
66-
"content_length", c.Request.ContentLength,
67-
"remote_address", utility.RemoteAddr(c.Request),
68-
"user-agent", c.Request.Header.Get(headers.UserAgent))
69-
70-
if traceID != "" {
71-
reqLogger = reqLogger.With(
72-
"trace.id", traceID,
73-
"transaction.id", transactionID,
74-
)
40+
c.Logger = loggerWithRequestContext(c)
41+
var err error
42+
if c.Logger, err = loggerWithTraceContext(c); err != nil {
43+
id := request.IDResponseErrorsInternal
44+
c.Logger.Error(request.MapResultIDToStatus[id].Keyword, logp.Error(err))
45+
c.Result.SetWithError(id, err)
46+
c.Write()
47+
return
7548
}
76-
77-
c.Logger = reqLogger
7849
h(c)
79-
reqLogger = reqLogger.With("event.duration", time.Since(start))
80-
50+
c.Logger = c.Logger.With("event.duration", time.Since(start))
8151
if c.MultipleWriteAttempts() {
82-
reqLogger.Warn("multiple write attempts")
52+
c.Logger.Warn("multiple write attempts")
8353
}
84-
8554
keyword := c.Result.Keyword
8655
if keyword == "" {
8756
keyword = "handled request"
8857
}
89-
90-
keysAndValues := []interface{}{"response_code", c.Result.StatusCode}
91-
if c.Result.Err != nil {
92-
keysAndValues = append(keysAndValues, "error", c.Result.Err.Error())
93-
}
94-
if c.Result.Stacktrace != "" {
95-
keysAndValues = append(keysAndValues, "stacktrace", c.Result.Stacktrace)
96-
}
97-
58+
c.Logger = loggerWithResult(c)
9859
if c.Result.Failure() {
99-
reqLogger.Errorw(keyword, keysAndValues...)
100-
} else {
101-
reqLogger.Infow(keyword, keysAndValues...)
60+
c.Logger.Error(keyword)
61+
return
10262
}
103-
63+
c.Logger.Info(keyword)
10464
}, nil
10565
}
10666
}
67+
68+
func loggerWithRequestContext(c *request.Context) *logp.Logger {
69+
logger := logp.NewLogger(logs.Request).With(
70+
"url.original", c.Request.URL.String(),
71+
"http.request.method", c.Request.Method,
72+
"user_agent.original", c.Request.Header.Get(headers.UserAgent),
73+
"source.address", utility.RemoteAddr(c.Request))
74+
if c.Request.ContentLength != -1 {
75+
logger = logger.With("http.request.body.bytes", c.Request.ContentLength)
76+
}
77+
return logger
78+
}
79+
80+
func loggerWithTraceContext(c *request.Context) (*logp.Logger, error) {
81+
tx := apm.TransactionFromContext(c.Request.Context())
82+
if tx == nil {
83+
uuid, err := uuid.NewV4()
84+
if err != nil {
85+
return c.Logger, err
86+
}
87+
return c.Logger.With("http.request.id", uuid.String()), nil
88+
}
89+
// This request is being traced, grab its IDs to add to logs.
90+
traceContext := tx.TraceContext()
91+
transactionID := traceContext.Span.String()
92+
return c.Logger.With(
93+
"trace.id", traceContext.Trace.String(),
94+
"transaction.id", transactionID,
95+
"http.request.id", transactionID,
96+
), nil
97+
}
98+
99+
func loggerWithResult(c *request.Context) *logp.Logger {
100+
logger := c.Logger.With(
101+
"http.response.status_code", c.Result.StatusCode)
102+
if c.Result.Err != nil {
103+
logger = logger.With("error.message", c.Result.Err.Error())
104+
}
105+
if c.Result.Stacktrace != "" {
106+
logger = logger.With("error.stack_trace", c.Result.Stacktrace)
107+
}
108+
return logger
109+
}

beater/middleware/log_middleware_test.go

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ import (
2121
"net/http"
2222
"testing"
2323

24-
"github.com/pkg/errors"
2524
"github.com/stretchr/testify/assert"
2625
"github.com/stretchr/testify/require"
2726
"go.uber.org/zap/zapcore"
2827

2928
"go.elastic.co/apm"
3029
"go.elastic.co/apm/apmtest"
3130

31+
"github.com/elastic/beats/v7/libbeat/common"
3232
"github.com/elastic/beats/v7/libbeat/logp"
33+
"github.com/elastic/beats/v7/libbeat/logp/configure"
3334

3435
"github.com/elastic/apm-server/beater/beatertest"
3536
"github.com/elastic/apm-server/beater/headers"
@@ -38,31 +39,30 @@ import (
3839
)
3940

4041
func TestLogMiddleware(t *testing.T) {
41-
err := logp.DevelopmentSetup(logp.ToObserverOutput())
42-
require.NoError(t, err)
4342

4443
testCases := []struct {
4544
name, message string
4645
level zapcore.Level
4746
handler request.Handler
4847
code int
49-
error error
50-
stacktrace bool
5148
traced bool
49+
ecsKeys []string
5250
}{
5351
{
5452
name: "Accepted",
5553
message: "request accepted",
5654
level: zapcore.InfoLevel,
5755
handler: beatertest.Handler202,
5856
code: http.StatusAccepted,
57+
ecsKeys: []string{"url.original"},
5958
},
6059
{
6160
name: "Traced",
6261
message: "request accepted",
6362
level: zapcore.InfoLevel,
6463
handler: beatertest.Handler202,
6564
code: http.StatusAccepted,
65+
ecsKeys: []string{"url.original", "trace.id", "transaction.id"},
6666
traced: true,
6767
},
6868
{
@@ -71,16 +71,15 @@ func TestLogMiddleware(t *testing.T) {
7171
level: zapcore.ErrorLevel,
7272
handler: beatertest.Handler403,
7373
code: http.StatusForbidden,
74-
error: errors.New("forbidden request"),
74+
ecsKeys: []string{"url.original", "error.message"},
7575
},
7676
{
77-
name: "Panic",
78-
message: "internal error",
79-
level: zapcore.ErrorLevel,
80-
handler: Apply(RecoverPanicMiddleware(), beatertest.HandlerPanic),
81-
code: http.StatusInternalServerError,
82-
error: errors.New("panic on Handle"),
83-
stacktrace: true,
77+
name: "Panic",
78+
message: "internal error",
79+
level: zapcore.ErrorLevel,
80+
handler: Apply(RecoverPanicMiddleware(), beatertest.HandlerPanic),
81+
code: http.StatusInternalServerError,
82+
ecsKeys: []string{"url.original", "error.message", "error.stack_trace"},
8483
},
8584
{
8685
name: "Error without keyword",
@@ -90,12 +89,19 @@ func TestLogMiddleware(t *testing.T) {
9089
c.Result.StatusCode = http.StatusForbidden
9190
c.Write()
9291
},
93-
code: http.StatusForbidden,
92+
code: http.StatusForbidden,
93+
ecsKeys: []string{"url.original"},
9494
},
9595
}
9696

9797
for _, tc := range testCases {
9898
t.Run(tc.name, func(t *testing.T) {
99+
// log setup
100+
configure.Logging("APM Server test",
101+
common.MustNewConfigFrom(`{"ecs":true}`))
102+
require.NoError(t, logp.DevelopmentSetup(logp.ToObserverOutput()))
103+
104+
// prepare and record request
99105
c, rec := beatertest.DefaultContextWithResponseRecorder()
100106
c.Request.Header.Set(headers.UserAgent, tc.name)
101107
if tc.traced {
@@ -105,39 +111,27 @@ func TestLogMiddleware(t *testing.T) {
105111
}
106112
Apply(LogMiddleware(), tc.handler)(c)
107113

114+
// check log lines
108115
assert.Equal(t, tc.code, rec.Code)
109-
for i, entry := range logp.ObserverLogs().TakeAll() {
110-
// expect only one log entry per request
111-
assert.Equal(t, i, 0)
112-
assert.Equal(t, logs.Request, entry.LoggerName)
113-
assert.Equal(t, tc.level, entry.Level)
114-
assert.Equal(t, tc.message, entry.Message)
116+
entries := logp.ObserverLogs().TakeAll()
117+
require.Equal(t, 1, len(entries))
118+
entry := entries[0]
119+
assert.Equal(t, logs.Request, entry.LoggerName)
120+
assert.Equal(t, tc.level, entry.Level)
121+
assert.Equal(t, tc.message, entry.Message)
115122

116-
ec := entry.ContextMap()
117-
t.Logf("context map: %v", ec)
118-
119-
assert.NotEmpty(t, ec["request_id"])
120-
assert.NotEmpty(t, ec["method"])
121-
assert.Equal(t, c.Request.URL.String(), ec["URL"])
122-
assert.NotEmpty(t, ec["remote_address"])
123-
assert.Contains(t, ec, "event.duration")
124-
assert.Equal(t, c.Request.Header.Get(headers.UserAgent), ec["user-agent"])
125-
// zap encoded type
126-
assert.Equal(t, tc.code, int(ec["response_code"].(int64)))
127-
if tc.error != nil {
128-
assert.Equal(t, tc.error.Error(), ec["error"])
129-
}
130-
if tc.stacktrace {
131-
assert.NotZero(t, ec["stacktrace"])
132-
}
133-
if tc.traced {
134-
assert.NotEmpty(t, ec, "trace.id")
135-
assert.NotEmpty(t, ec, "transaction.id")
136-
assert.Equal(t, ec["request_id"], ec["transaction.id"])
137-
} else {
138-
assert.NotContains(t, ec, "trace.id")
139-
assert.NotContains(t, ec, "transaction.id")
140-
}
123+
encoder := zapcore.NewMapObjectEncoder()
124+
ec := common.MapStr{}
125+
for _, f := range entry.Context {
126+
f.AddTo(encoder)
127+
ec.DeepUpdate(encoder.Fields)
128+
}
129+
keys := []string{"http.request.id", "http.request.method", "http.request.body.bytes",
130+
"source.address", "user_agent.original", "http.response.status_code", "event.duration"}
131+
keys = append(keys, tc.ecsKeys...)
132+
for _, key := range keys {
133+
ok, _ := ec.HasKey(key)
134+
assert.True(t, ok, key)
141135
}
142136
})
143137
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ var libbeatConfigOverrides = []cfgfile.ConditionalOverride{{
4848
"metrics": map[string]interface{}{
4949
"enabled": false,
5050
},
51+
"ecs": true,
5152
},
5253
}),
5354
}}

docs/copied-from-beats/docs/loggingconfig.asciidoc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,7 @@ When true, logs messages in JSON format. The default is false.
249249
[float]
250250
==== `logging.ecs`
251251

252-
When true, logs messages with minimal required Elastic Common Schema (ECS)
253-
information.
252+
When true, logs messages in Elastic Common Schema (ECS) compliant format.
254253

255254
ifndef::serverless[]
256255
[float]

systemtest/export_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func TestExportConfigDefaults(t *testing.T) {
4949

5050
expectedConfig := strings.ReplaceAll(`
5151
logging:
52+
ecs: true
5253
metrics:
5354
enabled: false
5455
path:
@@ -69,6 +70,7 @@ func TestExportConfigOverrideDefaults(t *testing.T) {
6970

7071
expectedConfig := strings.ReplaceAll(`
7172
logging:
73+
ecs: true
7274
metrics:
7375
enabled: true
7476
path:

systemtest/logging_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ func TestAPMServerRequestLoggingValid(t *testing.T) {
6969

7070
srv.Close()
7171
for _, entry := range srv.Logs.All() {
72-
if entry.Logger == "request" && entry.Fields["URL"] == "/intake/v2/events" {
73-
statusCode, _ := entry.Fields["response_code"].(float64)
72+
if entry.Logger == "request" && entry.Fields["url.original"] == "/intake/v2/events" {
73+
statusCode, _ := entry.Fields["http.response.status_code"].(float64)
7474
logEntries = append(logEntries, entry)
7575
requestEntries = append(requestEntries, requestEntry{
7676
level: entry.Level,
@@ -95,8 +95,8 @@ func TestAPMServerRequestLoggingValid(t *testing.T) {
9595
}}, requestEntries)
9696

9797
assert.NotContains(t, logEntries[0].Fields, "error")
98-
assert.Regexp(t, "validation error: 'transaction' required", logEntries[1].Fields["error"])
99-
assert.Equal(t, "event exceeded the permitted size.", logEntries[2].Fields["error"])
98+
assert.Regexp(t, "validation error: 'transaction' required", logEntries[1].Fields["error.message"])
99+
assert.Equal(t, "event exceeded the permitted size.", logEntries[2].Fields["error.message"])
100100
}
101101

102102
// validMetadataJSON returns valid JSON-encoded metadata,

0 commit comments

Comments
 (0)