mesh

package
v0.0.0-...-32720a6 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package mesh implements the Benzene Mesh design (the main repo's docs/specification/mesh.md, originally extracted from this package's earlier docs/design/mesh.md): a service's self-description (Descriptor) derived from its live Registry (what it provides, §2) and its live OutboundRegistry (what it consumes, §2.3) - including per-topic request/response JSON Schemas derived at startup from the registered types, and the contract hash that makes drift detectable (schema.go) - a reserved-topic interception middleware that serves that descriptor, and a trace middleware (trace.go) that turns every pipeline invocation into a semantic TraceEvent handed to an Exporter - either the zero-setup LogExporter (exporter.go) or the batching PushExporter (push.go) that feeds a collector over the mesh:* wire topics (wire.go), with span propagation for cross-service trace joins (span.go). The meshd package implements the collector side, where the declared Descriptor - not trace parentage - is the producer/consumer graph's sole source (mesh.md §4); traces there are an observed, additive signal for liveness and drift (§4.2), never for graph membership.

Every feed this package provides is independent and optional, and unavailability degrades the mesh rather than the service. A deployment that provisions only the trace feed - for example, when the descriptor endpoint is withheld pending a security review - still yields a reduced mesh (live stats and flows, no catalog entries), and a descriptor-only deployment yields the reverse. Concretely: Describe with a nil Registry or a nil OutboundRegistry returns a descriptor without that half of the catalog and records the missing feed in Degraded; TraceMiddleware with a nil Exporter is a pass-through; and a panicking or failing exporter never affects the invocation it observed.

Index

Constants

View Source
const (
	TopicRegister  = "benzene:mesh:register"
	TopicHeartbeat = "benzene:mesh:heartbeat"
	TopicTraces    = "benzene:mesh:traces"
	TopicIssues    = "benzene:mesh:issues"

	TopicQueryFleet   = "benzene:mesh:query:fleet"
	TopicQueryService = "benzene:mesh:query:service"
	TopicQueryTopic   = "benzene:mesh:query:topic"
	TopicQueryTrace   = "benzene:mesh:query:trace"
)

The mesh wire-contract topics (mesh.md §4): what a service sends to a collector (register/heartbeat/traces) and what a view reads back (query:*). The collector side is implemented by the meshd package; the names live here because both sides of the wire share them. They are namespaced under the benzene: default-service-standard prefix (design-principles.md §5.1) and match the .NET reference's MeshTopics/BenzeneTopic, so other language ports interoperate.

View Source
const (

	// ClassificationContractDrift is the issue classification reserved for collector/reader-
	// derived issues (mesh.md §4.1): a descriptor-hash mismatch, schema divergence, or - the
	// case mesh.md §4.2 defines - a trace naming a topic absent from the caller's declared
	// Consumes or the handler's declared Topics. ClassifyIssue's precedence table never
	// produces it; a collector (meshd) assigns it directly when it detects one of these cases.
	ClassificationContractDrift = "contract-drift"
)

Issue classification vocabulary (mesh.md §4.1) - a closed set assigned by the normative precedence table in ClassifyIssue. The other five are never produced outside that table, so they stay unexported; ClassificationContractDrift is exported because a collector assigns it directly, outside the precedence table (see its own doc).

View Source
const FeedOutboundRegistry = "outbound-registry"

FeedOutboundRegistry names the consumed-topic feed in Descriptor.Degraded: the OutboundRegistry the descriptor's Consumes list is derived from (mesh.md §2.3).

View Source
const FeedRegistry = "registry"

FeedRegistry names the topic-catalog feed in Descriptor.Degraded: the Registry the descriptor's topic list is derived from.

View Source
const TopicID = "benzene:mesh"

TopicID is the reserved topic intercepted by Middleware to serve the ServiceDescriptor (mesh.md §1). It is namespaced under the benzene: default-service-standard prefix (design-principles.md §5.1) and matches the .NET reference's BenzeneTopic.Mesh.

Variables

This section is empty.

Functions

func ClassifyIssue

func ClassifyIssue(status, exceptionType string) string

ClassifyIssue assigns an issue classification from a failing invocation's Benzene status and captured exception type, by the normative precedence of mesh.md §4.1 (evaluated in order):

  1. bad-request / validation-error -> validation (even with an exception type present - a deserialization or argument failure is still a validation issue);
  2. an exception type present -> exception (classify by the throw, not the mapped status);
  3. not-found / unauthorized / forbidden / not-implemented, or an empty status -> config-wiring (a wiring gap);
  4. service-unavailable / timeout / too-many-requests -> dependency;
  5. unexpected-error -> exception;
  6. any other failing status -> unclassified (an honest fallback beats a lying class).

func IssueFingerprint

func IssueFingerprint(service, topic, version, classification, discriminator string) string

IssueFingerprint derives the normative issue fingerprint of mesh.md §4.1: the lowercase hex of the first 16 bytes of SHA-256 over the UTF-8 bytes of service|topic|version|classification| discriminator (pipe-joined), where version is the empty string when absent and discriminator is the exception type when present, else the status. transport is deliberately excluded - the same failure over two transports is one issue.

func IssueMiddleware

func IssueMiddleware(info ServiceInfo, exporter IssueRecorder) benzene.Middleware

IssueMiddleware observes every invocation that passes through it and records a failure signature to exporter for each unsuccessful outcome (a framework failure, an application-defined failure, or an empty status - the wiring gap). Successful invocations produce nothing. Register it outermost alongside TraceMiddleware so it sees intercepted invocations too.

A nil exporter returns a pass-through middleware: the issue feed is simply off and the service behaves identically to an unmeshed one (mesh.md §4.1: optional on both sides). Recording can never fail, slow, or block the invocation it observed - Record holds only a brief lock and the exporter's send runs on its own goroutine.

func Middleware

func Middleware(descriptor Descriptor, aliases ...string) benzene.Middleware

Middleware intercepts the reserved benzene:mesh topic (plus any additional aliases) and short-circuits the pipeline with descriptor, exactly as the healthcheck package does for its reserved topic. Interception is by topic ID alone, ignoring version, matching healthcheck's behavior. Any other topic passes through to next unchanged.

Registering this middleware is what "provisions the descriptor endpoint" - a deployment that must not expose it (e.g. pending security review) simply leaves it out of the Pipeline, and the trace feed keeps working independently.

func RegisterOutbound

func RegisterOutbound[TReq, TRes any](r *OutboundRegistry, topic benzene.Topic) error

RegisterOutbound records topic as a message this service may send: the request type it sends as TReq, and the response type it expects back as TRes (mesh.md §2.3). A sender with no expected response type registers TRes as `any` - schema derivation already maps an interface type to `{}` (unconstrained), which is exactly the responseSchema mesh.md §2 specifies for "no declared response type".

Returns an error if topic is already registered - the same startup-error treatment Register gives a duplicate inbound registration.

func SpecHandler

func SpecHandler(descriptor Descriptor) http.Handler

SpecHandler serves the service's derived spec document over HTTP for the Cloud Service Profile's R5 (docs/specification/cloud-service-profile.md §2). The default service standard mounts it at httpbinding.SpecPath ("/benzene/spec"; design-principles.md §5.2).

The spec document IS the service Descriptor - the registry-derived topic catalog with request/response JSON schemas (mesh.md §5.1). The profile does not mandate a particular spec format (OpenAPI, AsyncAPI, or Benzene's own), only that a real derived document is exposed; the descriptor is exactly that, and because it is derived from the registry it can never go stale (design-principles.md §3). This is the same truth the reserved benzene:mesh topic serves for R6, offered here as a plain GET surface for tooling that wants the spec without speaking the wire envelope.

Only GET is served (405 otherwise). Provisioning it is opt-in, exactly like the descriptor Middleware: a deployment that must not expose the spec (e.g. pending a security review, design-principles.md §5.1) simply doesn't mount it, and the rest of the mesh keeps working.

func TraceMiddleware

func TraceMiddleware(info ServiceInfo, exporter Exporter) benzene.Middleware

TraceMiddleware observes every invocation that passes through it and hands the resulting TraceEvent to exporter after downstream middleware finishes. Register it outermost (before healthcheck/mesh/router interception) so it sees every invocation, including intercepted ones. Because the router converts missing handlers, conversion failures, and handler panics into Results rather than Go errors, every routed invocation produces a status - trace coverage is structural, not best-effort.

A nil exporter returns a pass-through middleware: the trace feed is simply off, and the service behaves identically to an unmeshed one. This is the "reduced mesh" posture - each feed degrades independently, never the service.

An incoming W3C traceparent header joins the existing trace (its trace-id is adopted and its parent-id recorded); a missing or malformed one starts a fresh trace. The x-correlation-id header, when present, is carried verbatim.

func WithTraceContext

func WithTraceContext(next client.Sender) client.Sender

WithTraceContext wraps next so an outbound Send propagates the current invocation's mesh trace as a W3C `traceparent` header. It is the outbound half of trace-bindings.md §2's third cross-cutting client behavior ("correlation ID injection, trace context, retry") - the direct counterpart to this package's inbound TraceMiddleware, and it lives here rather than in `client` because the trace context it propagates is this package's Span (keeping `client` free of any mesh dependency).

It reads the span TraceMiddleware recorded for this invocation (SpanFromContext) and, when present, sets `traceparent` to that span's value - this invocation as the downstream call's parent - which is exactly what lets a collector derive who-calls-whom across services without anyone declaring an edge (see Span.Traceparent). Compose it over any transport's outbound client alongside client.WithCorrelationID / client.WithRetry; httpclient.Client and every binding's Client satisfy client.Sender.

Degradation is the package rule: with no trace middleware installed (no span on the context) the decorator sends no traceparent and the call is otherwise untouched - an unmeshed hop loses trace continuity, never the request.

When a span IS present it always wins: any `traceparent` already in the outbound headers is replaced (matched case-insensitively). This is deliberately UNLIKE WithCorrelationID - a correlation id propagates unchanged down a chain, but a traceparent must re-parent at every hop. The case that makes this load-bearing: a handler that forwards its own inbound headers onto the outbound call carries the *inbound* traceparent; leaving it would parent the downstream call to this service's caller and hide this service from the derived who-calls-whom graph - the exact thing the decorator exists to get right.

Types

type Descriptor

type Descriptor struct {
	Service        string            `json:"service"`
	ServiceVersion string            `json:"serviceVersion,omitempty"`
	InstanceID     string            `json:"instanceId,omitempty"`
	Runtime        string            `json:"runtime"`
	Binding        string            `json:"binding,omitempty"`
	Placement      Placement         `json:"placement"`
	Topics         []TopicDescriptor `json:"topics"`
	// Consumes is every registered outbound topic (mesh.md §2.3): what this service consumes.
	// This is the field the collector's consumer-edge derivation reads (mesh.md §4) - a topic
	// absent here is not consumed by this service, regardless of what traffic has or hasn't
	// flowed. Populated the same way Topics is: always present, empty when the service consumes
	// nothing (as opposed to a nil OutboundRegistry, which instead degrades the feed).
	Consumes []TopicDescriptor `json:"consumes"`
	// DescriptorHash is the contract hash (mesh.md §2.2): stable across instances and
	// heartbeats of the same build, changed exactly when the contract changes - which is
	// what lets a collector detect a redeploy (or a schema change without a version bump)
	// from the hash alone. See descriptorHash for what it covers and excludes.
	DescriptorHash string `json:"descriptorHash,omitempty"`
	// Degraded lists the feeds that were unavailable when the descriptor was built (e.g.
	// FeedRegistry when Describe was given a nil Registry, FeedOutboundRegistry when given a
	// nil OutboundRegistry), so a reduced mesh is visible as reduced rather than mistaken for a
	// service that provides or consumes nothing.
	Degraded []string `json:"degraded,omitempty"`
}

Descriptor is the service self-description of mesh.md §2: identity, placement, the topic catalog derived from the Registry (what this service provides), and the consumed-topic catalog derived from the OutboundRegistry (what this service consumes, §2.3). It is what makes the mesh's catalog "derived, not declared" - there is no hand-maintained counterpart to go stale, on either side of the graph.

func Describe

func Describe(registry *benzene.Registry, outbound *OutboundRegistry, info ServiceInfo) Descriptor

Describe builds the service Descriptor from the live registry, the live outbound registry, and info. Call it after all Register/RegisterOutbound calls (registration is a startup activity, so both lists are complete and static from then on). A nil registry or a nil outbound registry is not an error: the descriptor is built without that half of the catalog and the missing feed is recorded in Degraded, so a service whose registry or outbound-registry feed is deliberately not wired up still participates in the mesh reduced, rather than not at all - the same degradation rule applied symmetrically to both halves of the contract.

type Exporter

type Exporter interface {
	Export(ctx context.Context, event TraceEvent)
}

Exporter receives every TraceEvent the trace middleware produces. Implementations must be safe for concurrent use (one pipeline serves concurrent invocations) and should never block for long - and regardless of what they do, the middleware guarantees an exporter cannot affect the invocation it observed (a panic is swallowed, and the middleware has no error path to poison).

type Heartbeat

type Heartbeat struct {
	Service        string               `json:"service"`
	InstanceID     string               `json:"instanceId,omitempty"`
	DescriptorHash string               `json:"descriptorHash,omitempty"`
	SentAt         time.Time            `json:"sentAt"`
	Health         healthcheck.Response `json:"health"`
}

Heartbeat is the body of a mesh:heartbeat message (mesh.md §5.3): the standard aggregate health response reused byte-for-byte (no new health vocabulary), wrapped with identity and the contract hash - a changed hash is how a collector notices a redeploy and knows to re-fetch the descriptor.

type Issue

type Issue struct {
	// Fingerprint is the lowercase hex of the first 16 bytes of SHA-256 over
	// service|topic|version|classification|discriminator (mesh.md §4.1); the collector's merge
	// key. An empty fingerprint is an invalid entry and is skipped, never rejecting the batch.
	Fingerprint    string    `json:"fingerprint"`
	Classification string    `json:"classification"`
	Service        string    `json:"service"`
	Topic          string    `json:"topic"`
	Version        string    `json:"version,omitempty"`
	Transport      string    `json:"transport,omitempty"`
	Status         string    `json:"status"`
	ExceptionType  string    `json:"exceptionType,omitempty"`
	Count          int64     `json:"count"`
	FirstSeen      time.Time `json:"firstSeen"`
	LastSeen       time.Time `json:"lastSeen"`
	// ExemplarTraceIds links the signature to concrete flows; the collector keeps the newest ≤3.
	ExemplarTraceIds []string `json:"exemplarTraceIds,omitempty"`
	ResolutionHint   string   `json:"resolutionHint,omitempty"`
}

Issue is one deduplicated failure signature (mesh.md §4.1). Count is a DELTA - occurrences since the emitter's previous successful flush, never a cumulative total - so a collector's merge (count += delta, firstSeen = min, lastSeen = max, newest exemplars kept) is restart-proof and needs no instance identity on the wire.

type IssueBatch

type IssueBatch struct {
	Service string  `json:"service"`
	Issues  []Issue `json:"issues"`
}

IssueBatch is the body of a benzene:mesh:issues message (mesh.md §4.1): a service's deduplicated, classified failure signatures since its last flush. Batch-level Service is REQUIRED (an aggregating relay may batch for several services); an empty Issues list is the feed's liveness assertion ("alive, nothing failing") and must be attributable.

type IssueOccurrence

type IssueOccurrence struct {
	Service       string
	Topic         string
	Version       string
	Status        string
	ExceptionType string
	TraceID       string
	At            time.Time
}

IssueOccurrence is one failure the middleware observed, before classification/fingerprinting. ExceptionType is left empty by the standard middleware: Go's router converts a handler panic to a service-unavailable Result before this middleware sees it, so there is no language-native thrown type to capture (mesh.md §4.1 makes exceptionType optional for exactly this reason).

type IssueRecorder

type IssueRecorder interface {
	Record(occurrence IssueOccurrence)
}

IssueRecorder receives the failure occurrences IssueMiddleware observes. Implementations must be safe for concurrent use and must not block the invocation.

type LogExporter

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

LogExporter writes one JSON line per TraceEvent to a writer - the zero-setup exporter: structured, greppable invocation logs on stdout, which every platform this module targets (Lambda, Functions, Cloud Run, plain processes) already collects. The push exporter that feeds a meshd collector is a later phase (mesh.md §8).

func NewLogExporter

func NewLogExporter(w io.Writer) *LogExporter

NewLogExporter returns a LogExporter writing to w; a nil w means os.Stdout.

func (*LogExporter) Export

func (e *LogExporter) Export(_ context.Context, event TraceEvent)

Export writes event as a single JSON line. Writes are serialized so concurrent invocations never interleave bytes within a line. A write failure is deliberately dropped: the feed is lossy by design, because an ailing log sink must never make the service slower or less reliable than an unmeshed one.

type OutboundRegistry

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

OutboundRegistry holds outbound registration records (mesh.md §2.3): topics this service *may send*, with the request type it sends and the response type it expects back - no handler, since nothing here receives. It mirrors Registry's handler discovery exactly, minus the handler, for the identical reason core-concepts.md §9 requires explicit registration for inbound: the list this type holds is what makes ServiceDescriptor.Consumes a hard-coded contract rather than an inference. A port MUST NOT populate it by scanning call sites, string literals, or any other static analysis over handler bodies - RegisterOutbound is the only path.

A registered entry needs no destination address, queue name, or topic ARN: those are transport/deployment configuration (transport-bindings.md), orthogonal to the contract this registers. A service can declare it consumes payments:capture while its actual queue URL is injected at deploy time - the descriptor doesn't change between environments, only the wiring does.

func NewOutboundRegistry

func NewOutboundRegistry() *OutboundRegistry

NewOutboundRegistry returns an empty OutboundRegistry.

func (*OutboundRegistry) TopicTypes

func (r *OutboundRegistry) TopicTypes(topic benzene.Topic) (request, response reflect.Type, ok bool)

TopicTypes returns the request and response types captured when topic was registered, or ok = false when topic isn't registered. Mirrors Registry.TopicTypes.

func (*OutboundRegistry) Topics

func (r *OutboundRegistry) Topics() []benzene.Topic

Topics returns every registered outbound topic, sorted by ID then Version - mirrors Registry.Topics, the enumeration behind Descriptor.Consumes.

type Placement

type Placement struct {
	Cloud  string `json:"cloud"`
	Region string `json:"region,omitempty"`
}

Placement locates a service instance (mesh.md §4.3). Cloud is one of "aws", "azure", "gcp", or "self-hosted" when detected; an explicit ServiceInfo.Placement override may carry any value.

func DetectPlacement

func DetectPlacement() Placement

DetectPlacement identifies where this process runs from each platform's documented environment variables (mesh.md §4.3), so placement needs no configuration on the three clouds this module ships bindings for:

  • AWS Lambda: AWS_LAMBDA_FUNCTION_NAME (a defined Lambda runtime variable); region from AWS_REGION.
  • Azure Functions: FUNCTIONS_CUSTOMHANDLER_PORT, the same documented custom-handler contract variable the azurefunctions package is driven by. The Functions host does not expose its region through a documented environment variable, so Region is left empty rather than guessed.
  • Google Cloud Run: K_SERVICE (a documented Cloud Run/Knative variable). Cloud Run exposes region only via the metadata server, not the environment, so Region is left empty rather than guessed.

When none match, the placement is "self-hosted". A service that knows better (or runs on a platform not listed) sets ServiceInfo.Placement explicitly, which bypasses detection entirely.

type PushExporter

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

PushExporter batches TraceEvents and sends them to a collector as mesh:traces envelopes (mesh.md §8 Phase 3) from a single background goroutine, so exporting never runs on an invocation's goroutine beyond a non-blocking channel send.

The trace feed is lossy by design, in every failure mode: a full buffer drops the new event, a failed send drops the batch (the Sender contract already converts transport failures to a Result, never an error), and a nil Sender yields a nil exporter whose methods are all nil-safe no-ops. The mesh must never make a service slower or less reliable than an unmeshed one.

func NewPushExporter

func NewPushExporter(sender Sender, options PushExporterOptions) *PushExporter

NewPushExporter starts the background sender and returns the exporter. A nil sender returns a nil exporter - usable directly as TraceMiddleware's exporter thanks to nil-safe methods, degrading to a disabled trace feed.

func (*PushExporter) Close

func (e *PushExporter) Close()

Close flushes everything already queued and stops the background sender. Safe to call more than once and on a nil exporter. Call it on shutdown so the tail of the trace feed isn't lost with the process.

func (*PushExporter) Export

func (e *PushExporter) Export(_ context.Context, event TraceEvent)

Export enqueues event for the background sender. Never blocks: a full buffer drops the event. Nil-safe, so a (*PushExporter)(nil) wired in as an Exporter behaves as the disabled trace feed rather than panicking.

type PushExporterOptions

type PushExporterOptions struct {
	// BatchSize is the number of events that triggers an immediate flush. Default 64.
	BatchSize int
	// FlushInterval is how often a partial batch is flushed anyway, so a quiet service's
	// traces still arrive promptly. Default 5s.
	FlushInterval time.Duration
	// BufferSize is the queue between invocations and the background sender. When it is
	// full, Export drops new events rather than blocking. Default 1024.
	BufferSize int
}

PushExporterOptions tunes a PushExporter. Zero values mean the defaults.

type PushIssueExporter

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

PushIssueExporter is the emitter half of the issue feed (mesh.md §4.1): it deduplicates failure occurrences by fingerprint at the source (per-occurrence events are never sent), accumulating delta counts, and flushes them as one benzene:mesh:issues envelope from a single background goroutine. Like the trace feed it is lossy by design - a failed send drops the window's deltas (the collector's delta-merge tolerates the loss) - and a nil Sender yields a nil exporter whose methods are nil-safe no-ops, degrading to a disabled feed.

func NewPushIssueExporter

func NewPushIssueExporter(sender Sender, service string, options PushIssueExporterOptions) *PushIssueExporter

NewPushIssueExporter starts the background flusher and returns the exporter. A nil sender or an empty service returns a nil exporter - usable directly as IssueMiddleware's recorder thanks to nil-safe methods, degrading to a disabled issue feed.

func (*PushIssueExporter) Close

func (e *PushIssueExporter) Close()

Close flushes the current window and stops the background flusher. Safe to call more than once and on a nil exporter; call it on shutdown so the tail of the feed isn't lost with the process.

func (*PushIssueExporter) Flush

func (e *PushIssueExporter) Flush()

Flush sends the accumulated deltas as one benzene:mesh:issues batch and resets the window. It always sends - an empty window flushes an empty batch, the feed's liveness assertion (mesh.md §4.1) - so a collector can distinguish a quiet wired service from an unwired one. The send result is deliberately ignored: a dropped batch loses its deltas, lossy by design. Exposed for tests and for a caller that wants to flush on its own schedule; nil-safe.

func (*PushIssueExporter) Record

func (e *PushIssueExporter) Record(occurrence IssueOccurrence)

Record classifies, fingerprints, and merges one occurrence into the current flush window. Never blocks for long (a brief map lock) and is nil-safe, so a (*PushIssueExporter)(nil) wired in behaves as the disabled feed.

