Repository navigation
Replies: 4 comments
Revision 1: every name humus defines starts with
|
| In the post | Now |
|---|---|
fit.evaluation, the span event |
humus.fit.evaluation |
fit.strip, the span event |
humus.fit.strip |
fit.point |
humus.fit.point |
fit.rule |
humus.fit.rule |
fit.delay |
humus.fit.delay |
fit.abort |
humus.fit.abort |
Wherever the post names one of them, in its prose, its sketches and its tests, read the new name. That includes the Summary, Telemetry, Injection points, Design decisions, Testing and Migration from today. Where Background quotes bedrock's "the fit.evaluation observer", it quotes bedrock, which left the name to humus.
These keep their names:
- OpenTelemetry's own, such as
error.type,error.messageandfeature_flag.evaluation, whichhumus.fit.evaluationis modelled on. - The end user's baggage member,
acme.faultin the sketches. Its key is the end user's to name, andjsonscenario.Newtakes it. - The point names, such as
http:payments:8443. They are values, not names.
Nothing else changes. Every open question is still open.
Revision 2: the open questions answered, and a fault is a real failure to retries and breakersThe open questions:
These changes weren't prototyped: the prototype showed Stop and Ignored. The stories' tests, listed in Testing below, cover the new behaviour. Each section below replaces or adds to the part of the same name in the post. SummaryThe third bullet becomes:
Injection pointsIn the table, the RabbitMQ The bullet on Kafka gains a sentence. The point moves to just after A fault is a real failureThis section replaces A fault belongs to its request. Every client decides an attempt that a fault ended with its own classifier, as it decides any other attempt. The fault emulates a failure of the dependency, and the classifier sees that failure:
No client's classifier changes. The stories add only the point, and the RabbitMQ abort's mark. Design decisions"A fault belongs to its request: see above, and open question 1" becomes:
The bullet on
TestingThe prototype's first sketch, and its results for a delay, gRPC, Kafka and RabbitMQ, showed Stop and Ignored, which this revision drops. The stories' tests show the following instead, each with the default policy:
"Every client's breaker ignores a delay cut short, as it ignores an abort" becomes: every client classifies an abort and a delay cut short as A fault is a real failure's table says. Changes to other files
Out of scope"Failing a dependency for every request, or opening a breaker for every caller" becomes:
ReferencesTwo references are added:
|
Revision 3: a delayed HTTP call that
|
Closing: failure injection has landedThe revisions' stories are done:
The revisions left no open questions. A fault is a real failure to the retries and breakers of the client that meets it. The rest is out of scope or belongs to other proposals, as "Out of scope" lists with revision 2's changes:
|
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation; see Proposal: humus on the bedrock process model, wave 5. It builds on bedrock's
fit, on the wave-3 servers (HTTP server foundation, gRPC services, jobs, queue processing and the Kafka runtime), and on the wave-4 clients (outbound HTTP clients, outbound gRPC clients, producing to Kafka, RabbitMQ). It depends on no other wave-5 proposal. Whom a server trusts is decided without Proposal: authentication and authorization, so this proposal stays in wave 5, as the charter's revision 7 allows. It records no instrument, so it doesn't depend on Proposal: humus's own metrics either.Summary
Failure injection lets one request fail on purpose, in production or in a test, without failing anyone else's. A request carries a failure scenario in its OpenTelemetry baggage. Each injection point the request passes through looks itself up in the scenario and learns whether to fail. bedrock's
fitships that model and leaves the conventions to humus. This proposal sets them:fault, builds an App's failure injection. The end user gives it afit.Decoder, which owns the wire format, and aMaxDelay. The App builds it beforeInit, from a function the end user passes withhumus.WithFaults, and hands it to every package inSetup.Faults. Without that option, failure injection is off and nothing changes.httpclient,grpcclient, the KafkaProducerand the RabbitMQPublisherevaluate a point named after their dependency, such ashttp:payments:8443orkafka:orders. Each emulates an abort the way its protocol fails, inside the breaker and the attempt's deadline.httpserver,grpcserverand the Kafka and RabbitMQ consumers. Each gets aTrustScenariofunc over its protocol's own request, set in code. The default trusts no caller.ForwardScenariosays that dependency takes part. Other baggage members always go through.fit.evaluationspan event. Every strip is afit.stripevent. An aborted attempt fails with a*fault.InjectedError, whoseerror.typeis the same in every client's spans and inotelhttp's metrics.fault/jsonscenario, carries a scenario as JSON in one baggage member whose key the end user names. Nothing makes an end user use it.Background
fitv0.22.0 to v0.25.0 provideScenario,Rule,Fault, theDecoder/Encodersplit andNewEvaluator(decoder, maxDelay, observe).fitis unchanged between v0.24.0 and v0.25.0. Its doc gives a table of which call belongs at startup and which per request. The implementation comment on z5labs/bedrock#578 names what is left to humus: "A signed codec, thefit.evaluationobserver, per-protocol injection points and Kafka header carriage." The review on #578 moved every wire convention into a caller-supplied codec. Its reason was that an organisation's edge, such as Envoy, Kong or Zuul, is usually owned by another team, and can't be made to produce bedrock's format.Evaluatorfrom settings, injection point names and what an abort looks like per protocol, and stripping at every server and in every client. OpenTelemetry without globals left "stripping untrusted baggage, which Feature request: request-scoped failure injection (Netflix FIT) on OpenTelemetry baggage bedrock#578 calls for" to it.telemetry/sdk.go#L56).httpserverwraps every handler inotelhttp, andgrpcserverusesotelgrpc's stats handler. Both extract it. The Kafka and RabbitMQ consumers extract it from each message's headers (kafka/service.go#L800,rabbitmq/service.go#L825). A job extracts it fromBAGGAGEin its environment (job/service.go#L105). Every client writes it back out.httpserver, that is inside the chain it wraps around the handler (service.go#L71). Forgrpcserver, it is after humus's recovery and before the end user's interceptors (internal/server/server.go#L106). For the consumers, it is between the extraction andProcess(kafka/service.go#L817).httpclient, it is around the call to thehttp.Transport, inside the attempt timeout (client.go#L119). Ingrpcclient, it is afterAllowand beforecc.Invokeorcc.NewStream(retry.go#L76,#L101). The KafkaProducermarks its seam before the breaker (producer.go#L294). The RabbitMQPublishermarks its seam inside its attempt (publisher.go#L586).rest/rest.go#L256-L261), which is the exposure chore(deps): update module github.com/cenkalti/backoff/v5 to v7 #578's Trust section describes.Proposed API
Each server gains a
TrustScenariofield, and each client gains aForwardScenariofield:faultandfault/jsonscenarioare in the core module. Its new dependency isgithub.com/z5labs/bedrock/fitv0.25.0, which needs only the OpenTelemetry API, already in the core module, and testify. Thegrpc,queue/kafkaandqueue/rabbitmqmodules reachfaultthrough the core module.Building it
The end user's function sources the settings, as in the Summary, with whatever variable names the service uses. It is given the
Setup, so aDecodercan load what it needs throughs.Host, such as a registry of scenario IDs from a file, and register a refresher withs.OnStop.Run calls it after telemetry and before
Init, because every server and client copiess.Faultswhen it is built. If the field were set inInit, a server built before the line that set it would strip nothing. The function runs once, and its failure is reported asfailure injection: …, through the lifecycle's ERROR record, likeInit's.Injection points
Each client evaluates one point per attempt, inside its breaker, so an attempt the breaker rejects evaluates nothing:
fit.evaluationevent goeshttpclienthttp:<host>:<port>ofBaseURL, with the scheme's port when the URL has none, such ashttp:payments:8443otelhttp's*url.Errorwrapping the*fault.InjectedError, with no responseAttemptTimeout, so it is in the attempt's span and inhttp.client.request.durationgrpcclientgrpc:<service>/<method>: the full method without its leading slash, asrpc.methodwrites it, such asgrpc:payments.v1.Payments/Chargeotelgrpcstarts the attempt's span inside grpc-goUnavailable, wrapping the*fault.InjectedError. The attempt gets a span of the Client's own, named and attributed as a rejected attempt's isTimeout; cut short, it ends as grpc-go would,DeadlineExceededorCanceledProducerkafka:<topic>, one evaluation per recordDeliveryTimeoutPublisherrabbitmq:<messaging.destination.name>:<exchange>:<routing key>, leaving out what is empty, such asrabbitmq:orders:acceptedPublish's spanrabbitmq: publishing to …: fit: injected failure at …Timeouthttp:payments:8443orhttp:*, and the names line up with the trace. A gRPC point is per method, sogrpc:payments.v1.Payments/*fails one service. A Kafka point is per topic. HTTP has no route template on the client side, so its point is the origin.*fault.InjectedError. This matches howProducereturns one error and how the breaker counts one call. The point moves from before the breaker, where Proposal: producing to Kafka #764 marked it, to just afterAllow, so a Kafka abort is placed like every other client's.s.Faults.Eval(ctx, "postgresql:orders").Inject(ctx). The docs page suggests naming such a point<system>:<name>, after the protocol's*.systemattribute.A fault belongs to its request
An attempt that a fault ended is an abort, or a delay that the attempt's deadline or its caller cut short. Every client decides such an attempt itself, without asking the end user's
Classify:So the call returns at once with the
*fault.InjectedError, and the breaker's counts don't move. The retry budget isn't touched either, because a Stop outcome draws no token. An attempt that waited out its delay and then went to the dependency is classified as any other attempt is.This is
fit's model: "Only requests that carry a scenario are affected, so a failure can be injected into production traffic one request at a time." Under the default policy, five aborted attempts in a row would open a shared breaker and shed every other caller's requests to that dependency. The retries would also draw down the budget that real traffic shares. A scenario that should fail a dependency for everyone is a chaos experiment, not FIT, and it is out of scope.The wave-4 proposals described the seam as one where "an injected abort is classified, counted and retried as a real failure would be". This proposal changes that, and its stories make the change in each client. Open question 1 asks whether that is right.
Stripping at servers
Each server strips a scenario right after it extracts the baggage, and before any of the end user's code runs, when the caller isn't trusted:
TrustScenariois givenhttpserverotelhttp, so a panic inTrustScenariois recorded as the handler's would be*http.Requestgrpcserverpeer.FromContextand the incoming metadata, and its full methodProcessMessageProcessMessageTrustScenariois asked only about a request that carries a scenario, and only when the App has failure injection. It costs nothing on other requests. A stripped request's span gets afit.stripevent.nil trusts no caller. A server that the end user hasn't decided about is a trust boundary. So an API on the internet strips every scenario without any setting, and only a server the end user marks takes them.
Trust needs nothing from authentication. Every protocol already gives the end user something to trust:
r.TLS.VerifiedChains, orcredentials.TLSInfofrom the gRPC peer.Message.UserID. RabbitMQ validatesuser-idagainst the publishing connection's user, so it names the publisher.A trust func is the end user's code, and may call whatever the end user has, such as a verify function of their own. This proposal doesn't depend on Proposal: authentication and authorization. Its strip runs before any handler or interceptor of the end user's, so a principal which that proposal puts in the context isn't there yet when
TrustScenariois asked.A job trusts its environment. The
BAGGAGEthat a job's run joins comes from its pod spec or whatever started it, and whoever sets that already sets every one of its settings.jobgets no field and no code change, only a paragraph in its doc. A job started by a service hands on that service's baggage, which that service's own server has already stripped.The probes servers need nothing.
httphealth's andgrpchealth's own servers extract no baggage, as exposing health probes noted. An attached handler is behind its Service's strip, like every other request.Stripping at clients
Each client strips the scenario from what it sends unless
ForwardScenariois set. It evaluates its point first, because the point reads the scenario that is about to be stripped:httpclientstrips in the injection layer. Afterotelhttphas written the attempt's headers, the layer clones the request, deletes the propagator's fields, and injects them again from the stripped context. That gives the attempt's span context and every baggage member except the scenario's. Proposal: outbound HTTP clients #762 put this strip in the retry layer, but there the point, which is inside, would no longer see the scenario.grpcclientstrips the attempt's context beforecc.Invokeorcc.NewStream, andotelgrpcinjects from it.Producerstrips the context that each record's headers are injected from. The evaluation still uses the record's own context.Publisherstrips the context its message's headers are injected from.A stripped attempt's span gets a
fit.stripevent. Stripping is the default for the same reason servers trust no one by default. A dependency the end user hasn't decided about is a trust boundary. It may be a third party that would only log the scenario, or a service of another team with a different codec.Telemetry
The observer is a span event, modelled on OpenTelemetry's
feature_flag.evaluation. It is added totrace.SpanFromContext(ctx), so theInjectorneeds no provider and touches no global. Every evaluation on a request that carries a scenario adds afit.evaluationevent:fit.point;fit.rule(the rule'sPoint),fit.delay(the clamped Delay, in seconds) andfit.abort;error.typeanderror.message. The request goes on unharmed, asfitrequires.An unmatched evaluation still lists the point, which is how
fitdiscovers the points a request passes through.A strip is a
fit.stripevent, with no attributes, on the span where it happens. The span's kind tells a server's strip from a client's.An aborted attempt's
error.typeisgithub.com/z5labs/humus/fault.InjectedErrorin every client, on spans and inotelhttp'shttp.client.request.duration.InjectedErrorimplementsErrorType, becauseotelhttp's metrics name an error by its reflected type whilesemconv.ErrorTypenames a pointer*fault.InjectedError. The prototype found that mismatch. One value lets a query find every injected failure across protocols.No log records and no instruments. Evaluations happen only on requests under test, and each is on the request's trace. The failure an abort causes is already recorded by the client's own span, and by its metrics where a request was sent. An instrument of evaluations is out of scope.
fit.is humus's namespace, ascircuit.is for the breakers' records. OpenTelemetry has no convention for failure injection.The codec
fault/jsonscenariois opt-in. Its grammar is{"rules":[{"point":…,"delay_ms":…,"abort":…}]}:pointis required.delay_msis a non-negative whole number of milliseconds, which any language can write.abortdefaults to false."abrot"is an error the observer reports, not a rule that silently spares its point.Encoderejects a member over 2048 bytes percent-encoded, a quarter of W3C's 8192-byte guarantee. otel-go'sbaggage.Parsedrops all of a header's baggage when it is over the limit, so an oversized scenario would wipe every other member downstream.Encoderejects a Delay that isn't whole milliseconds.Decodereads any size that arrives.TrustScenarioand stripping at the edge. An end user who wants a signed format, a registry of scenario IDs, or Envoy's static two-member form writes afit.Decoder, as bedrock's own examples do. The ones infit'scodec_test.goare each under 60 lines.The edge, or a test, writes a scenario with
fit.ContextWithScenario(ctx, codec, scenario), and the propagator carries it on. humus doesn't decide which requests are under test.Panics
Nothing is recovered beyond what each package already recovers. A panic in
TrustScenariois handled the way its server handles a panic of the end user's code.httpserverandgrpcserverrecord it and fail that request, and the consumers recover nothing, as for aProcessor. A panic in theDecoderpasses throughfitto the goroutine that evaluated it. A panic infaultitself is humus's bug.Design decisions
fault.fitwould clash with bedrock's package at every call site that uses both, andfit.Scenarioandfit.Decoderare the types an end user writes.fault.Injector,fault.Settingsandfault.InjectedErrorread plainly next to them. The charter calls the work "failure injection".Injectoris onSetup, likeTelemetry. Every server must strip, and every client must evaluate. If theInjectorwere passed in each package'sSettings, a server the end user forgot would strip nothing, and the scenario would reach clients that honor it. That hole can't be seen in review. OnSetup, no package can miss it, and Apps that don't use failure injection pay nothing. It is built by a function given theSetuprather than byInit, so it exists before the first server is built. Open question 4.Decoderis the end user's, and has no default. That is chore(deps): update module github.com/cenkalti/backoff/v5 to v7 #578's ruling: the organisation owns the wire format.jsonscenariois offered the wayopenapi/v3/jsoncodecis, as a ready-made choice that nothing requires.jsonscenarioisn'tjsoncodec. An edge service that serves an OpenAPI API and writes scenarios would import both, and two packages namedjsoncodecwould need an alias.ClassifyandBreakerPolicy. One App-wide policy over a common caller type would be a closed set of protocols in the core module, and couldn't name gRPC's peer or a KafkaMessagewithout importing those modules. A bool would trust every caller of a server or none of them, and one port often serves both a mesh and a gateway.fit.evaluationevent goes on the span in the point's context. Forhttpclientand the KafkaProducer, that is the span of the attempt or record that the fault fails. Forgrpcclient, it is the caller's, because grpc-go gives no hook betweenotelgrpc's span and the transport. The Client's own span for an aborted attempt still records the failure.InjectedErrorcovers a delay cut short. The client needs to know that the fault ended the attempt, so the breaker can ignore it. The caller still sees the deadline it hit, throughUnwrap. It isn'tfit.ErrInjected, whichfitdefines for an abort.MaxDelayis the operator's knob. A Decoder that rejects every scenario, such as a registry with no entries, turns it off in effect without a rebuild.Testing
A prototype of this proposal passed the tests below under
go test -race -count=3, on Go 1.27.0 andfitv0.25.0, against main at 265d361. It coveredfault,jsonscenario, the root package,httpserver,httpclient,grpcserver,grpcclient, the KafkaProducerandService, and the RabbitMQPublisher. Each test except RabbitMQ's runs whole Apps inside atesting/synctestbubble, on one humustestHost, over TLS fromhumustest.NewCA. Backoffs, delays and timeouts take no real time. Kafka's run against kfake, which franz-go keeps. RabbitMQ's run thePublisheron the package's unit-test stand-in, since humus keeps no broker. The RabbitMQ consumer's strip is the Kafka consumer's, and the prototype compiled it without running it, because that needs a container. Every existing unit test in the four modules still passed. The one exception wasgrpcserver's export hygiene test, which caught the prototype's wrapped stream type, so the stories put the interceptors ingrpc/internal/server, beside the streams interceptor. The prototype was in a scratchpad and is not kept, and the sketches below are trimmed from it.The service under test,
orders, serves an API withhttpserverand calls a fakepaymentsdependency withhttpclient. The test is the edge: it sends a request whose baggage carries a scenario and atenantmember.Ten aborted requests leave the breaker closed. A counted fault would have opened it after five:
With
TrustScenarionil, the scenario never reaches the handler, and the dependency still sees the other members:The prototype's tests also showed the following:
ForwardScenario. With it false, a trusted scenario that sparespaymentsreachesorders' handler, butpaymentssees noacme.faultmember, and the attempt's span has afit.stripevent. With it true,paymentssees the member. Either way, it seestenant=acmeand atraceparentof the attempt's span.http:payments:8443makes the attempt's span at least 1s long, and the call succeeds. An 8s delay is cut short byAttemptTimeoutat 5s, after one attempt. The handler gets an*fault.InjectedErrorthat matchescontext.DeadlineExceeded, and notfit.ErrInjected.humus.WithFaults, nothing is stripped, nothing is evaluated, and the scenario reachespaymentsas any baggage does.http.client.request.durationdata point witherror.typegithub.com/z5labs/humus/fault.InjectedErrorand a count of 10.frontis agrpcserverwhose handler callsbackendthroughgrpcclient.grpc:grpc.testing.TestService/EmptyCallreturnsUnavailableto the edge, ten times, and a clean call still goes through.error.type, and itsfit.evaluationevent is onfront's server span.fit.stripon the server span.ForwardScenarioworks as for HTTP.BAGGAGE, set withhumustest.WithEnv, aborts tenProducecalls tokafka:orders. Nothing reaches the topic, each record's span has the evaluation and theerror.type, and an eleventh call with no scenario is stored.ForwardScenariofalse, the stored record'sbaggageheader hastenantand noacme.fault.TrustScenarionil handsProcessa context without the scenario. Withfunc(m kafka.Message) bool { return m.Topic == topic }, it hands over the scenario.Publishcalls torabbitmq:orders:accepteddial nothing, and an eleventh is confirmed.ForwardScenariodecides whether the published headers carry the member.The stories add the tests the prototype didn't have:
fault.CheckMaxDelayaccepts a positive value and rejects the rest without naming the field.Newnames each bad field, and reports a nilDecoderand a badMaxDelaytogether. The zeroInjectorevaluates nothing and strips nothing. A rejected scenario's event haserror.typeanderror.message, and its request proceeds. A Delay overMaxDelayis clamped, and its event shows the clamped value. An unmatched evaluation hasfit.pointalone.Stripkeeps every other member.Faultsruns after telemetry and beforeInit, with the sameSetup. A*config.Errorfrom it exits 78, and any other error exits 1, with one ERROR record and the OnStop functions run. A nilFaultsleaves the zeroInjector.jsonscenario. It round-trips throughpropagation.Baggage. Unknown fields, a missing point, a negativedelay_msand trailing data are errors.Encoderejects a member over 2048 bytes and a Delay that isn't whole milliseconds.Newrejects an invalid key.Stripleaves other members alone.UserID, keeps its scenario.Each new package has an
Example, and is held to the 80% coverage gate.Changes to other files
This proposal owns failure injection, so its stories make these:
App.Faults,WithFaultsandSetup.Faults. Run builds theInjectorbetween telemetry andInit. Main's doc listsFaults: noneamong its defaults.go.modgainsgithub.com/z5labs/bedrock/fitv0.25.0, and so do thego.modfiles of thegrpc,queue/kafkaandqueue/rabbitmqmodules, through the core module.httpserver:Settings.TrustScenario, and the strip layer.httpclient:Settings.ForwardScenario, the injection layer inside the attempt timeout, and the client deciding an attempt a fault ended.grpc/grpcserverandgrpc/internal/server:Settings.TrustScenario, and the strip interceptors.grpc/grpcclient:Settings.ForwardScenario, the point inattemptandopen, the aborted attempt's span, and the Client deciding an attempt a fault ended.queue/kafka:Settings.TrustScenario,ProducerSettings.ForwardScenario, the point moved afterAllow, and the classifier ignoring an injected call. The marked seam's comment is updated.queue/rabbitmq:Settings.TrustScenario,PublisherSettings.ForwardScenario, the point inattempt, and both classifiers. The marked seam's comment is updated.job: a paragraph in its doc saying that the run'sBAGGAGEis trusted, and why.docs/content/failure-injection.md. It covers:humus.WithFaultsand sourcingMaxDelay;UserIDexamples;ForwardScenario;error.type;Host, a job'sBAGGAGE, and a Kafka record'sbaggageheader.Faultsto the startup order, after telemetry and beforeInit.telemetry,health,httphealth,grpchealth,openapi/v3andhumustestdon't change. A test turns failure injection on withhumustest.WithApp(humus.WithFaults(…)). A unit test of the end user's code that takes aSetupsetsSetup.Faultsfromfault.New.Migration from today
humus.WithFaults, afit.Decoder, andMaxDelayTrustScenariodoesn't trust; other members are keptotelhttpforwarded all baggage to every dependencyForwardScenariois sets.Faults.Evalfor the end user's ownfit.evaluationandfit.stripspan events, and oneerror.typefor injected failuresThere was nothing else to migrate.
Open questions
fit's promise and what production use needs. The alternative is what the wave-4 proposals sketched: retry and count it like a real failure. A scenario could then show retries and an opening breaker, but a few test requests would shed real traffic and drain the shared budget. A middle way retries and still ignores it in the breaker, which keeps the budget leak. Recommendation: Stop and Ignored. The clients' own tests already cover their retries and breakers against real failures.jsonscenario? As proposed, humus ships one opt-in codec: JSON in one member whose key the end user names, with no signature and no expiry, because trust comes fromTrustScenario. The alternative ships no codec, leaving every end user to write aDecoderas bedrock's examples do, or ships a signed codec too, which every edge would then have to compute. Recommendation: shipjsonscenarioalone. A service and its test harness need a shared format to start with, and a signature needs key distribution that humus doesn't own.TrustScenariotrusts no caller, and a client withForwardScenariofalse strips. Taking part across services means marking each server and each client on the path. The alternative trusts and forwards by default, which is less to set but exposes every internet-facing server that forgot. Recommendation: fail safe.InjectoronSetup, built beforeInit? As proposed,App.Faultsbuilds it and every package takes it from theSetup. The alternative passes it explicitly, in a field of every server's and client'sSettings, with no change to the root package. That is more explicit, but a missed server is a silent hole. Recommendation: onSetup.http:<host>:<port>,grpc:<service>/<method>,kafka:<topic>andrabbitmq:<exchange>:<routing key>. HTTP could instead leave out the port when it is the scheme's default, which is shorter but gives one dependency two spellings. Recommendation: as proposed, always with the port.Out of scope
fit.ContextWithScenarioand anEncoder.Decoderof the end user's.fithas onlyAbort, and chore(deps): update module github.com/cenkalti/backoff/v5 to v7 #578 left error kinds out.grpcclient'sConnected, none of which serves a request.TrustScenariofunc may do it with whatever the end user has.References
fitv0.25.0, and z5labs/bedrock#578: its review, its startup and per-request table, and what it leaves to humusTRACEPARENTandBAGGAGEthrough the SDK's propagator), queue processing and the Kafka runtime, outbound HTTP clients, outbound gRPC clients, producing to Kafka, RabbitMQ, exposing health probesfeature_flag.evaluationevent, whichfit.evaluationis modelled onAll reactions