Skip to content

Commit 1e1323b

Browse files
Mask sensitive information from the logs (FF-2514) (#55)
* Mask sensitive information from the logs (FF-2514) * feat: [add ability to users to provide a ApplicationLogger implementation in their preference library (zap, logrus, etc)] (FF-1940) * Add generic application logger interface, zap implementation and documentation for adding custom implementation * add masking to scrubbing logger * fix readme * instantiate once * add test for masking url parameters in a different order * fixup * rename package to v5 * update readme * required param
1 parent a614319 commit 1e1323b

18 files changed

+320
-61
lines changed

README.md

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ In your `go.mod`, add the SDK package as a dependency:
2222

2323
```
2424
require (
25-
github.com/Eppo-exp/golang-sdk/v4
25+
github.com/Eppo-exp/golang-sdk/v5
2626
)
2727
```
2828

2929
Or you can install the SDK from the command line with:
3030

3131
```
32-
go get github.com/Eppo-exp/golang-sdk/v4
32+
go get github.com/Eppo-exp/golang-sdk/v5
3333
```
3434

3535
## Quick start
@@ -40,7 +40,7 @@ Begin by initializing a singleton instance of Eppo's client. Once initialized, t
4040

4141
```go
4242
import (
43-
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
43+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient"
4444
)
4545

4646
var eppoClient *eppoclient.EppoClient
@@ -62,7 +62,7 @@ func main() {
6262

6363
```go
6464
import (
65-
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
65+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient"
6666
)
6767

6868
var eppoClient *eppoclient.EppoClient
@@ -107,7 +107,7 @@ The code below illustrates an example implementation of a logging callback using
107107

108108
```go
109109
import (
110-
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
110+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient"
111111
"gopkg.in/segmentio/analytics-go.v3"
112112
)
113113

@@ -133,6 +133,68 @@ func main() {
133133
}
134134
```
135135

136+
## Provide a custom logger
137+
138+
If you want to provide a logging implementation to the SDK to capture errors and other application logs, you can do so by passing in an implementation of the `ApplicationLogger` interface to the `InitClient` function.
139+
140+
You can use the `eppoclient.ScrubbingLogger` to scrub PII from the logs.
141+
142+
```go
143+
import (
144+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient"
145+
"github.com/sirupsen/logrus"
146+
)
147+
148+
type LogrusApplicationLogger struct {
149+
logger *logrus.Logger
150+
logLevel logrus.Level
151+
}
152+
153+
func NewLogrusApplicationLogger(logLevel logrus.Level) *LogrusApplicationLogger {
154+
logger := logrus.New()
155+
logger.SetFormatter(&logrus.JSONFormatter{})
156+
return &LogrusApplicationLogger{logger: logger, logLevel: logLevel}
157+
}
158+
159+
func (l *LogrusApplicationLogger) Debug(args ...interface{}) {
160+
if l.logLevel <= logrus.DebugLevel {
161+
l.logger.Debug(args...)
162+
}
163+
}
164+
165+
func (l *LogrusApplicationLogger) Info(args ...interface{}) {
166+
if l.logLevel <= logrus.InfoLevel {
167+
l.logger.Info(args...)
168+
}
169+
}
170+
171+
func (l *LogrusApplicationLogger) Warn(args ...interface{}) {
172+
if l.logLevel <= logrus.WarnLevel {
173+
l.logger.Warn(args...)
174+
}
175+
}
176+
177+
func (l *LogrusApplicationLogger) Error(args ...interface{}) {
178+
if l.logLevel <= logrus.ErrorLevel {
179+
l.logger.Error(args...)
180+
}
181+
}
182+
183+
func main() {
184+
// Initialize a custom logger; example using logrus
185+
// Set log level to Info
186+
applicationLogger := NewLogrusApplicationLogger(logrus.InfoLevel)
187+
scrubbingLogger := eppoclient.NewScrubbingLogger(applicationLogger)
188+
189+
// Initialize the Eppo client
190+
eppoClient, _ = eppoclient.InitClient(eppoclient.Config{
191+
SdkKey: "<your_sdk_key>",
192+
AssignmentLogger: assignmentLogger,
193+
ApplicationLogger: scrubbingLogger
194+
})
195+
}
196+
```
197+
136198
### De-duplication of assignments
137199

138200
The SDK may see many duplicate assignments in a short period of time, and if you
@@ -145,15 +207,15 @@ It can be configured with a maximum size to fit your desired memory allocation.
145207

146208
```go
147209
import (
148-
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
210+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient"
149211
)
150212

151213
var eppoClient *eppoclient.EppoClient
152214

153215
func main() {
154216
assignmentLogger := NewExampleAssignmentLogger()
155217

156-
eppoClient = eppoclient.InitClient(eppoclient.Config{
218+
eppoClient, _ = eppoclient.InitClient(eppoclient.Config{
157219
ApiKey: "<your_sdk_key>",
158220
// 10000 is the maximum number of assignments to cache
159221
// Depending on the length of your flag and subject keys, taking a median
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package applicationlogger
2+
3+
type Logger interface {
4+
Debug(args ...interface{})
5+
Info(args ...interface{})
6+
Warn(args ...interface{})
7+
Error(args ...interface{})
8+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package applicationlogger
2+
3+
import (
4+
"regexp"
5+
)
6+
7+
type ScrubbingLogger struct {
8+
innerLogger Logger
9+
}
10+
11+
func NewScrubbingLogger(innerLogger Logger) *ScrubbingLogger {
12+
return &ScrubbingLogger{innerLogger: innerLogger}
13+
}
14+
15+
func (s *ScrubbingLogger) scrub(args ...interface{}) []interface{} {
16+
scrubbedArgs := make([]interface{}, len(args))
17+
for i, arg := range args {
18+
strArg, ok := arg.(string)
19+
if ok {
20+
strArg = maskSensitiveInfo(strArg)
21+
scrubbedArgs[i] = strArg
22+
} else {
23+
scrubbedArgs[i] = arg
24+
}
25+
}
26+
return scrubbedArgs
27+
}
28+
29+
// maskSensitiveInfo replaces sensitive information (like apiKey or sdkKey)
30+
// in the error message with 'XXXXXX' to prevent exposure of these keys in
31+
// logs or error messages.
32+
func maskSensitiveInfo(errMsg string) string {
33+
// Scrub apiKey and sdkKey from error messages containing URLs
34+
// Matches any string that starts with apiKey or sdkKey followed by any characters until the next & or the end of the string
35+
re := regexp.MustCompile(`(apiKey|sdkKey)=[^&]*`)
36+
return re.ReplaceAllString(errMsg, "$1=XXXXXX")
37+
}
38+
39+
func (s *ScrubbingLogger) Debug(args ...interface{}) {
40+
s.innerLogger.Debug(s.scrub(args...)...)
41+
}
42+
43+
func (s *ScrubbingLogger) Info(args ...interface{}) {
44+
s.innerLogger.Info(s.scrub(args...)...)
45+
}
46+
47+
func (s *ScrubbingLogger) Warn(args ...interface{}) {
48+
s.innerLogger.Warn(s.scrub(args...)...)
49+
}
50+
51+
func (s *ScrubbingLogger) Error(args ...interface{}) {
52+
s.innerLogger.Error(s.scrub(args...)...)
53+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package applicationlogger
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func Test_maskSensitiveInfo(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
expected string
14+
}{
15+
{
16+
name: "mask apiKey",
17+
input: "https://example.com?apiKey=123456&anotherParam=foo",
18+
expected: "https://example.com?apiKey=XXXXXX&anotherParam=foo",
19+
},
20+
{
21+
name: "mask sdkKey",
22+
input: "https://example.com?sdkKey=abcdef&anotherParam=foo",
23+
expected: "https://example.com?sdkKey=XXXXXX&anotherParam=foo",
24+
},
25+
{
26+
name: "no sensitive info",
27+
input: "https://example.com?param=value&anotherParam=foo",
28+
expected: "https://example.com?param=value&anotherParam=foo",
29+
},
30+
{
31+
name: "mask apiKey and sdkKey",
32+
input: "https://example.com?apiKey=123456&sdkKey=abcdef&anotherParam=foo",
33+
expected: "https://example.com?apiKey=XXXXXX&sdkKey=XXXXXX&anotherParam=foo",
34+
},
35+
{
36+
name: "mask apiKey and sdkKey out of order",
37+
input: "https://example.com?anotherParam=foo&apiKey=123456&sdkKey=abcdef",
38+
expected: "https://example.com?anotherParam=foo&apiKey=XXXXXX&sdkKey=XXXXXX",
39+
},
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
result := maskSensitiveInfo(tt.input)
45+
assert.Equal(t, tt.expected, result)
46+
})
47+
}
48+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package applicationlogger
2+
3+
import (
4+
"go.uber.org/zap"
5+
)
6+
7+
/**
8+
* The default logger for the SDK.
9+
*/
10+
type ZapLogger struct {
11+
logger *zap.Logger
12+
}
13+
14+
func NewZapLogger(logger *zap.Logger) *ZapLogger {
15+
return &ZapLogger{logger: logger}
16+
}
17+
18+
func (z *ZapLogger) Debug(args ...interface{}) {
19+
z.logger.Sugar().Debug(args...)
20+
}
21+
22+
func (z *ZapLogger) Info(args ...interface{}) {
23+
z.logger.Sugar().Info(args...)
24+
}
25+
26+
func (z *ZapLogger) Warn(args ...interface{}) {
27+
z.logger.Sugar().Warn(args...)
28+
}
29+
30+
func (z *ZapLogger) Error(args ...interface{}) {
31+
z.logger.Sugar().Error(args...)
32+
}

eppoclient/bandits_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func Test_bandits_sdkTestData(t *testing.T) {
6161
logger := new(mockLogger)
6262
logger.Mock.On("LogAssignment", mock.Anything).Return()
6363
logger.Mock.On("LogBanditAction", mock.Anything).Return()
64-
client := newEppoClient(configStore, nil, nil, logger)
64+
client := newEppoClient(configStore, nil, nil, logger, applicationLogger)
6565

6666
tests := readJsonDirectory[banditTest]("test-data/ufc/bandit-tests/")
6767
for file, test := range tests {

eppoclient/client.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package eppoclient
33
import (
44
"fmt"
55
"time"
6+
7+
"github.com/Eppo-exp/golang-sdk/v5/eppoclient/applicationlogger"
68
)
79

810
type Attributes map[string]interface{}
@@ -14,14 +16,22 @@ type EppoClient struct {
1416
configRequestor *configurationRequestor
1517
poller *poller
1618
logger IAssignmentLogger
19+
applicationLogger applicationlogger.Logger
1720
}
1821

19-
func newEppoClient(configurationStore *configurationStore, configRequestor *configurationRequestor, poller *poller, assignmentLogger IAssignmentLogger) *EppoClient {
22+
func newEppoClient(
23+
configurationStore *configurationStore,
24+
configRequestor *configurationRequestor,
25+
poller *poller,
26+
assignmentLogger IAssignmentLogger,
27+
applicationLogger applicationlogger.Logger,
28+
) *EppoClient {
2029
return &EppoClient{
2130
configurationStore: configurationStore,
2231
configRequestor: configRequestor,
2332
poller: poller,
2433
logger: assignmentLogger,
34+
applicationLogger: applicationLogger,
2535
}
2636
}
2737

@@ -32,6 +42,7 @@ func (ec *EppoClient) GetBoolAssignment(flagKey string, subjectKey string, subje
3242
}
3343
result, ok := variation.(bool)
3444
if !ok {
45+
ec.applicationLogger.Error("failed to cast %v to bool", variation)
3546
return defaultValue, fmt.Errorf("failed to cast %v to bool", variation)
3647
}
3748
return result, err
@@ -44,6 +55,7 @@ func (ec *EppoClient) GetNumericAssignment(flagKey string, subjectKey string, su
4455
}
4556
result, ok := variation.(float64)
4657
if !ok {
58+
ec.applicationLogger.Error("failed to cast %v to float64", variation)
4759
return defaultValue, fmt.Errorf("failed to cast %v to float64", variation)
4860
}
4961
return result, err
@@ -56,6 +68,7 @@ func (ec *EppoClient) GetIntegerAssignment(flagKey string, subjectKey string, su
5668
}
5769
result, ok := variation.(int64)
5870
if !ok {
71+
ec.applicationLogger.Error("failed to cast %v to int64", variation)
5972
return defaultValue, fmt.Errorf("failed to cast %v to int64", variation)
6073
}
6174
return result, err
@@ -68,6 +81,7 @@ func (ec *EppoClient) GetStringAssignment(flagKey string, subjectKey string, sub
6881
}
6982
result, ok := variation.(string)
7083
if !ok {
84+
ec.applicationLogger.Error("failed to cast %v to string", variation)
7185
return defaultValue, fmt.Errorf("failed to cast %v to string", variation)
7286
}
7387
return result, err
@@ -183,16 +197,19 @@ func (ec *EppoClient) getAssignment(config configuration, flagKey string, subjec
183197

184198
flag, err := config.getFlagConfiguration(flagKey)
185199
if err != nil {
200+
ec.applicationLogger.Info("failed to get flag configuration: %v", err)
186201
return nil, err
187202
}
188203

189204
err = flag.verifyType(variationType)
190205
if err != nil {
206+
ec.applicationLogger.Info("failed to verify flag type: %v", err)
191207
return nil, err
192208
}
193209

194-
assignmentValue, assignmentEvent, err := flag.eval(subjectKey, subjectAttributes)
210+
assignmentValue, assignmentEvent, err := flag.eval(subjectKey, subjectAttributes, ec.applicationLogger)
195211
if err != nil {
212+
ec.applicationLogger.Info("failed to evaluate flag: %v", err)
196213
return nil, err
197214
}
198215

@@ -202,7 +219,7 @@ func (ec *EppoClient) getAssignment(config configuration, flagKey string, subjec
202219
defer func() {
203220
r := recover()
204221
if r != nil {
205-
fmt.Println("panic occurred:", r)
222+
ec.applicationLogger.Error("panic occurred: %v", r)
206223
}
207224
}()
208225

0 commit comments

Comments
 (0)