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