Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Info

As you start learning about these mechanisms, you will slowly start noticing that Go’s native fmt.Stringer interface (and the String() method) becomes less and less relevant in your code — none of the logging or error code ever uses it if your objects implement SafeFormatter or SafeValue. In fact, we are likely to slowly phase out String() methods over time.

Examples

Before

After

Code Block
languagego
// type MetricSnap does not implement SafeFormat and its representation
// as string is thus considered fully unsafe by default.

func (m MetricSnap) String() string {
        suffix := ""
        if m.ConnsRefused > 0 {
                suffix = fmt.Sprintf(", refused %d conns", m.ConnsRefused)
        }
        return fmt.
SPrintf
Sprintf("infos %d/%d sent/received, bytes %dB/%dB sent/received%s",
                m.InfosSent, m.InfosReceived,
                m.BytesSent, m.BytesReceived,
                suffix)
}
Code Block
languagego
// SafeFormat implements the redact.SafeFormatter interface.
func (m MetricSnap) SafeFormat(w redact.SafePrinter, _ rune) {
        // Notice how similar the code below is to the original code on the
        // left. The SafePrinter API has been designed to make it easy
        // to “migrate” existing String() methods into SafeFormat().
        //
        // Why this “does the right thing” without special annotations:
        // - The format string for w.Printf() is a literal constant and considered safe.
        // - The numeric arguments are simple integers and thus considered safe.
        // As a result, the entire string produced is automatically considered
        // safe. No special “this is safe” annotations are needed.
        w.Printf("infos %d/%d sent/received, bytes %dB/%dB sent/received",
                m.InfosSent, m.InfosReceived,
                m.BytesSent, m.BytesReceived)
        if m.ConnsRefused > 0 {
                w.Printf(", refused %d conns", m.ConnsRefused)
        }
}

func (m MetricSnap) String() string {
        // StringWithoutMarkers applies the SafeFormat method
        // then removes the redaction markers to produce a “flat” string.
        // This helps avoid code duplication between String()
        // and SafeFormat().
        //
        // Note: The resulting String() method is only rarely
        // called, since most relevant uses of MetricSnap
        // will now use .SafeFormat() directly.
        return redact.StringWithoutMarkers(m)
}
Code Block
languagego
// type OutgoingConnStatus does not implement SafeFormat and its representation
// as string is thus considered fully unsafe by default.

func (c OutgoingConnStatus) String() string {(w redact.SafePrinter, _ rune) {
        return fmt.Printf("%d: %s (%s: %s)",
                c.NodeID, c.Address,
                roundSecs(time.Duration(c.AgeNanos)), c.MetricSnap)
}
Code Block
languagego
// SafeFormat implements the redact.SafeFormatter interface.
func (c OutgoingConnStatus) SafeFormat(w redact.SafePrinter, _ rune) {
        // Notice how similar the code below is to the original code on the
        // left. The SafePrinter API has been designed to make it easy
        // to “migrate” existing String() methods into SafeFormat().
        //
        // Why this “does the right thing” without special annotations:
        // - The format argument is a literal constant and considered safe.
        // - c.NodeID is a roachpb.NodeID,
        //   which aliases a basic integer type and implements SafeValue() and is considered safe.
        // - c.Address is a string and is unsafe.
        // - roundSecs() returns a time.Duration and this type has been registered as safe.
        // - c.MetricSnap implements a SafeFormat method, which is called implicitly to "do the right thing".
        // The resulting string contains a mix of safe/unsafe information:
        // the address is marked as unsafe, the rest is safe.
        w.Printf("%d: %s (%s: %s)",
                c.NodeID, c.Address,
                roundSecs(time.Duration(c.AgeNanos)), c.MetricSnap)
}

// This String() method is defined via SafeFormat(). See explanation in the other example above.
func (c OutgoingConnStatus) String() string { return redact.StringWithoutMarkers(c) }
Code Block
languagego
type Gossip struct {
   ...
   // lastConnectivity remembers the connectivity details
   // across calls to the LogStatus() method.
   lastConnectivity string
}
// LogStatus logs the current status of gossip such as the incoming and
// outgoing connections.
func (g *Gossip) LogStatus() {
        // The log call below should only report the connectivity
        // if it is different from the last call to LogStatus().
        var connectivity string
        if s := g.Connectivity().String(); s != g.lastConnectivity {
                g.lastConnectivity = s
                connectivity = s
        }

        log.Infof(ctx, "gossip status: %s", connectivity)
}

Code Block
languagego
type Gossip struct {
   ...
   // lastConnectivity remembers the connectivity details
   // across calls to the LogStatus() method.
   lastConnectivity redact.RedactableString
}
// LogStatus logs the current status of gossip such as the incoming and
// outgoing connections.
func (g *Gossip) LogStatus() {
        // g.Connectivity() returns an object that implements SafeFormat().
        // Its redactable representation contains a mix of safe and unsafe information.
        //
        // (Again, notice how the code below is similar to the code on the left.)
        var connectivity redact.RedactableString
        if s := redact.Sprint(g.Connectivity()); s != g.lastConnectivity {
                g.lastConnectivity = s
                connectivity = s
        }

        log.Infof(ctx, "gossip status: %s", connectivity)
}

When to use SafeFormatter vs SafeValue

...

An axiom is an argument expressed in the code that a bit of information is safe or unsafe in a way that provably always true regardless of which data is processed by CockroachDB. Axioms thus have the same general quality as proofs and are thus superior to promises. We prefer axioms where the argument that it makes can be verified locally at the position in the code where it is made, without relying on knowledge pulled from elsewhere.

For example:

Bad

Good

Code Block
languagego
func foo(s string) RedactableString {
  // Casting an arbitrary string to RedactableString
  // is a PROMISE: only the programmer knows
  // that s does not contain redaction markers
  // and that the string concatenation is guaranteed
  // not to leak information.
  //
  // The promise can easily be broken “by accident” if
  // a new call is made to foo() with a broken
  // string as input.
  return RedactableString("hello ‹" + s + "›") 
}
Code Block
languagego
func foo(s string) RedactableString {
  // The redact.Sprintf function is a PROOF:
  // its algorithm guarantees that the unsafe
  // information in s will be properly annotated
  // in the result, without confidentiality leak.
  return redact.Sprintf("hello %s", s) 
}

Code Block
languagego
type myStruct { s string }

func (m *myStruct) foo(v string) {
   m.s = string(redact.Sprintf("hello %s", v))
}

func (m *myStruct) bar() string {
  // PROMISE: m.s has not be modified since foo() was called,
  // and is thus known (only to the programmer) to still be
  // properly redactable.
  //
  // The promise can easily be broken “by accident” if
  // another programmer adds a separate method that modifies
  // m.s.
  return redact.RedactableString(m.s).Redact().StripMarkers()
}
Code Block
languagego
type myStruct { s redact.ReadactableString }

func (m *myStruct) foo(v string) {
   m.s = redact.Sprintf("hello %s", v)
}

func (m *myStruct) bar() string {
  // PROOF: as long as the type rules are obeyed, m.s
  // will have remained redactable ever since it was
  // constructed.
  return m.s.Redact().StripMarkers()
}

Redactability in error objects

...