-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add minimal viable version for ECS encoder for zap. #1
Conversation
Adds minimal required ECS information to every log output. Uses ECS version 1.5.0. related to #elastic/ecs-logging#15
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, but I think we should also consider how errors will be encoded.
By overriding the zapcore.Encoder, we only have limited means of influencing the output structure. The standard zapcore.Core implementation is a bit too opinionated about the structure of errors:
I think we might need to provide a zapcore.Core if we want to format errors according to ECS. This would also give us an opportunity to enrich the errors, i.e. to set error.id, error.code, error.type; and at some point, ideally to set error causes.
@axw thank you for the detailed feedback! Regarding the error handling, I was working on another branch on adding What do you think about this solution? I will also look a bit into providing a full I tried to keep this PR small with a minimum initial support, therefore I created a second branch with more extensive support. |
@simitt I think that'll lead to an unfriendly API, forcing users to create an ECS structure for every error they log. We could provide an logger := zap.New(ecszap.NewCore(os.Stdout, zap.DebugLevel))
...
logger.With(zap.Error(err)).Error("boom") If we didn't format the error objects automatically, then I think users would need to update all logging call sites. (BTW I'm definitely on board with creating an MVP, but this probably affects the public API. If we go the Core route, then I'm not sure it would make sense to expose an encoder at all.) |
@axw I am glad we are having the discussion now, as I fully agree that we should decide on which route to go before merging in the MVP. I'll close this for now, and revisit. |
Add ecszap.Core implementation and make jsonEncoder unexported. This allows for more flexibility when encoding errors. Changes for users are kept to a minimum, mainly concerning the newly introduced configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I think that works pretty well. I don't love having to copy the code, but I suppose it's not the end of the world if we do have to. I noted a possible alternative, but I may have missed something.
It occurs to me that we probably do need to expose the encoder, and make it possible to wrap another Core. I'm thinking of the libbeat/logp, which has Core implementations for Syslog and Windows Event Log. I think we would need to wrap the syslog/eventlog Core with ecszap.Core, and the syslog/eventlog would call the ecszap-provided encoder.
e.g.
cfg := ecszap.NewDefaultEncoderConfig()
encoder := ecszap.NewEncoder(cfg)
// This could also be a syslog/eventlog Core. The important thing is that we provide it an ecszap encoder, and later wrap the core so that errors don't get mangled.
core := zapcore.NewCore(encoder, os.Stdout, zap.DebugLevel)
logger := zap.New(ecszap.WrapCore(core), zap.AddCaller())
defer logger.Sync()
It becomes a little more fragile, as a user might create the encoder but forget to wrap the underlying core. We could continue to provide and primarily recommend the all-in-one NewCore constructor, but also provide these other bits for more advanced composition.
json_encoder.go
Outdated
} | ||
|
||
// add log.origin.* | ||
if ent.Caller.Defined && !final.SkipCaller { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just looking at the zapcore code, and the only thing I saw that made it difficult to reuse the JSONEncoder (i.e. without copying the source code) is Caller. Did I miss any others?
Caller is tricky because CallerEncoder takes a PrimitiveArrayEncoder, which doesn't provide a means of encoding an object.
However, it's called with a *zapcore.jsonEncoder: https://github.com/uber-go/zap/blob/9646482274d01f82b74200da5598b5964c2ea828/zapcore/json_encoder.go#L367
So I think we could just type-assert it to ArrayEncoder, and use AppendObject with the caller details. Kinda dirty, but since we're not making the underlying encoder configurable it should be of minimal risk, and then we could avoid duplicating all the code. Not sure if it's worthwhile or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ecs caller implementation sets 2 keys ("log.origin.file.line"
, "log.origin.file.name"
), whereas the zapcore implementation only one, and all the key setting logic is private. But I agree, we could work around that with some kind of hack.
I did not find a way to work around the error encoding though. EncodeEntry
calls addFields
which is implemented in the ecszap
package, allowing for dedicated error handling. I didn't find a solution avoiding to copy all the encoder code but still allowing to encode errors in an ecs compliant way. Please let me know if you have any ideas for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@simitt and I discussed this live, but for posterity:
- I had in mind that for caller we would just encode one key, but with an object type. e.g.
"log.origin": {"file.line": 123, "file.name": "foo.go"}
. In this case, we can stick with the one caller key which the zapcore JSON encoder makes configurable, and use the type-assertion hack to encode an object value. - For errors, before handing off to the zapcore JSON encoder, we can pre-process the fields to replace any ErrorType fields with an ObjectType that encodes the error according to ECS
Reuse as much code as possible, implementing dedicated solution for caller and errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
StackTrace() errors.StackTrace | ||
} | ||
|
||
// *** code is copied from github.com/zapcore/core.go *** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With code copied directly from zap, do we need to add some license info to the file header?
LevelKey: "log.level", | ||
TimeKey: "@timestamp", | ||
LineEnding: ec.LineEnding, | ||
EncodeTime: internal.EpochMicrosTimeEncoder, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be in favor if we make timestamp encoding configurable. One might want to use nanoseconds, milliseconds precision or Text output with timezone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adds minimal required ECS information to every log output.
Uses ECS version 1.5.0.
related to #elastic/ecs-logging#15