Skip to content

Instantly share code, notes, and snippets.

@haleyrc
Last active March 13, 2019 13:46
Show Gist options
  • Save haleyrc/03e22dffd9920e978666a7a9be26c71a to your computer and use it in GitHub Desktop.
Save haleyrc/03e22dffd9920e978666a7a9be26c71a to your computer and use it in GitHub Desktop.
An experiment in safer logging.
// This gist is an experiment toward safer logs that don't leak PII or secrets. We present three different logging
// implementations and outline the pros and cons of both. Ultimately, however, the "safeish" version provides the
// best mix of developer-friendliness and safety.
//
// Note that none of the safety mechanisms here apply in cases where fields of a data type are logged directly, but
// if log.Printf(data.Password) passes code review, there are other problems.
//
// A runnable version of this is available at:
// https://play.golang.org/p/_jePSMkWM1O
package main
import (
"fmt"
)
// mydata is an example of a typical data structure that contains a mix of sensitive and non-sensitive fields.
type mydata struct {
Password string
Name string
}
func (md mydata) Anonymize() interface{} {
return mydata{Password: "", Name: md.Name}
}
// Logger is our "standard" logging interface. In reality it would have all of the methods we need for
// structured logging in production (Debugf, Infof, Criticalf, etc.)
type Logger interface {
Printf(format string, v ...interface{})
}
// SafeLogger is the stricter version that only takes types that satisfy Anonymize. If we use this interface,
// every call to Printf would need to be modified to wrap built-in types and user-defined types would have to
// be updated en masse to satisfy the Anonymizer interface.
type SafeLogger interface {
Printf(format string, v ...Anonymizer)
}
// unsafe implements the Logger interface and is similar to any standard logging package.
type unsafe struct{}
func (s unsafe) Printf(format string, v ...interface{}) {
fmt.Printf(format, v...)
}
// safe wraps a Logger and satisfies the SafeLogger interface. If any v are passed that don't implement Anonymizer,
// however, the program will fail to compile, leading to requirements such as SafeString below which is much more
// explicit, but also unfriendly.
type safe struct{ l Logger }
func (s safe) Printf(format string, v ...Anonymizer) {
var ifs []interface{}
for _, a := range v {
ifs = append(ifs, a.Anonymize())
}
s.l.Printf(format, ifs...)
}
// Anonymizer SHOULD be implemented by all user defined data types even if they are just passthroughs and should
// mask or remove sensitive information not suitable for logging.
type Anonymizer interface {
Anonymize() interface{}
}
// safeString is required for the SafeLogger interface since built-in types obviously don't satisfy the Anonymizer
// interface. This (and simiilar versions for other types), would have to be used for *every* call to the Logger
// implementation.
type safeString string
func (s safeString) Anonymize() interface{} {
return s
}
func SafeString(s string) Anonymizer {
return safeString(s)
}
// safeish is a compromise that allows our types to satisfy the Anonymizer interface if they require it, but
// doesn't impose any unwieldy requirements on developers logging built-in types.
type safeish struct{}
func (s safeish) Printf(format string, v ...interface{}) {
av := make([]interface{}, 0, len(v))
for _, iface := range v {
a, ok := iface.(Anonymizer)
if ok {
av = append(av, a.Anonymize())
} else {
av = append(av, iface)
}
}
fmt.Printf(format, av...)
}
func main() {
data := mydata{Password: "oops", Name: "world"}
// The unsafe version successfully logs user-defined and built-in types,
// but we don't have any way of signaling information that shouldn't
// appear in logs.
fmt.Println("Unsafe:")
var ul Logger = unsafe{}
ul.Printf("\t%#v\n", data)
ul.Printf("\tHello, %s!\n", "world")
// The strict version makes it explicit that our user-defined types should
// implement the Anonymizer interface, but since built-in types don't, we
// are left to wrap every built-in type (or even user-defined types from
// external packages) that handles Anonymizing or passing through.
fmt.Println("Strict Safe:")
var sl SafeLogger = safe{ul}
sl.Printf("\t%#v\n", data)
// Won't compile because strings aren't safe
// sl.Printf("\tHello, %s!\n", "world")
sl.Printf("\tHello, %s!\n", SafeString("world"))
// The safe-ish version accepts the same arguments as the unsafe version, but
// checks each argument to see *if* it implements the Anonymizer interface, and
// uses that version of the data if it does. We don't impose any hard restrictions
// on what can be passed, but we also don't make the logger ungainly to use and
// types that we know are dangerous just need to implement one interface.
fmt.Println("Safe-ish:")
var sil Logger = safeish{}
sil.Printf("\t%#v\n", data)
sil.Printf("\tHello, %s!\n", "world")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment