Documentation
¶
Overview ¶
Package shadowflow runs a new code path alongside an existing one on a sample of traffic, diffs their results, and logs the differences — optionally encrypting the logged values to avoid leaking sensitive data.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type EncryptionService ¶
EncryptionService encrypts the diff values logged by a ShadowFlow so they don't leak sensitive data in plain text.
Implementations must not embed plainText or key material in the error they return from Encrypt: ShadowFlow logs that error's type on failure, but never trusts its message, since a message like "failed to encrypt %q with key %x" would leak the exact data encryption is meant to protect.
type KeyFingerprinter ¶
type KeyFingerprinter interface {
KeyFingerprint() string
}
KeyFingerprinter is implemented by EncryptionService implementations that have an associated key, so a ShadowFlow can log which key encrypted a value. This lets old log lines be matched to the right private key after key rotation.
type NoopEncryptionService ¶
type NoopEncryptionService struct{}
NoopEncryptionService is a version of the EncryptionService that doesn't perform any encryption, it only encodes the differences as a base64 string as defined in RFC 4648.
func NewNoopEncryptionService ¶
func NewNoopEncryptionService() *NoopEncryptionService
NewNoopEncryptionService creates a NoopEncryptionService.
type Option ¶
type Option func(*config) error
Option configures optional ShadowFlow settings. Pass options to New.
func WithEncryptionService ¶
func WithEncryptionService(encryptionService EncryptionService) Option
WithEncryptionService enables logging of the changed values, encrypted with the given service. Without it only the names of the differing fields are logged.
func WithLogger ¶
WithLogger routes the shadow flow logs to the given logger instead of slog.Default().
func WithMaxConcurrentShadows ¶
WithMaxConcurrentShadows caps the number of shadow flows running at the same time; sampled calls beyond the cap are skipped, never queued, so a slow new flow cannot pile up goroutines. Defaults to 100.
func WithPlaintextProperties ¶
func WithPlaintextProperties() Option
WithPlaintextProperties logs the differing field paths in plain text next to the encrypted values. By default an encryption service suppresses them, because diff paths include map keys, which may themselves be sensitive.
func WithShadowTimeout ¶
WithShadowTimeout bounds each shadow flow call: the context passed to the new flow is cancelled after the given duration. Without it, shadow flows get a default timeout of 10 seconds; use WithoutShadowTimeout to run them unbounded instead.
func WithoutShadowTimeout ¶
func WithoutShadowTimeout() Option
WithoutShadowTimeout disables the default shadow timeout, so the shadow flow runs until it returns on its own. A hung new flow then holds its concurrency slot indefinitely; prefer WithShadowTimeout unless the new flow is already known to be bounded.
type PublicKeyEncryptionService ¶
type PublicKeyEncryptionService struct {
// contains filtered or unexported fields
}
PublicKeyEncryptionService is a struct that represents the EncryptionService for encrypting data using a public key. The encryption process uses SHA-256 as the hash function.
func NewPublicKeyEncryptionService ¶
func NewPublicKeyEncryptionService(publicKey *rsa.PublicKey) (*PublicKeyEncryptionService, error)
NewPublicKeyEncryptionService creates a PublicKeyEncryptionService that encrypts with the given RSA public key. The key must be at least 2048 bits; smaller RSA keys provide inadequate encryption strength.
func (*PublicKeyEncryptionService) Encrypt ¶
func (e *PublicKeyEncryptionService) Encrypt(plainText string) (string, error)
Encrypt encrypts plainText with RSA-OAEP using the configured public key and returns the result base64-encoded.
func (*PublicKeyEncryptionService) KeyFingerprint ¶
func (e *PublicKeyEncryptionService) KeyFingerprint() string
KeyFingerprint returns a short, stable identifier for the configured public key (the first 8 bytes of the SHA-256 hash of its DER encoding), so log lines can be matched to the private key that can decrypt them.
type ShadowFlow ¶
type ShadowFlow[T any] struct { // contains filtered or unexported fields }
ShadowFlow runs a new code path alongside an existing one on a sample of traffic, diffs their results, and logs what changed.
func New ¶
New creates a ShadowFlow for the given instance name, sampling percentage (0-100), and options.
func (*ShadowFlow[T]) Compare ¶
func (s *ShadowFlow[T]) Compare(ctx context.Context, currentFlow, newFlow func(context.Context) (*T, error)) (*T, error)
Compare runs the current flow and, based on a random percentage, may also run the new flow. If the new flow is run, it compares the results of the current and new flows, logs the differences, and optionally encrypts and logs the changed values if an encryption service is provided. It always returns the result of the current flow.
The context is passed to currentFlow as-is. The new flow runs in the background on a context derived with context.WithoutCancel, so it keeps the request's values (trace IDs) but is not cancelled together with the request; it is instead bounded by a default timeout of 10 seconds unless overridden with WithShadowTimeout, or left unbounded with WithoutShadowTimeout.
Both results are normalised through a JSON round-trip before comparison, so the caller may mutate the returned value right away and only differences that survive encoding/json are reported: unexported fields and fields tagged `json:"-"` are never compared.
currentFlow: A function that when called with ctx, returns the result of the current flow. newFlow: A function that when called with ctx, returns the result of the new flow.
If currentFlow returns a nil result with a nil error, the shadow comparison is skipped entirely (newFlow is not called) since there is nothing meaningful to diff against; this is logged at debug level.
Returns: The result of the current flow.
func (*ShadowFlow[T]) CompareSlices ¶
func (s *ShadowFlow[T]) CompareSlices(ctx context.Context, currentFlow, newFlow func(context.Context) ([]T, error)) ([]T, error)
CompareSlices is the slice-returning counterpart to Compare: it runs currentFlow, samples the traffic percentage to decide whether to also run newFlow, and logs the differences between the two slice results.
func (*ShadowFlow[T]) Wait ¶
func (s *ShadowFlow[T]) Wait()
Wait blocks until every in-flight shadow comparison has finished. Call it on graceful shutdown so pending diffs are not lost when the process exits.
Stop issuing Compare calls before calling Wait: sync.WaitGroup requires that an Add starting from a zero counter happens before Wait, so shut down the traffic source (e.g. the HTTP server) first and drain the shadow flows last.