type PushIssueExporterOptions

type PushIssueExporterOptions struct {
	// FlushInterval is how often the accumulated deltas are flushed as one benzene:mesh:issues
	// batch. An empty interval flush still sends an empty batch - the feed's liveness assertion,
	// so a collector can tell a quiet wired service from an unwired one. Default 5s.
	FlushInterval time.Duration
	// Now supplies the clock, for tests. Defaults to time.Now.
	Now func() time.Time
}

PushIssueExporterOptions tunes a PushIssueExporter. Zero values mean the defaults.

type Sender

type Sender interface {
	Send(ctx context.Context, topic benzene.Topic, headers map[string]string, message []byte) benzene.Result[json.RawMessage]
}

Sender is the outbound-client subset PushExporter needs - transport-bindings.md §2's one send interface, satisfied by *httpclient.Client. An interface rather than the concrete client so the trace feed isn't married to HTTP: any transport that can carry a wire envelope can carry mesh traffic.

type ServiceInfo

type ServiceInfo struct {
	Service        string
	ServiceVersion string
	InstanceID     string
	Binding        string
	Placement      Placement
}

ServiceInfo is the static identity a service supplies to Describe and TraceMiddleware. Every field is optional; zero values simply leave the corresponding descriptor/trace fields empty. Placement, when its Cloud is non-empty, overrides detection wholesale - otherwise DetectPlacement runs.

type Span

type Span struct {
	TraceID string
	SpanID  string
}

Span is the current invocation's position in a trace: the ids TraceMiddleware assigned (or adopted from the caller's traceparent) for the event it will export. Handlers use it to propagate the trace onto outbound calls, which is what lets a collector join spans across services - an observed signal for liveness and undeclared-edge drift (mesh.md §4.2), layered on top of the producer/consumer graph the declared ServiceDescriptor alone provides (mesh.md §4), never a way to derive who-calls-whom in place of declaring it.

func SpanFromContext

func SpanFromContext(ctx context.Context) (Span, bool)

SpanFromContext returns the span TraceMiddleware recorded for the current invocation. ok is false when no trace middleware is installed - per this package's degradation rule, a caller must then simply send no traceparent header (an unmeshed hop degrades trace continuity, never the call).

func (Span) Traceparent

func (s Span) Traceparent() string

Traceparent renders the span as a W3C traceparent header value for outbound propagation: version 00, this span as the parent-id, sampled flag set.

type TopicDescriptor

type TopicDescriptor struct {
	ID             string         `json:"id"`
	Version        string         `json:"version,omitempty"`
	RequestSchema  map[string]any `json:"requestSchema"`
	ResponseSchema map[string]any `json:"responseSchema"`
}

