Skip to content

Commit

Permalink
Add log filtering functionality (#71)
Browse files Browse the repository at this point in the history
* New utility function in `internal/hclogutils`: `ArgsToKeys`

It takes `ImplicitArgs()` from `hclog.Logger`, and returns the keys of the k/v pairs slice

* Implementing the business logic of filtering in `internal/logging` package

* Defining utilities to create `loggerKey`s for storing log filtering configuration in Context

* Exposing log filtering on all offerings of this library

- tflog root logger
- tflog subsystem logger
- tfsdklog root logger
- tfsdklog subsystem logger

* Adding CHANGELOG entries for log filtering and preparing for v0.5.0 release
  • Loading branch information
Ivan De Marino committed Jul 14, 2022
1 parent 0f9eafb commit 6c641bd
Show file tree
Hide file tree
Showing 18 changed files with 3,738 additions and 37 deletions.
15 changes: 15 additions & 0 deletions .changelog/71.txt
@@ -0,0 +1,15 @@
```release-note:feature
tflog: Added `WithOmitLogWithFieldKeys()`, `WithOmitLogWithMessageRegex()`, `WithOmitLogMatchingString()`, `WithMaskFieldValueWithFieldKeys()`, `WithMaskMessageRegex()` and `WithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for the provider root logger
```

```release-note:feature
tflog: Added `SubsystemWithOmitLogWithFieldKeys()`, `SubsystemWithOmitLogWithMessageRegex()`, `SubsystemWithOmitLogMatchingString()`, `SubsystemWithMaskFieldValueWithFieldKeys()`, `SubsystemWithMaskMessageRegex()` and `SubsystemWithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for provider subsystem loggers
```

```release-note:feature
tfsdklog: Added `WithOmitLogWithFieldKeys()`, `WithOmitLogWithMessageRegex()`, `WithOmitLogMatchingString()`, `WithMaskFieldValueWithFieldKeys()`, `WithMaskMessageRegex()` and `WithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for the SDK root logger
```

```release-note:feature
tfsdklog: Added `SubsystemWithOmitLogWithFieldKeys()`, `SubsystemWithOmitLogWithMessageRegex()`, `SubsystemWithOmitLogMatchingString()`, `SubsystemWithMaskFieldValueWithFieldKeys()`, `SubsystemWithMaskMessageRegex()`and `SubsystemWithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for SDK subsystem loggers
```
9 changes: 9 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,12 @@
# 0.5.0 (July 14, 2022)

FEATURES:

* tflog: Added `SubsystemWithOmitLogWithFieldKeys()`, `SubsystemWithOmitLogWithMessageRegex()`, `SubsystemWithOmitLogMatchingString()`, `SubsystemWithMaskFieldValueWithFieldKeys()`, `SubsystemWithMaskMessageRegex()` and `SubsystemWithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for provider subsystem loggers ([#71](https://github.com/hashicorp/terraform-plugin-log/issues/71))
* tflog: Added `WithOmitLogWithFieldKeys()`, `WithOmitLogWithMessageRegex()`, `WithOmitLogMatchingString()`, `WithMaskFieldValueWithFieldKeys()`, `WithMaskMessageRegex()` and `WithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for the provider root logger ([#71](https://github.com/hashicorp/terraform-plugin-log/issues/71))
* tfsdklog: Added `SubsystemWithOmitLogWithFieldKeys()`, `SubsystemWithOmitLogWithMessageRegex()`, `SubsystemWithOmitLogMatchingString()`, `SubsystemWithMaskFieldValueWithFieldKeys()`, `SubsystemWithMaskMessageRegex()`and `SubsystemWithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for SDK subsystem loggers ([#71](https://github.com/hashicorp/terraform-plugin-log/issues/71))
* tfsdklog: Added `WithOmitLogWithFieldKeys()`, `WithOmitLogWithMessageRegex()`, `WithOmitLogMatchingString()`, `WithMaskFieldValueWithFieldKeys()`, `WithMaskMessageRegex()` and `WithMaskLogMatchingString()` functions, which provide log omission and log masking filtering, based on message and argument keys, for the SDK root logger ([#71](https://github.com/hashicorp/terraform-plugin-log/issues/71))

# 0.4.1 (June 6, 2022)

NOTES:
Expand Down
49 changes: 38 additions & 11 deletions internal/hclogutils/args.go
@@ -1,7 +1,11 @@
package hclogutils

// MapsToArgs will shallow merge field maps into the slice of key1, value1,
// key2, value2, ... arguments expected by hc-log.Logger methods.
import (
"fmt"
)

// MapsToArgs will shallow merge field maps into a slice of key/value pairs
// arguments (i.e. `[k1, v1, k2, v2, ...]`) expected by hc-log.Logger methods.
func MapsToArgs(maps ...map[string]interface{}) []interface{} {
switch len(maps) {
case 0:
Expand All @@ -10,27 +14,50 @@ func MapsToArgs(maps ...map[string]interface{}) []interface{} {
result := make([]interface{}, 0, len(maps[0])*2)

for k, v := range maps[0] {
result = append(result, k)
result = append(result, v)
result = append(result, k, v)
}

return result
default:
mergedMap := make(map[string]interface{}, 0)
// Pre-allocate a map to merge all the maps into,
// that has at least the capacity equivalent to the number
// of maps to merge
mergedMap := make(map[string]interface{}, len(maps))

// Merge all the maps into one;
// in case of clash, only the last key is preserved
for _, m := range maps {
for k, v := range m {
mergedMap[k] = v
}
}

result := make([]interface{}, 0, len(mergedMap)*2)
// As we have merged all maps into one, we can use this
// same function recursively for the `switch case 1`.
return MapsToArgs(mergedMap)
}
}

for k, v := range mergedMap {
result = append(result, k)
result = append(result, v)
}
// ArgsToKeys will extract all keys from a slice of key/value pairs
// arguments (i.e. `[k1, v1, k2, v2, ...]`) expected by hc-log.Logger methods.
//
// Note that, in case of an odd number of arguments, the last key captured
// will refer to a value that does not actually exist.
func ArgsToKeys(args []interface{}) []string {
// Pre-allocate enough capacity to fit all the keys,
// i.e. all the elements in the input array in even position
keys := make([]string, 0, len(args)/2)

return result
for i := 0; i < len(args); i += 2 {
// All keys should be strings, but in case they are not
// we format them to string
switch k := args[i].(type) {
case string:
keys = append(keys, k)
default:
keys = append(keys, fmt.Sprintf("%s", k))
}
}

return keys
}
139 changes: 139 additions & 0 deletions internal/hclogutils/args_test.go
Expand Up @@ -171,3 +171,142 @@ func TestMapsToArgs(t *testing.T) {
})
}
}

func TestArgsToKeys(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
args []interface{}
expectedKeys []string
}{
"nil": {
args: []interface{}{},
expectedKeys: []string{},
},
"simple": {
args: []interface{}{
"map1-key1", "map1-value1",
"map1-key2", "map1-value2",
"map1-key3", "map1-value3",
},
expectedKeys: []string{
"map1-key1",
"map1-key2",
"map1-key3",
},
},
"non-even-number-of-args": {
args: []interface{}{
"map1-key1", "map1-value1",
"map1-key2", "map1-value2",
"map1-key3",
},
expectedKeys: []string{
"map1-key1",
"map1-key2",
"map1-key3",
},
},
"multiple-different-keys": {
args: []interface{}{
"map1-key1", "map1-value1",
"map1-key2", "map1-value2",
"map1-key3", "map1-value3",
"map2-key1", "map2-value1",
"map2-key2", "map2-value2",
"map2-key3", "map2-value3",
},
expectedKeys: []string{
"map1-key1",
"map1-key2",
"map1-key3",
"map2-key1",
"map2-key2",
"map2-key3",
},
},
"multiple-mixed-keys": {
args: []interface{}{
"key1", "map1-value1",
"key2", "map1-value2",
"key3", "map1-value3",
"key4", "map2-value4",
"key1", "map2-value1",
"key5", "map2-value5",
},
expectedKeys: []string{
"key1",
"key2",
"key3",
"key4",
"key1",
"key5",
},
},
"multiple-overlapping-keys": {
args: []interface{}{
"key1", "map1-value1",
"key2", "map1-value2",
"key3", "map1-value3",
"key1", "map2-value1",
"key2", "map2-value2",
"key3", "map2-value3",
},
expectedKeys: []string{
"key1",
"key2",
"key3",
"key1",
"key2",
"key3",
},
},
"multiple-overlapping-keys-shallow": {
args: []interface{}{
"key1", map[string]interface{}{
"submap-key1": "map1-value1",
"submap-key2": "map1-value2",
"submap-key3": "map1-value3",
},
"key2", "map1-value2",
"key3", "map1-value3",
"key1", map[string]interface{}{
"submap-key4": "map2-value4",
"submap-key5": "map2-value5",
"submap-key6": "map2-value6",
},
"key2", "map2-value2",
"key3", "map2-value3",
},
expectedKeys: []string{
"key1",
"key2",
"key3",
"key1",
"key2",
"key3",
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := hclogutils.ArgsToKeys(testCase.args)

if got == nil && testCase.expectedKeys == nil {
return // sortedGot will return []interface{}{} below, nil is what we want
}

sort.Strings(got)
sort.Strings(testCase.expectedKeys)

if diff := cmp.Diff(got, testCase.expectedKeys); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
105 changes: 105 additions & 0 deletions internal/logging/filtering.go
@@ -0,0 +1,105 @@
package logging

import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-log/internal/hclogutils"
)

const logMaskingReplacementString = "***"

// ShouldOmit takes a log's *string message and slices of arguments,
// and determines, based on the LoggerOpts configuration, if the
// log should be omitted (i.e. prevent it to be printed on the final writer).
func (lo LoggerOpts) ShouldOmit(msg *string, argSlices ...[]interface{}) bool {
// Omit log if any of the configured keys is found
// either in the logger implied arguments,
// or in the additional arguments
if len(lo.OmitLogWithFieldKeys) > 0 {
for _, args := range argSlices {
argKeys := hclogutils.ArgsToKeys(args)
if argKeysContain(argKeys, lo.OmitLogWithFieldKeys) {
return true
}
}
}

// Omit log if any of the configured regexp matches the log message
if len(lo.OmitLogWithMessageRegex) > 0 {
for _, r := range lo.OmitLogWithMessageRegex {
if r.MatchString(*msg) {
return true
}
}
}

// Omit log if any of the configured strings is contained in the log message
if len(lo.OmitLogWithMessageStrings) > 0 {
for _, s := range lo.OmitLogWithMessageStrings {
if strings.Contains(*msg, s) {
return true
}
}
}

return false
}

// ApplyMask takes a log's *string message and slices of arguments,
// and applies masking of keys' values and/or message,
// based on the LoggerOpts configuration.
//
// Note that the given input is changed-in-place by this method.
func (lo LoggerOpts) ApplyMask(msg *string, argSlices ...[]interface{}) {
if len(lo.MaskFieldValueWithFieldKeys) > 0 {
for _, k := range lo.MaskFieldValueWithFieldKeys {
for _, args := range argSlices {
// Here we loop `i` with steps of 2, starting from position 1 (i.e. `1, 3, 5, 7...`).
// We then look up the key for each argument, by looking at `i-1`.
// This ensures that in case of malformed arg slices that don't have
// an even number of elements, we simply skip the last k/v pair.
for i := 1; i < len(args); i += 2 {
switch argK := args[i-1].(type) {
case string:
if k == argK {
args[i] = logMaskingReplacementString
}
default:
if k == fmt.Sprintf("%s", argK) {
args[i] = logMaskingReplacementString
}
}
}
}
}
}

// Replace any part of the log message matching any of the configured regexp,
// with a masking replacement string
if len(lo.MaskMessageRegex) > 0 {
for _, r := range lo.MaskMessageRegex {
*msg = r.ReplaceAllString(*msg, logMaskingReplacementString)
}
}

// Replace any part of the log message equal to any of the configured strings,
// with a masking replacement string
if len(lo.MaskMessageStrings) > 0 {
for _, s := range lo.MaskMessageStrings {
*msg = strings.ReplaceAll(*msg, s, logMaskingReplacementString)
}
}
}

func argKeysContain(haystack []string, needles []string) bool {
for _, h := range haystack {
for _, n := range needles {
if n == h {
return true
}
}
}

return false
}

0 comments on commit 6c641bd

Please sign in to comment.