shadow

package
v0.1.23 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package shadow compares a candidate channel against the live one.

The comparison itself is the easy part. The whole value is in two guarantees, and both are enforced by construction rather than by configuration:

  • A shadow cannot deliver. It holds a transformation pipeline and no senders, so there is no code path from here to a receiver. A setting cannot create one and a mistake cannot enable one.
  • A shadow cannot affect the live channel. It is called after the live message has been delivered and acknowledged, its panics are recovered, and its timeouts are its own.

Anything less than that and shadow mode becomes a way to break production while trying to avoid breaking production.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Identify

func Identify(raw []byte) (controlID, msgType string)

identify pulls the control ID and type out of a message, for the report. Identify is exported so retroactive replay can label a message the same way a shadow difference does. The two features report on the same messages, and a control ID formatted one way in one report and another way in the other is a small thing that makes somebody doubt both.

Types

type DiffOptions

type DiffOptions struct {
	// Compare limits the comparison to these paths. Empty means every populated field in
	// either message.
	Compare []string
	// Ignore skips these paths, keyed upper case. Timestamps and control IDs belong here:
	// they differ on every message and say nothing about whether a change is safe.
	Ignore map[string]bool
}

DiffOptions narrows what a comparison looks at.

type Difference

type Difference struct {
	At        time.Time `json:"at"`
	ControlID string    `json:"controlId,omitempty"`
	Type      string    `json:"messageType,omitempty"`

	// Kind is filter, transform, candidate-error or live-error.
	Kind string `json:"kind"`

	// Fields lists the paths that differ, with both values.
	Fields []FieldDifference `json:"fields,omitempty"`

	// Note explains a difference that is not field-level.
	Note string `json:"note,omitempty"`
}

Difference is one message the two versions disagreed about.

type FieldDifference

type FieldDifference struct {
	Path      string `json:"path"`
	Live      string `json:"live"`
	Candidate string `json:"candidate"`
}

FieldDifference is one path where the two versions disagree.

func Diff

func Diff(live, candidate []byte, opts DiffOptions) []FieldDifference

Diff reports where two versions of the same message disagree.

Extracted from the shadow runner so that retroactive replay can use it. Both features are asking the same question - did this change alter the output - and answering it twice would eventually produce two answers, which for a tool whose whole job is to be trusted about safety would be worse than having one of them.

func DiffX12

func DiffX12(live, candidate []byte, opts DiffOptions) []FieldDifference

func DiffXML

func DiffXML(live, candidate []byte, opts DiffOptions) []FieldDifference

DiffXML reports where two XML documents disagree.

Attributes are compared as well as element text, and that is the whole point: in v3 a value is almost always an attribute, so a diff comparing only text would report two documents as identical when the birth date, the gender and every identifier had changed.

type Result

type Result struct {
	// Accepted is false when the filter declined the message.
	Accepted bool
	// RejectedBy names what declined it.
	RejectedBy string
	// Message is the transformed message, when it was accepted.
	Message []byte
}

Result is what a pipeline made of one message.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner compares one candidate against the live channel.

func New

func New(channel string, cfg *config.Shadow, dataType config.DataType, live, candidate Transformer) *Runner

New builds a runner.

dataType decides the comparison. Passing it explicitly rather than defaulting to HL7 is deliberate: the previous signature had no way to say, so every shadow compared with the v2 walker regardless of what the channel carried.

func (*Runner) Differences

func (r *Runner) Differences() []Difference

Differences returns a copy of the recorded differences.

func (*Runner) Observe

func (r *Runner) Observe(parent context.Context, raw []byte)

Observe runs one message through both pipelines and records the comparison.

Called after the live message has been delivered and acknowledged. It returns nothing, because there is nothing the caller should do differently based on the result: a shadow exists to inform a person, not to change what happens to a message.

func (*Runner) Stats

func (r *Runner) Stats() Stats

Stats returns a copy of the counters.

func (*Runner) Verdict

func (r *Runner) Verdict() string

Verdict summarises whether the candidate looks safe to promote.

Phrased as an observation rather than an approval. Nothing here can know whether a difference is the intended one; only that there is one, or that there is not.

type Stats

type Stats struct {
	// Compared is how many messages went through both pipelines.
	Compared int64 `json:"compared"`
	// Skipped is how many were not sampled.
	Skipped int64 `json:"skipped"`
	// Same is how many produced identical output.
	Same int64 `json:"same"`
	// Differed is how many produced different output.
	Differed int64 `json:"differed"`
	// FilterDisagreed is counted separately, because it is the most consequential
	// difference available: one version keeps a message the other drops.
	FilterDisagreed int64 `json:"filterDisagreed"`
	// CandidateFailed is how many the candidate could not process at all.
	CandidateFailed int64 `json:"candidateFailed"`
	// LiveFailed is how many the live channel could not process, which is not the
	// candidate's fault and must not be reported as though it were.
	LiveFailed int64 `json:"liveFailed"`
	// CandidateSlower is the candidate's total time minus the live channel's.
	CandidateExtra time.Duration `json:"candidateExtraNanos"`
	StartedAt      time.Time     `json:"startedAt"`
	LastCompared   time.Time     `json:"lastCompared,omitempty"`
}

Stats summarises a shadow run.

type Transformer

type Transformer interface {
	// TransformOnly applies the filter and the transformation pipeline and returns
	// the result. It must not deliver anything.
	TransformOnly(ctx context.Context, raw []byte) (Result, error)
}

Transformer is the part of a channel a shadow needs.

Deliberately this narrow. A shadow is handed the ability to transform a message and nothing else - no senders, no queue, no store - so that "the shadow cannot deliver" is a fact about the type rather than a promise in a comment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL