Skip to content
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

Merged
merged 1 commit into from
Oct 22, 2015

Conversation

mattkanwisher
Copy link
Contributor

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

@juliusv
Copy link
Member

juliusv commented Oct 21, 2015

@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 SetFormatter() (or whatever it will be named in the end) initialization method to initialize logging properly. That's because there are multiple interdependent flags, so configuring the logger can't just be solved in each flag's custom parsing code, like we currently do it with the log level.

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.

@brian-brazil
Copy link
Contributor

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.

@mattkanwisher
Copy link
Contributor Author

@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.

@juliusv
Copy link
Member

juliusv commented Oct 21, 2015

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.

@taotetek
Copy link

Please note that syslog-ng and rsyslog both support JSON parsing natively. See:

Which is part of our interest in @Cee JSON support.

@beorn7
Copy link
Member

beorn7 commented Oct 21, 2015

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.
Happy to accept any outcome...

@mattkanwisher
Copy link
Contributor Author

@juliusv we can certainly change the code so the interface doesn't change for all components downstream

@mattkanwisher
Copy link
Contributor Author

@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
Copy link
Member

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
}
Copy link
Member

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
}

Copy link
Contributor Author

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

Copy link
Member

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 ")
Copy link
Member

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?

Copy link
Contributor Author

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{}
Copy link
Member

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?

Copy link
Contributor Author

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 ")
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none fixed

@grobie
Copy link
Member

grobie commented Oct 21, 2015

👍

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" {
Copy link
Member

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)
}

Copy link
Contributor Author

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

@mattkanwisher mattkanwisher force-pushed the master branch 2 times, most recently from 83d9663 to 4958f27 Compare October 21, 2015 18:25
return setFormatterSylog(appname, local)
case "stdout":
default:
return fmt.Errorf("unsupported logger", u.Opaque)
Copy link
Member

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).

Copy link
Contributor Author

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 ;)

@mattkanwisher mattkanwisher force-pushed the master branch 2 times, most recently from dc5811e to ea15603 Compare October 21, 2015 19:08
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)
Copy link
Member

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 ")
Copy link
Member

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.

@mattkanwisher
Copy link
Contributor Author

Cool updated all the feedback


// String implements flag.Value.
func (f logFormatFlag) String() string {
return origLogger.Level.String()
Copy link
Member

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].

Copy link
Contributor Author

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

Copy link
Member

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?)

Copy link
Contributor Author

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

Copy link
Member

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)
Copy link
Member

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"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

;) fixed

@mattkanwisher
Copy link
Contributor Author

I think we have 5 hours in on this PR now ;) how about we merge this sucker

}, err
}

func getLocal(level string) syslog.Priority {
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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...

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

@juliusv
Copy link
Member

juliusv commented Oct 21, 2015

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
Copy link
Member

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?

Copy link
Contributor Author

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

@grobie
Copy link
Member

grobie commented Oct 21, 2015

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 != "" {
Copy link
Member

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 == "".

Copy link
Contributor Author

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
Copy link
Member

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.

@mattkanwisher mattkanwisher force-pushed the master branch 2 times, most recently from 7341a0f to b1f2ce5 Compare October 21, 2015 21:01
@fabxc
Copy link
Contributor

fabxc commented Oct 22, 2015

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":
Copy link
Contributor

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.

sirupsen/logrus@f8f0884

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch fixed

@fabxc
Copy link
Contributor

fabxc commented Oct 22, 2015

Thanks a bunch! 👍

fabxc added a commit that referenced this pull request Oct 22, 2015
Add structured logging to json and support for syslog
@fabxc fabxc merged commit 369ec04 into prometheus:master Oct 22, 2015
gouthamve pushed a commit to gouthamve/common that referenced this pull request Jul 22, 2018
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants