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 log filtering functionality #71

Merged
merged 6 commits into from Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Although it looks like this should never happen, if len(args) is an odd/uneven number, then a key with no corresponding value could be appended to keys.

Perhaps for i := 0; i + 1 < len(args); i += 2 { could be used to avoid such a scenario?

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 added tests for this as well

// 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 {
detro marked this conversation as resolved.
Show resolved Hide resolved
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 {
detro marked this conversation as resolved.
Show resolved Hide resolved
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
}