corso/src/pkg/logger/example_logger_test.go
ryanfkeepers 6ed54fad9c first pass on ServiceResource tuple compliance
First pass for updating code outside of the path package
to comply with ServiceResource tuple slices.  Compliance
is still incomplete, so the build and tests
will still fail.  Changes
in this PR are focused on the easier-to-make changes, mostly
generating ServiceResources where there were
previously manual declarations of service and resource.
2023-08-10 17:55:34 -06:00

207 lines
6.9 KiB
Go

package logger_test
import (
"context"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/resource"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------------------------------------------------------------------
// mock helpers
// ---------------------------------------------------------------------------
const itemID = "item_id"
var (
err error
itemPath, _ = path.Build(
"tid",
[]path.ServiceResource{{
Service: path.ExchangeService,
ProtectedResource: "own",
}},
path.ContactsCategory,
false,
"foo")
)
// ---------------------------------------------------------------------------
// examples
// ---------------------------------------------------------------------------
// ExampleSeed showcases seeding a logger into the context.
func Example_seed() {
// Before logging, a logger instance first needs to get seeded into
// the context. Seeding only needs to be done once. For example
// Corso's CLI layer seeds the logger in the cli initialization.
ctx := context.Background()
ls := logger.Settings{
File: logger.Stderr,
Level: logger.LLInfo,
PIIHandling: logger.PIIPlainText,
}
ctx, log := logger.Seed(ctx, ls)
// SDK consumers who configure their own zap logger can Set their logger
// into the context directly, instead of Seeding a new one.
ctx = logger.Set(ctx, log)
// logs should always be flushed before exiting whichever func
// seeded the logger.
defer func() {
_ = log.Sync() // flush all logs in the buffer
}()
// downstream, the logger will retrieve its configuration from
// the context.
func(ctx context.Context) {
log := logger.Ctx(ctx)
log.Info("hello, world!")
}(ctx)
}
// ExampleLoggerStandards reviews code standards around logging in Corso.
func Example_logger_standards() {
log := logger.Ctx(context.Background())
// 1. Keep messages short. When possible, messages should state the current action.
// Lowercase text, no ending punctuation.
// This ensures logs are easy to scan, and simple to grok.
//
// preferred
log.Info("getting item")
// avoid
log.Info("Getting one item from the service so that we can send it through the item feed.")
// 2. Avoid statements like "unable to...", "failed to..", or "error when...".
// Error level logs automatically imply a failure to do the action.
//
// preferred
log.With("err", err).Error("connecting to repo")
// avoid
log.With("err", err).Error("unable to connect to repo")
// 3. Do not fmt values into the message. Use With() or -w() to add structured data.
// By keeping dynamic data in a structured format, we maximize log readability,
// and make logs very easy to search or filter in bulk, and very easy to control pii.
//
// preferred
log.With("err", err).Error("getting item")
log.Errorw("getting item", "err", err)
// avoid
log.Errorf("getting item %s: %v", itemID, err)
// 4. Give data keys reasonable namespaces. Use snake_case.
// Overly generic keys can collide unexpectedly.
//
// preferred
log.With("item_id", itemID).Info("getting item")
// avoid
log.With("id", itemID).Error("getting item")
// 4. Avoid Warn-level logging. Prefer Info or Error.
// Minimize confusion/contention about what level a log
// "should be". Error during a failure, Info (or Debug)
// otherwise.
//
// preferred
log.With("err", err).Error("getting item")
// avoid
log.With("err", err).Warn("getting item")
// 5. Avoid Panic/Fatal-level logging. Prefer Error.
// Panic and Fatal logging can crash the application without
// flushing buffered logs and finishing out other telemetry.
//
// preferred
log.With("err", err).Error("connecting to repo")
// avoid
log.With("err", err).Panic("connecting to repo")
}
// ExampleLoggerCluesStandards reviews code standards around using the Clues package while logging.
func Example_logger_clues_standards() {
ctx := clues.Add(context.Background(), "foo", "bar")
log := logger.Ctx(ctx)
// 1. Clues Ctx values are always added in .Ctx(); you don't
// need to add them directly.
//
// preferred
ctx = clues.Add(ctx, "item_id", itemID)
logger.Ctx(ctx).Info("getting item")
//
// avoid
ctx = clues.Add(ctx, "item_id", itemID)
logger.Ctx(ctx).With(clues.In(ctx).Slice()...).Info("getting item")
// 2. The last func to handle a context must add the clues to the error.
//
// preferred
err := clues.Wrap(err, "reason").WithClues(ctx)
// this dereference added for linter happiness
_ = err
// 3. Always extract structured data from errors.
//
// preferred
log.With("error", err).Errorw("getting item", clues.InErr(err).Slice()...)
//
// avoid
log.Errorw("getting item", "err", err)
//
// you can use the logger helper CtxErr() for the same results.
// This helps to ensure all error values get packed into the logs
// in the expected format.
logger.CtxErr(ctx, err).Error("getting item")
// 3. Protect pii in logs.
// When it comes to protecting sensitive information, we only want
// to hand loggers (and, by extension, clues errors) using one of
// three approaches to securing values.
//
// First: plain, unhidden data. This can only be logged if we are
// absolutely assured that this data does not expose sensitive
// information for a user. Eg: internal ids and enums are fine to
// log in plain text. Everything else must be considered wisely.
//
// Second: manually concealed values. Strings containing sensitive
// info, and structs from external pacakges containing sensitive info,
// can be logged by manually wrapping them with a clues.Hide() call.
// Ex: clues.Hide(userName). This will hash the value according to
// the user's hash algorithm configuration.
//
// Third: managed string concealers. Certain values have common
// format and content, but appear commonly in the code as strings.
// Examples include URLs and kopia repository paths. These values
// may have a concealer built specifically for them to maximize the
// data we can view when debugging, instead of hashing the complete
// string. See graph/middleware.go LoggableURL{} and path/elements.go
// LoggableDir{}.
//
// Fourth: structs that comply with clues.Concealer. The Concealer
// interface requires a struct to comply with Conceal() (for cases
// where the struct is handed to a clues aggregator directly), and
// fmt's Format(state, verb), where the assumption is the standard
// format writer will be replaced with a Conceal() call (for cases
// where the struct is handed to some non-compliant formatter/printer).
//
// preferred
log.With(
// internal type, safe to log plainly
"resource_type", resource.Users,
// string containing sensitive info, wrap with Hide()
"user_name", clues.Hide("your_user_name@microsoft.example"),
// string partially concealed by a managed concealer.
"request_url", graph.LoggableURL("https://corsobackup.io"),
// a concealer-compliant struct, safe to add plainly
"storage_path", itemPath)
}