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

Re-introduce terse configuration #274

Merged
merged 11 commits into from
Feb 10, 2017
Merged

Re-introduce terse configuration #274

merged 11 commits into from
Feb 10, 2017

Conversation

akshayjshah
Copy link
Contributor

@akshayjshah akshayjshah commented Feb 4, 2017

This PR re-introduces a nice, terse way to configure sane-by-default loggers. It also introduces a configuration struct for loggers, since many applications want to drive logger configuration from config instead of code.

@mention-bot
Copy link

@akshayjshah, thanks for your PR! By analyzing the history of the files in this pull request, we identified @jcorbin and @prashantv to be potential reviewers.

@@ -80,16 +182,32 @@ type ObjectEncoder interface {
// arrays even though they aren't typical in Go. Like slices, ArrayEncoders
// aren't safe for concurrent use (though typical use shouldn't require locks).
type ArrayEncoder interface {
// Built-in types
PrimitiveArrayEncoder
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'll break out this change into a separate PR - it was an oversight on my part. Passing an ArrayEncoder to a TimeEncoder opens us up to infinite recursion :/

@akshayjshah akshayjshah force-pushed the ajs-config branch 2 times, most recently from 1c6b27f to 75f58f4 Compare February 9, 2017 01:34
@akshayjshah akshayjshah changed the title WIP: Re-introduce terse configuration Re-introduce terse configuration Feb 9, 2017
Akshay Shah added 7 commits February 9, 2017 09:13
Amend `zapcore.Level`'s text unmarshaling logic to convert the zero-value string
to the zero-value Level, then add text unmarshaling support to atomic levels too.
Provide an all-in-one wrapper around the open/tee/lock dance.
config.go Outdated
// global CPU and I/O load that logging puts on your process while preserving a
// representative subset of your logs.
//
// See zapcore.Sample for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we mention that these values are per-second?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

config.go Outdated
Development bool `json:"development",yaml:"development"`
// DisableCaller stops annotating logs with the calling function's file
// name and line number.
DisableCaller bool `json:"disable_caller",yaml:"disable_caller"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason for snake case in JSON? I tend to use the google JSON style guide which suggests camelCase
https://google.github.io/styleguide/jsoncstyleguide.xml#Property_Name_Format

I'm not sure what's appropriate for YAML, don't think there's any standard, I've seen hyphernated-case as well as camelCase and snake_case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like everyone just ports the convention from their primary programming language. FB's API is snake-cased, Twitter's API is snake-cased, and the portions of the AWS APIs I've used are snake-cased.

I don't really care much though. Happy to change if you have a strong preference.

config.go Outdated
// DisableCaller stops annotating logs with the calling function's file
// name and line number.
DisableCaller bool `json:"disable_caller",yaml:"disable_caller"`
// DisableStacktrace completely disables automatic stacktrace capturing.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add information about when stack traces are captured (looks like error or higher for prod, and warn or higher for dev)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great minds do indeed think alike. 👍 👍

config.go Outdated
EncodeTime: zapcore.EpochTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
OutputPaths: []string{"stdout"},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should the default output for logs be stderr?
logrus seems to use stderr by default: https://godoc.org/github.com/sirupsen/logrus#Logger
As does the standard go log package.

(Ideally production configs would actually use a file, but that's not a great default)

Copy link
Contributor Author

@akshayjshah akshayjshah Feb 9, 2017

Choose a reason for hiding this comment

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

Sure, sounds good. This is yet another place where Go's standard lib chose to go down an unusual route :(

We'll have to disagree about prod logs - I still think they should log to standard out, and it's the process manager's job to pipe that to a file. That's the 12-factor standard.

config.go Outdated
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
},
OutputPaths: []string{"stdout"},
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as earlier, might want to use stderr for all logs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep.

}

if cfg.Sampling != nil {
opts = append(opts, WrapFacility(func(fac zapcore.Facility) zapcore.Facility {
Copy link
Collaborator

Choose a reason for hiding this comment

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

(just a question) is there a difference between wrapping the facility when passing it into New vs calling WrapFacility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. This was just a bit easier organizationally - otherwise creating the facility needed its own helper method.

writer.go Outdated
return zapcore.AddSync(ioutil.Discard), nil
}
writers, err := open(paths)
return zapcore.Lock(zapcore.MultiWriteSyncer(writers...)), err
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should MultiWriteSyncer always lock?

Copy link
Contributor Author

@akshayjshah akshayjshah Feb 9, 2017

Choose a reason for hiding this comment

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

Not sure exactly what you're asking here. Since we're opening paths, the WriteSyncers in question are files, so we always need a lock.

In general, though, I don't think MultiWriteSyncers should lock - it's perfectly reasonable to write to both a file and a Kafka client, in which case locking should happen underneath the tee layer.

writer.go Outdated
func Open(paths ...string) (zapcore.WriteSyncer, error) {
if len(paths) == 0 {
return zapcore.AddSync(ioutil.Discard), nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we special case len(paths) == 1 and return the WriteSyncer directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

writer.go Outdated
writers = append(writers, os.Stderr)
continue
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we never read the file, I think we can use O_WRONLY.

For the mask, it is then umasked, so create uses 0666:
https://golang.org/src/os/file.go?s=7623:7662#L248

We should probably do the same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

// Sample creates a facility that samples incoming entries.
// Sample creates a Facility that samples incoming entries, which caps the CPU
// and I/O load of logging while preserving a representative subset of your
// logs.
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we mention the "optimized for performance over correctness" here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I was saving that until the sampler fix lands, but this is a perfectly fine time to add it.

config.go Outdated
)

// SamplingConfig sets a sampling strategy for the logger. Sampling caps the
// global CPU and I/O load that logging puts on your process while preserving a
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer to soften this to "while attempting to preserve a".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

// Development puts the logger in development mode, which changes the
// behavior of DPanicLevel and takes stacktraces more liberally.
Development bool `json:"development",yaml:"development"`
// DisableCaller stops annotating logs with the calling function's file
Copy link
Contributor

Choose a reason for hiding this comment

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

Say more about when it happens if not enabled, always? Only for warn and up?

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

config.go Outdated
// DisableCaller stops annotating logs with the calling function's file
// name and line number.
DisableCaller bool `json:"disable_caller",yaml:"disable_caller"`
// DisableStacktrace completely disables automatic stacktrace capturing.
Copy link
Contributor

Choose a reason for hiding this comment

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

Say more about when it happens if not enabled, always? Only for error and up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

// level, so calling Config.Level.SetLevel will atomically change the log
// level of all loggers descended from this config. The zero value is
// InfoLevel.
Level AtomicLevel `json:"level",yaml:"level"`
Copy link
Contributor

Choose a reason for hiding this comment

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

How about a:

Level Level
AllowLevelChange bool

And defaulting to static levels?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why? The performance overhead is near-zero, and I'd always recommend using a dynamic level.

config.go Outdated
DisableStacktrace bool `json:"disable_stacktrace",yaml:"disable_stacktrace"`
// Sampling sets a sampling policy. A nil SamplingConfig disables sampling.
Sampling *SamplingConfig `json:"sampling",yaml:"sampling"`
// Encoding sets the logger's encoding. Valid values are "json" and
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, I would've expected more of a:

EnableConsole bool // maybe disable instead of enable
DisableStandardErrors bool
FileEncoding string
OutputPaths []string
ErrorOutputPaths []string

The idea being:

  • stdout is always console encoded
  • files are always json encoded
  • errors at least go to stderr, and maybe also a file

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'm glad that we can support that setup, but I'm less of a fan of making that part of the quickstart. I remembered discussing it with you and played with it for a while, and I found it pretty jarring. IMO it's just too unusual.

If and when we add new encoders, I'd also like this config struct to support them. Again, I have msgpack in production in mind.

level.go Outdated
// "panic", and "fatal").
func (lvl *AtomicLevel) UnmarshalText(text []byte) error {
if lvl.l == nil {
lvl.l = atomic.NewInt32(int32(InfoLevel))
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer to defer thus until after we've parsed without err.

level.go Outdated
if err := l.UnmarshalText(text); err != nil {
return err
}
lvl.SetLevel(l)
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer to:

if lvl.l == nil {
  lvl.l = atomic.NewInt32(int32(l))
} else {
  lvl.SetLevel(l)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

logger.go Outdated
@@ -105,6 +99,16 @@ func (log *Logger) Named(s string) *Logger {
return l
}

// WithOptions clones the current Logger, applies the supplied Options, and
// returns the result. It's safe to use concurrently.
Copy link
Contributor

Choose a reason for hiding this comment

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

"... and returns the clone." (... "cloned child logger." ?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

writer.go Outdated
writers = append(writers, os.Stderr)
continue
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
Copy link
Contributor

Choose a reason for hiding this comment

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

if this were a default:, you wouldn't need those continues

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but then I'd need another level of nesting. To me, this is similar to an early return. Reasonable?

@@ -65,7 +65,13 @@ func fnv32a(s string) uint32 {
return hash
}

// Sample creates a facility that samples incoming entries.
// Sample creates a Facility that samples incoming entries, which caps the CPU
// and I/O load of logging while preserving a representative subset of your
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "attempts to preserve a"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

@akshayjshah
Copy link
Contributor Author

akshayjshah commented Feb 9, 2017

Addressed all comments except:

1. Casing of JSON/YAML keys in config struct. Not opposed to changing, but the widely-used APIs I'm familiar with prefer snake case. Converted to camel-case, since this is a Go library and Go camel-cases names.
2. Different encodings for different outputs. In practice, feels jarring to me; also complicates config somewhat.

I'll hold off on rebasing until all CR is resolved.

Copy link
Contributor

@jcorbin jcorbin left a comment

Choose a reason for hiding this comment

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

Makes sense, I can live with it :-)

@akshayjshah akshayjshah merged commit 4277ba7 into dev Feb 10, 2017
@akshayjshah akshayjshah deleted the ajs-config branch February 10, 2017 16:38
akshayjshah added a commit that referenced this pull request Feb 15, 2017
Re-introduce a nice, terse way to configure sane-by-default loggers. Also, introduce a configuration struct for loggers, since many applications want to drive logger configuration from config instead of code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants