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 ¶
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 ¶
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.
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.