2022-08-06 14:21:18 +00:00
|
|
|
package logging
|
|
|
|
|
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
|
|
|
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Option defines a modification to the configuration for a logger.
|
|
|
|
type Option func(LoggerOpts) LoggerOpts
|
|
|
|
|
|
|
|
// LoggerOpts is a collection of configuration settings for loggers.
|
|
|
|
type LoggerOpts struct {
|
|
|
|
// Name is the name or "@module" of a logger.
|
|
|
|
Name string
|
|
|
|
|
|
|
|
// Level is the most verbose level that a logger will write logs for.
|
|
|
|
Level hclog.Level
|
|
|
|
|
|
|
|
// IncludeLocation indicates whether logs should include the location
|
|
|
|
// of the logging statement or not.
|
|
|
|
IncludeLocation bool
|
|
|
|
|
|
|
|
// AdditionalLocationOffset is the number of additional stack levels to
|
|
|
|
// skip when finding the file and line information for the log line.
|
|
|
|
// Defaults to 1 to account for the tflog and tfsdklog logging functions.
|
|
|
|
AdditionalLocationOffset int
|
|
|
|
|
|
|
|
// Output dictates where logs are written to. Output should only ever
|
|
|
|
// be set by tflog or tfsdklog, never by SDK authors or provider
|
|
|
|
// developers. Where logs get written to is complex and delicate and
|
|
|
|
// requires a deep understanding of Terraform's architecture, and it's
|
|
|
|
// easy to mess up on accident.
|
|
|
|
Output io.Writer
|
|
|
|
|
|
|
|
// IncludeTime indicates whether logs should include the time they were
|
|
|
|
// written or not. It should only be set to true when testing tflog or
|
|
|
|
// tfsdklog; providers and SDKs should always include the time logs
|
|
|
|
// were written as part of the log.
|
|
|
|
IncludeTime bool
|
|
|
|
|
|
|
|
// Fields indicates the key/value pairs to be added to each of its log output.
|
|
|
|
Fields map[string]interface{}
|
|
|
|
|
|
|
|
// IncludeRootFields indicates whether a new subsystem logger should
|
|
|
|
// copy existing fields from the root logger. This is only performed
|
|
|
|
// at the time of new subsystem creation.
|
|
|
|
IncludeRootFields bool
|
|
|
|
|
|
|
|
// OmitLogWithFieldKeys indicates that the logger should omit to write
|
|
|
|
// any log when any of the given keys is found within the fields.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// OmitLogWithFieldKeys = `['foo', 'baz']`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "...", fields = { 'foo': '...', 'bar': '...' }` -> omitted
|
|
|
|
// log2 = `{ msg = "...", fields = { 'bar': '...' }` -> printed
|
|
|
|
// log3 = `{ msg = "...", fields = { 'baz': '...', 'boo': '...' }` -> omitted
|
|
|
|
//
|
|
|
|
OmitLogWithFieldKeys []string
|
|
|
|
|
|
|
|
// OmitLogWithMessageRegexes indicates that the logger should omit to write
|
|
|
|
// any log that matches any of the given *regexp.Regexp.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// OmitLogWithMessageRegexes = `[regexp.MustCompile("(foo|bar)")]`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "banana apple foo", fields = {...}` -> omitted
|
|
|
|
// log2 = `{ msg = "pineapple mango", fields = {...}` -> printed
|
|
|
|
// log3 = `{ msg = "pineapple mango bar", fields = {...}` -> omitted
|
|
|
|
//
|
|
|
|
OmitLogWithMessageRegexes []*regexp.Regexp
|
|
|
|
|
|
|
|
// OmitLogWithMessageStrings indicates that the logger should omit to write
|
|
|
|
// any log that matches any of the given string.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// OmitLogWithMessageStrings = `['foo', 'bar']`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "banana apple foo", fields = {...}` -> omitted
|
|
|
|
// log2 = `{ msg = "pineapple mango", fields = {...}` -> printed
|
|
|
|
// log3 = `{ msg = "pineapple mango bar", fields = {...}` -> omitted
|
|
|
|
//
|
|
|
|
OmitLogWithMessageStrings []string
|
|
|
|
|
|
|
|
// MaskFieldValuesWithFieldKeys indicates that the logger should mask with asterisks (`*`)
|
|
|
|
// any field value where the key matches one of the given keys.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// MaskFieldValuesWithFieldKeys = `['foo', 'baz']`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "...", fields = { 'foo': '***', 'bar': '...' }` -> masked value
|
|
|
|
// log2 = `{ msg = "...", fields = { 'bar': '...' }` -> as-is value
|
|
|
|
// log3 = `{ msg = "...", fields = { 'baz': '***', 'boo': '...' }` -> masked value
|
|
|
|
//
|
|
|
|
MaskFieldValuesWithFieldKeys []string
|
|
|
|
|
|
|
|
// MaskAllFieldValuesRegexes indicates that the logger should replace, within
|
|
|
|
// all the log field values, the portion matching one of the given *regexp.Regexp.
|
|
|
|
//
|
|
|
|
// Note that the replacement will happen, only for field values that are of type string.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// MaskAllFieldValuesRegexes = `[regexp.MustCompile("(foo|bar)")]`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "...", fields = { 'k1': '***', 'k2': '***', 'k3': 'baz' }` -> masked value
|
|
|
|
// log2 = `{ msg = "...", fields = { 'k1': 'boo', 'k2': 'far', 'k3': 'baz' }` -> as-is value
|
|
|
|
// log2 = `{ msg = "...", fields = { 'k1': '*** *** baz' }` -> masked value
|
|
|
|
//
|
|
|
|
MaskAllFieldValuesRegexes []*regexp.Regexp
|
|
|
|
|
|
|
|
// MaskAllFieldValuesStrings indicates that the logger should replace, within
|
|
|
|
// all the log field values, the portion equal to one of the given strings.
|
|
|
|
//
|
|
|
|
// Note that the replacement will happen, only for field values that are of type string.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// MaskAllFieldValuesStrings = `['foo', 'baz']`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "...", fields = { 'k1': '***', 'k2': 'bar', 'k3': '***' }` -> masked value
|
|
|
|
// log2 = `{ msg = "...", fields = { 'k1': 'boo', 'k2': 'far', 'k3': '***' }` -> as-is value
|
|
|
|
// log2 = `{ msg = "...", fields = { 'k1': '*** bar ***' }` -> masked value
|
|
|
|
MaskAllFieldValuesStrings []string
|
|
|
|
|
|
|
|
// MaskMessageRegexes indicates that the logger should replace, within
|
|
|
|
// a log message, the portion matching one of the given *regexp.Regexp.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// MaskMessageRegexes = `[regexp.MustCompile("(foo|bar)")]`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "banana apple ***", fields = {...}` -> masked portion
|
|
|
|
// log2 = `{ msg = "pineapple mango", fields = {...}` -> as-is
|
|
|
|
// log3 = `{ msg = "pineapple mango ***", fields = {...}` -> masked portion
|
|
|
|
//
|
|
|
|
MaskMessageRegexes []*regexp.Regexp
|
|
|
|
|
|
|
|
// MaskMessageStrings indicates that the logger should replace, within
|
|
|
|
// a log message, the portion equal to one of the given strings.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// MaskMessageStrings = `['foo', 'bar']`
|
|
|
|
//
|
|
|
|
// log1 = `{ msg = "banana apple ***", fields = {...}` -> masked portion
|
|
|
|
// log2 = `{ msg = "pineapple mango", fields = {...}` -> as-is
|
|
|
|
// log3 = `{ msg = "pineapple mango ***", fields = {...}` -> masked portion
|
|
|
|
//
|
|
|
|
MaskMessageStrings []string
|
|
|
|
}
|
|
|
|
|
2023-03-20 20:25:53 +00:00
|
|
|
// Copy creates a duplicate LoggerOpts. This should be used to ensure
|
|
|
|
// safe LoggerOpts modification when the LoggerOpts could be saved into a
|
|
|
|
// new context.Context.
|
|
|
|
func (o LoggerOpts) Copy() LoggerOpts {
|
|
|
|
result := LoggerOpts{
|
|
|
|
AdditionalLocationOffset: o.AdditionalLocationOffset,
|
|
|
|
Fields: make(map[string]any, len(o.Fields)),
|
|
|
|
IncludeLocation: o.IncludeLocation,
|
|
|
|
IncludeRootFields: o.IncludeRootFields,
|
|
|
|
IncludeTime: o.IncludeTime,
|
|
|
|
Level: o.Level,
|
|
|
|
MaskAllFieldValuesRegexes: make([]*regexp.Regexp, len(o.MaskAllFieldValuesRegexes)),
|
|
|
|
MaskAllFieldValuesStrings: make([]string, len(o.MaskAllFieldValuesStrings)),
|
|
|
|
MaskFieldValuesWithFieldKeys: make([]string, len(o.MaskFieldValuesWithFieldKeys)),
|
|
|
|
MaskMessageRegexes: make([]*regexp.Regexp, len(o.MaskMessageRegexes)),
|
|
|
|
MaskMessageStrings: make([]string, len(o.MaskMessageStrings)),
|
|
|
|
Name: o.Name,
|
|
|
|
OmitLogWithFieldKeys: make([]string, len(o.OmitLogWithFieldKeys)),
|
|
|
|
OmitLogWithMessageRegexes: make([]*regexp.Regexp, len(o.OmitLogWithMessageRegexes)),
|
|
|
|
OmitLogWithMessageStrings: make([]string, len(o.OmitLogWithMessageStrings)),
|
|
|
|
Output: o.Output,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy all slice/map contents to prevent leaking memory references
|
|
|
|
// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
|
|
|
|
for key, value := range o.Fields {
|
|
|
|
result.Fields[key] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
copy(result.MaskAllFieldValuesRegexes, o.MaskAllFieldValuesRegexes)
|
|
|
|
copy(result.MaskAllFieldValuesStrings, o.MaskAllFieldValuesStrings)
|
|
|
|
copy(result.MaskFieldValuesWithFieldKeys, o.MaskFieldValuesWithFieldKeys)
|
|
|
|
copy(result.MaskMessageRegexes, o.MaskMessageRegexes)
|
|
|
|
copy(result.MaskMessageStrings, o.MaskMessageStrings)
|
|
|
|
copy(result.OmitLogWithFieldKeys, o.OmitLogWithFieldKeys)
|
|
|
|
copy(result.OmitLogWithMessageRegexes, o.OmitLogWithMessageRegexes)
|
|
|
|
copy(result.OmitLogWithMessageStrings, o.OmitLogWithMessageStrings)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2022-08-06 14:21:18 +00:00
|
|
|
// ApplyLoggerOpts generates a LoggerOpts out of a list of Option
|
|
|
|
// implementations. By default, AdditionalLocationOffset is 1, IncludeLocation
|
|
|
|
// is true, IncludeTime is true, and Output is os.Stderr.
|
|
|
|
func ApplyLoggerOpts(opts ...Option) LoggerOpts {
|
|
|
|
// set some defaults
|
|
|
|
l := LoggerOpts{
|
|
|
|
AdditionalLocationOffset: 1,
|
|
|
|
IncludeLocation: true,
|
|
|
|
IncludeTime: true,
|
|
|
|
Output: os.Stderr,
|
|
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
|
|
l = opt(l)
|
|
|
|
}
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithAdditionalLocationOffset sets the WithAdditionalLocationOffset
|
|
|
|
// configuration option, allowing implementations to fix location information
|
|
|
|
// when implementing helper functions. The default offset of 1 is automatically
|
|
|
|
// added to the provided value to account for the tflog and tfsdk logging
|
|
|
|
// functions.
|
|
|
|
func WithAdditionalLocationOffset(additionalLocationOffset int) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.AdditionalLocationOffset = additionalLocationOffset + 1
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithOutput sets the Output configuration option, controlling where logs get
|
|
|
|
// written to. This is mostly used for testing (to write to os.Stdout, so the
|
|
|
|
// test framework can compare it against the example output) and as a helper
|
|
|
|
// when implementing safe, specific output strategies in tfsdklog.
|
|
|
|
func WithOutput(output io.Writer) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.Output = output
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithField sets the provided key/value pair, onto the LoggerOpts.Fields field.
|
|
|
|
//
|
|
|
|
// Behind the scene, fields are stored in a map[string]interface{}:
|
|
|
|
// this means that in case the same key is used multiple times (key collision),
|
|
|
|
// the last one set is the one that gets persisted and then outputted with the logs.
|
|
|
|
func WithField(key string, value interface{}) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
// Lazily create this map, on first assignment
|
|
|
|
if l.Fields == nil {
|
|
|
|
l.Fields = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
l.Fields[key] = value
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithFields sets all the provided key/value pairs, onto the LoggerOpts.Fields field.
|
|
|
|
//
|
|
|
|
// Behind the scene, fields are stored in a map[string]interface{}:
|
|
|
|
// this means that in case the same key is used multiple times (key collision),
|
|
|
|
// the last one set is the one that gets persisted and then outputted with the logs.
|
|
|
|
func WithFields(fields map[string]interface{}) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
// Lazily create this map, on first assignment
|
|
|
|
if l.Fields == nil {
|
|
|
|
l.Fields = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range fields {
|
|
|
|
l.Fields[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithRootFields enables the copying of root logger fields to a new subsystem
|
|
|
|
// logger during creation.
|
|
|
|
func WithRootFields() Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.IncludeRootFields = true
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithoutLocation disables the location included with logging statements. It
|
|
|
|
// should only ever be used to make log output deterministic when testing
|
|
|
|
// terraform-plugin-log.
|
|
|
|
func WithoutLocation() Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.IncludeLocation = false
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithoutTimestamp disables the timestamp included with logging statements. It
|
|
|
|
// should only ever be used to make log output deterministic when testing
|
|
|
|
// terraform-plugin-log.
|
|
|
|
func WithoutTimestamp() Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.IncludeTime = false
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithOmitLogWithFieldKeys appends keys to the LoggerOpts.OmitLogWithFieldKeys field.
|
|
|
|
func WithOmitLogWithFieldKeys(keys ...string) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.OmitLogWithFieldKeys = append(l.OmitLogWithFieldKeys, keys...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithOmitLogWithMessageRegexes appends *regexp.Regexp to the LoggerOpts.OmitLogWithMessageRegexes field.
|
|
|
|
func WithOmitLogWithMessageRegexes(expressions ...*regexp.Regexp) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.OmitLogWithMessageRegexes = append(l.OmitLogWithMessageRegexes, expressions...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithOmitLogWithMessageStrings appends string to the LoggerOpts.OmitLogWithMessageStrings field.
|
|
|
|
func WithOmitLogWithMessageStrings(matchingStrings ...string) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.OmitLogWithMessageStrings = append(l.OmitLogWithMessageStrings, matchingStrings...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMaskFieldValuesWithFieldKeys appends keys to the LoggerOpts.MaskFieldValuesWithFieldKeys field.
|
|
|
|
func WithMaskFieldValuesWithFieldKeys(keys ...string) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.MaskFieldValuesWithFieldKeys = append(l.MaskFieldValuesWithFieldKeys, keys...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMaskAllFieldValuesRegexes appends keys to the LoggerOpts.MaskAllFieldValuesRegexes field.
|
|
|
|
func WithMaskAllFieldValuesRegexes(expressions ...*regexp.Regexp) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.MaskAllFieldValuesRegexes = append(l.MaskAllFieldValuesRegexes, expressions...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMaskAllFieldValuesStrings appends keys to the LoggerOpts.MaskAllFieldValuesStrings field.
|
|
|
|
func WithMaskAllFieldValuesStrings(matchingStrings ...string) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.MaskAllFieldValuesStrings = append(l.MaskAllFieldValuesStrings, matchingStrings...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMaskMessageRegexes appends *regexp.Regexp to the LoggerOpts.MaskMessageRegexes field.
|
|
|
|
func WithMaskMessageRegexes(expressions ...*regexp.Regexp) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.MaskMessageRegexes = append(l.MaskMessageRegexes, expressions...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMaskMessageStrings appends string to the LoggerOpts.MaskMessageStrings field.
|
|
|
|
func WithMaskMessageStrings(matchingStrings ...string) Option {
|
|
|
|
return func(l LoggerOpts) LoggerOpts {
|
|
|
|
l.MaskMessageStrings = append(l.MaskMessageStrings, matchingStrings...)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
}
|