-
Notifications
You must be signed in to change notification settings - Fork 321
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 structured logging to json and support for syslog #11
Conversation
@fabxc @brian-brazil @beorn7 So I've been neglecting this PR here for a while, but talked to @mattkanwisher from DigitalOcean at VelocityConf in NYC about this. What do you think? Is it worth adding these features to our logging library directly? It would mean that any user of the logging library needs to call the Alternatively, what about adding only JSON support, because that is harder to solve externally? Prometheus would then still focus on only logging to stderr and leave any syslog support up to a wrapper. |
I think having syslog out of the box would be useful, as pretty much everything log-related integrates with it. JSON seems a little more custom, so we should probably draw a line in the sand now rather than dealing with feature creep later. |
@brian-brazil structured logging isn't really a feature creep as the library already is doing it. Honestly the current structured logging isn't useful to ingest into logging systems like kibana. We can do something where have a Database like DSN to specify logging settings "rsylog:local7?format=json" or something. |
Yeah, it's harder to structure the logs externally once they're already outputted unstructured. Supporting machine-readable structured output would however mean that any subsequent change to the format (whether through switching to a different underlying library than logrus or else) we'll have to treat as a breaking interface change in all components using the library (with only human-readable format, you usually don't care). Just a thing to keep in mind. |
Please note that syslog-ng and rsyslog both support JSON parsing natively. See:
Which is part of our interest in @Cee JSON support. |
Not really deep into this subject right now. Intuitively, I'd say if our logging is structured internally, we should have a way to output it in a structured form. |
@juliusv we can certainly change the code so the interface doesn't change for all components downstream |
@juliusv ok fixed the interface is no longer changed downstream. Do we have any other issues here? ./prometheus -log.format="logger:syslog?json=true&appname=bob&local=local7 |
origLogger.Formatter = fmter | ||
} | ||
} | ||
return nil |
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.
Try to reduce indentation in go.
if appname == "" || local == "" {
return nil
}
fmter, err := newSyslogger(appname, local, origLogger.Formatter)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating syslog formatter: %v\n", err)
origLogger.Errorf("can't connect logger to syslogs: %v", err)
return nil
}
origLogger.Formatter = fmter
return nil
I don't really understand, why your method signature includes a error
return value. You never check for the return value and always return nil
anyway.
return syslog.LOG_LOCAL6 | ||
case "7": | ||
return syslog.LOG_LOCAL7 | ||
} |
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.
This seems to be easier solved by a map than by running through a switch statement every time?
var localMapping = map[string]syslog.Priority{
"0": syslog.LOG_LOCAL0,
"1": syslog.LOG_LOCAL1,
// ...
}
func getLocal(level string) syslog.Priority {
p, ok := localMapping[level]
if !ok {
return syslog.LOG_LOCAL0
}
return p
}
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 function only runs once ever
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.
Nevermind, it's only used during initialization anyways.
func init() { | ||
// In order for this flag to take effect, the user of the package must call | ||
// flag.Parse() before logging anything. | ||
flag.Var(levelFlag{}, "log.level", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal].") | ||
flag.Var(formatLog{}, "log.format", "If set use a syslog logger or json logging. Example: logger:syslog?appname=bob&local=local7 or logger:stdout@?json=true ") |
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.
So this example shows local=local7
. The getLocal
method in the formatter only knows about local=7
though. Am I missing where you remove the local
part from local7
?
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.
Example is wrong good catch should say local=7, updated
|
||
var ceeTag = []byte("@cee:") | ||
|
||
var _ logrus.Formatter = &syslogger{} |
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.
What is the purpose of this line?
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.
not needed removed, this was copied from our internal logging library that also uses logrus
func init() { | ||
// In order for this flag to take effect, the user of the package must call | ||
// flag.Parse() before logging anything. | ||
flag.Var(levelFlag{}, "log.level", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal].") | ||
flag.Var(formatLog{}, "log.format", "If set use a syslog logger or json logging. Example: logger:syslog?appname=bob&local=7 or logger:stdout@?json=true ") |
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.
What's the purpose of the @ sign in the stdout example?
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.
none fixed
👍 Structured logs are so much easier to deal with, I'd always prefer them. Especially as Prometheus de-facto already uses structured logging, having a well defined format makes a lot of sense. Syslog is so common that we should support. The majority of software I've been working with only supports syslog style logging and nothing else. |
setJsonFormatter() | ||
} | ||
|
||
if u.Opaque == "syslog" { |
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.
What do you think about being explicit about the acceptable loggers? Might help to avoid confusion.
switch u.Opague {
case "syslog":
// ...
case "stdout":
default:
return fmt.Error("unsupported logger", u.Opague)
}
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 like the switch fixed
83d9663
to
4958f27
Compare
return setFormatterSylog(appname, local) | ||
case "stdout": | ||
default: | ||
return fmt.Errorf("unsupported logger", u.Opaque) |
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.
Ah sorry, I forgot there isn't a fmt.Error
method like the log.Error
one. Please add the missing %s
to print the logger value (go vet
should also mark this problem).
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.
fixed, make vet doesn't cover common ill do it manually ;)
dc5811e
to
ea15603
Compare
fmter, err := newSyslogger(appname, local, origLogger.Formatter) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "error creating syslog formatter: %v\n", err) | ||
origLogger.Errorf("can9't connect logger to syslogs: %v", err) |
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.
"can9't" -> "can't"
// flag.Parse() before logging anything. | ||
flag.Var(levelFlag{}, "log.level", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal].") | ||
flag.Var(formatLog{}, "log.format", "If set use a syslog logger or JSON logging. Example: logger:syslog?appname=bob&local=7 or logger:stdout?json=true ") |
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.
Naming consistency: if we name the above levelFlag
, then we should also name this one logFormatFlag
instead of formatLog
.
Cool updated all the feedback |
|
||
// String implements flag.Value. | ||
func (f logFormatFlag) String() string { | ||
return origLogger.Level.String() |
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.
This now should return something different than the log level? Guess this was a copy-and-paste from the levelFlag.String()
implementation?
See also the wrong default values in the flag help output now:
-log.format "info"
If set use a syslog logger or JSON logging. Example:
logger:syslog?appname=bob&local=7 or logger:stdout?json=true
-log.level "info"
Only log messages with the given severity or above. Valid levels:
[debug, info, warn, error, fatal].
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.
so I'm just going to return the original log format uri, since thats probably the most useful
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 guess it should return the empty string for an empty default value?)
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.
Probably, I'm not sure how useful this really is. Normally I woudn't do custom struct flags
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.
Yeah, what you did is actually correct I think.
fmter, err := newSyslogger(appname, local, origLogger.Formatter) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "error creating syslog formatter: %v\n", err) | ||
origLogger.Errorf("can't connect logger to syslogs: %v", err) |
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.
Is this supposed to read "syslogs" instead of "syslog"?
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.
;) fixed
I think we have 5 hours in on this PR now ;) how about we merge this sucker |
}, err | ||
} | ||
|
||
func getLocal(level string) syslog.Priority { |
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.
Would getPriority
be a clearer name 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.
no maybe Facility would be better https://en.wikipedia.org/wiki/Syslog but its not a priority
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.
Well, it returns a syslog.Priority
. The stdlib docs say:
"The Priority is a combination of the syslog facility and severity."
So I guess priority would actually be correct here...
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.
This method won't ever return severity, so I don't think getPriority makes sense. +1 for getFacility, but getLevel is fine as well. It's not a complicated method.
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.
Ok.
Looks good to me now, but want another person to do a final 👍 and merge, since it's touching the whole ecosystem and I'm vacationing :) |
case "7": | ||
return syslog.LOG_LOCAL7 | ||
} | ||
return syslog.LOG_LOCAL0 |
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.
@mattkanwisher So I was thinking about this after the introduction of the switch Opague
. It might be a good idea to not return a default value for all unknown levels, but to return an error. That way a user will have immediate feedback what's wrong with local=local7
instead of trying to understand why their log messages are delivered using the wrong facility. What do you think?
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.
ok sure returning error now
Awesome! Thanks so much for sticking with us ;) |
@@ -39,10 +41,64 @@ func (f levelFlag) Set(level string) error { | |||
return nil | |||
} | |||
|
|||
func setSyslogFormatter(appname, facility string) error { | |||
if appname != "" && facility != "" { |
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.
Wait, this should have been changed to be the other way around. appname == "" || facility == ""
.
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.
good catch fixed
@@ -39,10 +41,64 @@ func (f levelFlag) Set(level string) error { | |||
return nil | |||
} | |||
|
|||
func setSyslogFormatter(appname, facility string) error { | |||
if appname != "" && facility != "" { | |||
return nil |
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.
Might be good to return an error here instead of nil. This code path is only reached if someone set logger:syslog
. We should tell them that appname and facility are mandatory.
If you think it makes sense, we could alternatively set facility to some default value.
7341a0f
to
b1f2ce5
Compare
Chiming in late – I derived this package a few weeks back exactly because I wanted more structured logging. While changing a structured log message is more explicitly breaking, it is still an order of magnitude better than parsing unstructured logs. In the end we have to decide whether our logging output is ruled by our semantic versioning or not – I doubt that it makes sense. Syslog and JSON support is fine I suppose since it is handled by our underlying library anyway. Thanks for writing this and everyone else for reviewing 👍 |
appname := u.Query().Get("appname") | ||
facility := u.Query().Get("local") | ||
return setSyslogFormatter(appname, facility) | ||
case "stdout": |
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.
logrus defaults to stderr
though. So People will provide stdout, and will get stderr, while providing stderr will fail.
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.
nice catch fixed
Thanks a bunch! 👍 |
Add structured logging to json and support for syslog
3ec519f Merge pull request prometheus#12 from weaveworks/prefix-unit-test-sched 856790f Use golang package name as a prefix on unit test schedules. 7f5fa57 Merge pull request prometheus#11 from weaveworks/10-dont-delete-sshed-gce-nodes 0bff9b4 Don't delete GCE nodes for running (but failed) builds 03aed8f Merge pull request prometheus#9 from weaveworks/configure-pac-domain c04c534 socks: Make main shExpMatch expression configurable git-subtree-dir: tools git-subtree-split: 3ec519f704ed7d334ca48abcb6211885f204f630
We use kibana currently to manage our centralized logging, adding json support will allow native integration with tools like this and logstash. In addition I added syslogger support so people don't need to do manual piping to syslog