TopicDescriptor is one registered topic in a Descriptor (mesh.md §5.1). The schemas describe the marshaled request/response forms, derived at startup from the TReq/TRes types captured at the Register call site (see deriveSchema for the exact mapping); they are what lets the mesh flag schema drift from live data instead of hand-written specs. The schemas are never omitted from the wire even when unconstrained ({}): a consumed topic with no declared response type (mesh.md §2.3) marshals responseSchema as the empty object, not as an absent key - a reader must be able to tell "unconstrained" from "not derived".

type TraceBatch

type TraceBatch struct {
	Events []TraceEvent `json:"events"`
}

TraceBatch is the body of a mesh:traces message: the events a PushExporter accumulated since its last flush.

type TraceEvent

type TraceEvent struct {
	TraceID      string `json:"traceId"`
	SpanID       string `json:"spanId"`
	ParentSpanID string `json:"parentSpanId,omitempty"`
	Service      string `json:"service,omitempty"`
	InstanceID   string `json:"instanceId,omitempty"`
	Topic        string `json:"topic"`
	TopicVersion string `json:"topicVersion,omitempty"`
	// Status is the Benzene status verbatim (empty when no downstream middleware produced
	// a Result - a pipeline without a router, which is a wiring gap the mesh reports
	// as-is rather than papering over).
	Status        string    `json:"status"`
	DurationMs    float64   `json:"durationMs"`
	StartedAt     time.Time `json:"startedAt"`
	CorrelationID string    `json:"correlationId,omitempty"`
}

TraceEvent is one pipeline invocation as seen by the mesh (mesh.md §5.2). It is semantic where a transport-level span is not: it carries the topic (+ version) and the Benzene status, not a URL and an HTTP code. TraceID/SpanID/ParentSpanID are the W3C traceparent fields (hex, 16/8/8 bytes), so mesh traces interleave with any existing OpenTelemetry pipeline instead of competing with it.

Jump to

Keyboard shortcuts

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