Documentation
¶
Overview ¶
Package telemetry is the only place in this server that knows about OpenTelemetry.
Everything else reaches observability through the small API here, or through the OTel global providers this package installs. That is deliberate: an instrumentation library spread across 176 packages is one nobody can remove, reconfigure or reason about, and the seams that matter here are few enough to be listed. Nothing outside this package imports go.opentelemetry.io.
Off by default, and why ¶
Telemetry stays off unless an operator turns it on, for privacy rather than for cost. Instrumenting a deployment is the operator's decision to make about their own users, and a server that traced by default would be making it for them. PRIVACY.md's promise is unaffected either way: it is scoped to what the maintainer receives, and an exporter an operator points at their own collector sends nothing to anyone else.
When it is on, the server says so. Snapshot feeds the `observability` block of the server card, so somebody connecting to a published endpoint can see that their calls are instrumented rather than having to ask.
Configuration ¶
One switch is ours and the rest is the specification's. `--telemetry` (or GITLAB_MCP_TELEMETRY) turns it on; endpoint, headers, timeouts, sampling and resource attributes come from the standard OTEL_* environment variables the exporters read themselves. Reinventing that surface would mean maintaining a second, worse copy of a configuration an operator already knows.
The name is deliberate in both halves. It is not in the OTEL_ namespace: nothing forbids that (the OTEL_{LANGUAGE}_{FEATURE} convention carries no RFC 2119 keyword and addresses SDKs), but the namespace belongs to the specification and to the language SDKs, it is actively occupied (OTEL_GO_X_RESOURCE, OTEL_GO_X_OBSERVABILITY, OTEL_GO_X_CARDINALITY_LIMIT), a future release could claim a plain name like OTEL_ENABLED and change its meaning underneath us, and an operator seeing an OTEL_ prefix will reasonably assume the SDK is what reads it. And it carries the GITLAB_MCP_ prefix so it cannot collide with whatever else a host has exported; a bare TELEMETRY or OBSERVABILITY is the kind of name two programs on one machine will both want. Once shipped, the name and its false default cannot move without a major version, so it is decided here or not at all.
The standard OTEL_* variables keep their names and must never be given a prefix of ours. They are not read by this code at all: the exporters read them. Shadowing them under GITLAB_MCP_ would mean passing the value as a programmatic option, which in Go is applied after the environment and so silently kills the variable it was meant to mirror, and it would break the ordinary case of a host that exports OTEL_EXPORTER_OTLP_ENDPOINT once for every service running on it.
OTEL_SDK_DISABLED is honored as a veto on top, which is a different thing from an off switch; SDKDisabledByEnv says why the two cannot be collapsed.
What an attribute may carry ¶
The same discipline the logs already keep, stated here because a span makes it easy to break: record what was called and how it ended, never what was passed. Tool names, action ids, outcome, duration and the identity already in the log line are in; parameters, queries, tokens and response bodies are out. The existing code is the precedent: the dynamic surface logs `query_len` and not the query, and the pool logs a token suffix and not the token.
Index ¶
- Constants
- func DropToolName(policy ToolNamePolicy, toolSurface string) bool
- func EnvSwitch() bool
- func InsecureCredentialSignals(signals Signals) []string
- func NewSlogHandler(base slog.Handler, floor slog.Level, identity *Redactor) slog.Handler
- func OutboundContext(ctx context.Context) context.Context
- func PolicyDescription(policy IdentityPolicy) string
- func RedactResourceURIs(text string) string
- func SDKDisabledByEnv() bool
- type Config
- type IdentityPolicy
- type Keyring
- type Provider
- type Redactor
- type ResourceRedactor
- type Signals
- type Snapshot
- type ToolNamePolicy
Constants ¶
const ( LogFieldUser = "user" LogFieldUserID = "user_id" // LogFieldTokenSuffix is the masked tail of a client credential the pool // and the refusal paths log. It is declared here for the same reason as the // two above: the export-side strip list has to find it by name, and it is // stripped rather than policy-governed because four characters of a token // are a correlation handle and not an identity anyone chose to publish. // The name of a field, never a credential: the value it names is four // masked characters, and this constant is what removes them. LogFieldTokenSuffix = "token_suffix" //nolint:gosec // a log field name, not a secret )
The slog field names a tool-call record carries on the stderr leg.
Declared here rather than where they are written, because the redactor has to find them to apply the policy to the exported copy, and two spellings of the same field would mean the policy silently applies to neither.
They are not the user.* names below on purpose: those are the OpenTelemetry registry's, for what leaves the process, while these are what an operator reads in their own terminal. Reconciling the two is a separate change with a dual-emit window, since it breaks whatever parses stderr today.
const ( // AttrUserID is "Unique identifier of the user". The GitLab numeric id. AttrUserID = "user.id" // AttrUserName is "Short name or login/username of the user". AttrUserName = "user.name" // AttrUserHash is "Unique user hash to correlate information for a user in // anonymized form", to be used "when user.id or user.name contain // confidential data". That is this policy's pseudonymous mode exactly. // // One objection to this key deserves an answer rather than a dismissal: a // hash over GitLab's numeric ids is reversible by enumeration, because the // input space is small and predictable. That is true of a plain digest and // false of what [Redactor.pseudonym] computes, which is an HMAC under a // 32-byte key generated per process and never written down. Without the key // there is nothing to enumerate against. What remains is inherent to // pseudonymity rather than to the construction: somebody who can correlate a // known user's activity with a digest can link the two, which is why the // mode is called pseudonymous and not anonymous. AttrUserHash = "user.hash" )
The registry-defined keys this policy emits.
All three are in the user.* namespace, and that uniformity is the point: the same three names appear on a span, on a log record and anywhere else identity is recorded, so an operator writes one query rather than three.
The enduser.* namespace was the first choice and is the wrong one. Only two attributes live there, enduser.id and enduser.pseudo.id, and the registry tags them "contains sensitive (PII) information" and "contains sensitive (linkable PII) information" respectively, which is a heavier warning than the user.* pair carries for the same values. There is no enduser.name at all: an earlier version of this file invented one, which is precisely what the naming guidance rules out, since a key in a namespace OpenTelemetry owns can be given a different meaning by a future release.
const ( // AttrResourceURI is the MCP convention's own key, Conditionally Required // on a span "when the client executes a request type that includes a // resource URI parameter". It carries the URI exactly, so it is only ever // set under [IdentityFull]. AttrResourceURI = "mcp.resource.uri" // AttrResourceRef is this server's key for the same resource named // indirectly. It is deliberately not the convention's key: a consumer // reading mcp.resource.uri is entitled to find a URI there, and putting a // digest under that name would be a lie told in the schema. AttrResourceRef = "gitlab_mcp.resource.ref" )
const ( ProtocolHTTP = "http/protobuf" ProtocolGRPC = "grpc" )
Protocol selects the OTLP transport.
The two values are the ones the specification defines for OTEL_EXPORTER_OTLP_PROTOCOL. `http/protobuf` is the default because it crosses proxies and ingress that gRPC does not, which is the common shape for a collector that is not on the same host.
const DefaultIdentityPolicy = IdentityNone
DefaultIdentityPolicy is what an operator gets without deciding.
const DefaultKeyRotation = time.Duration(0)
DefaultKeyRotation is how long a generated key lives without a setting.
Zero, meaning the life of the process, which is what this server did before the interval existed. Rotating by default would silently make the multi replica case worse than it is: replicas start at different moments, so they would rotate out of phase, and a count of distinct users would churn without anybody asking for it. Rotation is coherent on one instance and is opt-in for that reason.
const DefaultLogSeverity = slog.LevelInfo
DefaultLogSeverity is the floor for records that reach a collector.
Info rather than debug, and the reason is bounded resource use rather than taste: "Logging could consume much memory by default if the end user application emits too many logs... the end user should consider reducing logs that are passed to the exporters." A debug run of this server emits a record per GitLab round trip, and exporting all of them would duplicate on the wire what the spans already describe, on top of the spans.
const DefaultServiceName = "gitlab-mcp-server"
DefaultServiceName is what this server calls itself to a collector when OTEL_SERVICE_NAME says nothing.
const EnvIdentityKeyName = "GITLAB_MCP_TELEMETRY_IDENTITY_KEY"
EnvIdentityKeyName is the environment variable holding the operator's pseudonymisation secret.
It is a secret in the GDPR sense: Article 4(5) calls it the "additional information" that allows attribution, and the EDPB says controllers "need to keep them separately and subject them to technical and organisational measures that ensure their confidentiality". Read from the environment because that is where this project's other secrets come from, with the same caveat every environment secret carries: it is visible to anything that can read the process environment.
const EnvIdentityName = "GITLAB_MCP_TELEMETRY_IDENTITY"
EnvIdentityName is the environment variable that selects the identity policy.
Prefixed, like every variable this server owns, so it cannot collide with whatever else a host has exported. The standard OTEL_* names are never prefixed, because the SDK reads those and we do not.
const EnvIdentityRotationName = "GITLAB_MCP_TELEMETRY_IDENTITY_ROTATION"
EnvIdentityRotationName is the environment variable holding the lifetime of a generated key.
const EnvSwitchName = "GITLAB_MCP_TELEMETRY"
EnvSwitchName is the environment variable that turns telemetry on.
It carries this project's prefix rather than living in the OTEL_ namespace, for the reasons in the package doc: that namespace belongs to the specification and the language SDKs, and a bare name would collide with whatever else a host has exported.
const EnvToolNameName = "GITLAB_MCP_TELEMETRY_TOOL_NAME"
EnvToolNameName is the environment variable that selects the policy.
const MaxKeyRotation = 30 * 24 * time.Hour
MaxKeyRotation bounds the interval, for the same reason every other duration this server accepts is bounded: a value with a typo in it should be refused at startup rather than discovered a month later.
Variables ¶
This section is empty.
Functions ¶
func DropToolName ¶
func DropToolName(policy ToolNamePolicy, toolSurface string) bool
DropToolName reports whether the tool name should be filtered out of metric attributes for a given tool surface.
The number that decides this ¶
The individual surface registers between about 850 and 1071 distinct tools, one per catalog action. As a metric dimension that is up to 1071 time series per method, multiplied by every other dimension, against a Go SDK default cardinality limit of 2000 per instrument per collection cycle.
What happens at the limit is worse than an error. No measurement is lost, but everything past the limit collapses into one synthetic series marked otel.metric.overflow, and cumulative temporality makes it first-come-wins: the first combinations seen after startup are kept forever and everything later collapses. So the sample is biased by call order rather than by importance, and the long tail of rarely-used GitLab actions becomes unattributable precisely when somebody is trying to debug one. The only visible signal is the synthetic series itself.
The dynamic surface has two tool names and the meta surface about fifty. Neither is a problem, and on the dynamic surface the attribute is nearly all a metric has to go on, so dropping it there would cost real information for no benefit.
Two attributes, one decision ¶
The drop takes gitlab_mcp.action with it, and it has to. On the individual surface the two are one to one, because that surface projects one visible tool per catalog action, so a filter that removed the tool name and left the action behind shed no series at all: the same eleven hundred values simply arrived under a different key. The budget is a property of the pair, so the decision is one decision.
This is a documented deviation, not a permitted variation ¶
gen_ai.tool.name is Conditionally Required in the MCP convention, and the requirement-levels table allows exclusion via configuration for Recommended and Opt-In only. Placing it at Conditionally Required is an assertion by the convention's authors that a tool name is not a high-cardinality metric attribute, which is true for a server with a dozen tools and false for one with a thousand. The attribute still passes the convention's own aggregation test, so this is a budget problem rather than a modeling error, and the deviation is recorded rather than hidden.
func EnvSwitch ¶
func EnvSwitch() bool
EnvSwitch reports whether the environment asks for telemetry.
Parsed with the same grammar as every other boolean here, which is stricter than Go's: only the case-insensitive string "true" enables, an empty value counts as unset, and anything unrecognized is a warning and a false rather than an error. Using one parser for our switch and for OTEL_SDK_DISABLED is deliberate: two booleans in one configuration surface that disagree about whether "1" means true would be worse than either rule alone.
func InsecureCredentialSignals ¶
InsecureCredentialSignals names the signals that would send a collector credential over plaintext to another host.
What this is not ¶
It is not a refusal, and it must not become one. A collector on a trusted private network reached over plaintext is a legitimate deployment, and it is the one this project's telemetry work was validated against. The endpoint and the headers are both the operator's own configuration, and the specification is deliberate that OTEL_EXPORTER_OTLP_* belongs to the exporters. Overriding an explicit choice about somebody's own network is not this server's call.
What is this server's call is not letting the mistake be silent. The guide carries the warning in prose; this is the same sentence at the moment it applies, which is the one that reaches an operator who did not read that section.
Why loopback is exempt ¶
A credential that never leaves the machine cannot be observed on a network, so a sidecar collector on 127.0.0.1 is not a disclosure. Anything else is, including a private address: "the LAN is trusted" is a judgement the operator is entitled to make and this function is not, so it says what is happening rather than what to do about it.
func NewSlogHandler ¶
NewSlogHandler wraps an existing handler so records go to both stderr and the collector.
Why both, rather than one or the other ¶
The stderr JSON is what an operator reads over somebody's shoulder, what a container platform captures, and what works when telemetry is off, which is the default. It cannot be replaced. The OTLP leg is what correlates a log record with the span it happened inside, which stderr cannot do at all.
So this is a fan-out rather than a redirect, and the stderr leg is deliberately first: if the bridge ever blocks or panics, the record has already been written where somebody can see it.
The severity floor ¶
The collector leg is filtered and the stderr leg is not. An operator running at debug wants everything on their terminal and almost certainly does not want a record per GitLab round trip on their collector, on top of a span describing the same call. LOG_LEVEL still governs stderr; this floor governs only what is exported.
What this does not do ¶
It adds no attributes of its own. The identity policy, the redaction rules and the decision about what a record may carry all live where the record is written, so a field that must not be exported must not be logged either. A bridge that filtered fields would be a second place to get that wrong, and the two would disagree the first time somebody added a log line.
func OutboundContext ¶
OutboundContext strips a caller's baggage before this server makes a request of its own.
Why this is an action rather than a default ¶
It is tempting to assume nothing forwards baggage unless asked. That is false. propagation.Baggage.Inject writes baggage.FromContext(ctx).String() unconditionally whenever it is non-empty, and the global propagator this package installs contains propagation.Baggage. So the moment the GitLab client's transport is instrumented, which adopting traces implies, and the inbound request's context reaches client-go, which is the ordinary Go idiom, a client's baggage rides outbound. Nobody has to opt in for the leak; someone has to opt out for it not to happen. This function is that opt-out, and it is meant to be called at every point where a context that came from a caller becomes a request this server makes.
The shape of the exposure ¶
This server sits between a client it does not control and a GitLab instance it calls with a privileged credential. Baggage is a header a client fills in freely: "Baggage values are any valid UTF-8 strings. Language API MUST accept any valid UTF-8 string as baggage value in Set and return the same value from Get." A well-formed header of 64 keys and 8192 bytes, all attacker-chosen, parses cleanly and would arrive at the customer's GitLab instance carrying this server's name.
This declines a documented SHOULD, on purpose ¶
W3C says "A system receiving a baggage request header SHOULD send it to outgoing requests." We do not. The same section provides the escape hatch in the same breath ("Any key/value pair MAY be deleted"), and the OTel Baggage API requires the facility used here to exist for exactly this reason: "To avoid sending any name/value pairs to an untrusted process, the Baggage API MUST provide a way to remove all baggage entries from a context." Declining a SHOULD is legitimate when the reason is recorded, and the reason is that the process downstream of us belongs to someone else.
The trace context itself is untouched. Only baggage is cleared, so a distributed trace still joins up across the call.
func PolicyDescription ¶
func PolicyDescription(policy IdentityPolicy) string
PolicyDescription says in plain words what a policy exports, for the startup log and the server card.
It exists so that the one line an operator reads at startup says what will actually leave the process, rather than a mode name they would have to look up. "pseudonymous" means nothing on its own; "a per-process digest, no readable identity" is the sentence that lets somebody notice a mistake.
func RedactResourceURIs ¶
RedactResourceURIs replaces resource URIs with a marker.
Exported because two places need it and neither can import the other: the span status description in internal/mcpotel, which imports the OpenTelemetry API and never the SDK, and the log handler here, which is built on the SDK. A test in internal/mcpotel asserts the two agree, since a rule stated twice is a rule that drifts.
func SDKDisabledByEnv ¶
func SDKDisabledByEnv() bool
SDKDisabledByEnv reports whether the specification's own kill switch is set.
OTEL_SDK_DISABLED cannot be this server's on switch, and reading it as one would be worse than not reading it at all. Its specified default is false, meaning "the SDK is enabled", while telemetry here is off until an operator asks for it. Treating the variable as the single switch would therefore invert its meaning for exactly the operators who already know what it means. It composes instead: our own switch turns telemetry on, and this vetoes.
Nothing beneath us implements it. The string does not appear anywhere in the OpenTelemetry Go modules, and the specification's compliance matrix records no Go support, so a deployment that sets it and expects to be obeyed is relying on this function existing.
The carve-out in the specification is honored by where this is called rather than by anything here: "This setting has no effect on propagators configured through the OTEL_PROPAGATORS variable." Returning early from Start leaves the global propagator untouched, which is the no-op composite the SDK installs by default.
Types ¶
type Config ¶
type Config struct {
// Enabled turns telemetry on. False leaves the OTel globals as the noop
// implementations the API ships with, so every instrumentation call in the
// rest of the codebase costs a nil check.
Enabled bool
// Protocol is [ProtocolHTTP] or [ProtocolGRPC]. Empty means HTTP.
Protocol string
// ServiceName overrides OTEL_SERVICE_NAME. Empty means
// [DefaultServiceName], or whatever the environment already says.
ServiceName string
// ServiceVersion is reported as service.version, so a collector can tell
// which build produced a span.
ServiceVersion string
// DropToolNameFromMetrics removes gen_ai.tool.name from metric attributes.
//
// Set by the caller, which knows the registered tool surface; see
// [DropToolName] for the decision and for why the individual surface needs
// it. It affects metrics only: the span keeps the attribute on every
// surface, because one span carrying a tool name costs one span while one
// metric dimension carrying it costs a time series per tool, forever.
DropToolNameFromMetrics bool
// Signals selects what is exported. A zero value means all three.
Signals Signals
}
Config is what this server decides. Everything else is read from the standard OTEL_* environment by the exporters.
type IdentityPolicy ¶
type IdentityPolicy string
IdentityPolicy decides what a signal leaving this process may say about who made a call.
It is deliberately not a log level. Verbosity is how much is recorded; identity is what each record contains, and fusing them fails both ways: at WARN the identity would disappear from exactly the records where it matters most, a refusal or a throttle or a failure, and at DEBUG it would arrive alongside a flood nobody asked for.
It is also not scoped to logs. A span attribute and a metric label carry a user id just as easily, so a policy that governed only the log signal would leave identity flowing through traces while claiming to be off.
The rule ¶
What leaves the process is redacted unless the operator says otherwise; what stays in the operator's own stderr is not. That boundary is what keeps this from quietly undoing ADR-0008, which put identity into the logs for audit: stderr is unchanged for every existing deployment, and identity does not cross to a collector until somebody asks for it.
const ( // IdentityNone exports nothing about who made a call. The default, // because it is the only value that is safe for an operator who has not // thought about the question. IdentityNone IdentityPolicy = "none" // IdentityPseudonymous exports a stable per-user digest and no readable // identity. It keeps the one thing a shared endpoint genuinely needs, // telling one caller's traffic from another's so a burst can be attributed // and a session followed, without naming anyone. It is also the nearest // thing this server has to the per-call correlation ADR-0008 records as // missing. IdentityPseudonymous IdentityPolicy = "pseudonymous" // IdentityFull exports the user id and username already present in the // stderr log line. What an enterprise auditing its own users wants, and // what it is entitled to: those are its employees and its collector. IdentityFull IdentityPolicy = "full" )
func ParseIdentityPolicy ¶
func ParseIdentityPolicy(value string) (IdentityPolicy, error)
ParseIdentityPolicy validates an operator-supplied value.
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring owns the secrets that turn an identifier into a pseudonym, and decides how long each one lives.
Why this is one object rather than two package variables ¶
Every pseudonym this server emits has to come from the same decision. Two independently generated secrets would give the same person one user.hash on a span and a different one on a log record, which reads as two people while each signal looks correct on its own. A hosted deployment produced exactly that, from a redactor built once per pooled server rather than once per process, and no single signal could show it.
The two lifetimes, and why the operator picks ¶
Nothing prescribes an answer here. The OpenTelemetry registry defines user.hash as a value "to correlate information for a user in anonymized form" and says nothing about how it is computed or how long it should hold; neither the specification nor ENISA offers guidance on the lifetime of a pseudonymisation secret. What the field does show is two coherent designs, and Matomo ships both at once: an installation-wide salt that never rotates where a pseudonym must persist, and a seed discarded every day where it must not.
So this offers both, and the choice is the operator's:
A configured secret. Every replica derives the same keys from it, so a caller carries one pseudonym across the whole deployment and across restarts. It never rotates here, because a key the operator supplied is theirs to rotate, on their schedule, from outside.
No secret. Keys are generated at startup from crypto/rand and never written anywhere, which is what a single instance wants: nothing to store, nothing to leak, and a pseudonym that dies with the process. A rotation interval bounds it further.
The EDPB calls the persistent form a person pseudonym, notes that it "requires long-term storage of the pseudonymisation secrets", and warns that "the risk of unauthorised attribution is comparatively high". That is the cost of the correlation, stated rather than hidden: with the key in hand, recovering a GitLab user id from a digest is an enumeration of eight-digit integers, which is about two minutes on one core.
Why HKDF rather than using the secret directly ¶
Two reasons, both cheap. The operator's secret is never used as a key itself, so a value reused elsewhere does not become an HMAC key here. And one supplied secret yields two independent keys, so a digest of a user and a digest of a resource cannot be compared against each other.
func NewKeyring ¶
NewKeyring builds the keyring for a secret and a rotation interval.
An empty secret generates keys instead. A rotation interval is honored only for generated keys; with a secret it is ignored, and the caller is expected to say so where an operator will read it.
func (*Keyring) Configured ¶
Configured reports whether the keys came from the operator.
func (*Keyring) IdentityPseudonym ¶
IdentityPseudonym returns the digest naming a caller.
func (*Keyring) ResourcePseudonym ¶
ResourcePseudonym returns the digest naming a resource URI.
Why a digest rather than the URI ¶
This server's documented position is that resource URIs are never exported, because they embed project and group identifiers, and it holds that position even against the MCP semantic convention, which marks mcp.resource.uri Conditionally Required on a resources/read span. Declining it there and then writing the same value onto a subscription poll span would be the same disclosure through a different code path, and a worse one: a read happens once, while a poll repeats for the life of the watch, so one subscription would write a project id into a telemetry backend hundreds of times.
Why a digest rather than nothing ¶
Dropping the attribute entirely would leave an operator unable to tell two watchers of the same kind apart, so "one subscription is failing every poll" and "every subscription is failing" would look identical. The digest keeps that distinction and the correlation across a watcher's lifetime while naming nothing, which is exactly the trade the pseudonymous identity policy already makes for users.
What remains is inherent to pseudonymity rather than to this construction: somebody who can correlate a known subscription with a digest can link the two.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider owns the exporters and the global registrations they back.
The zero value is a working disabled provider, so a caller that never started telemetry can still call Provider.Shutdown and Provider.Snapshot without a nil check.
func Start ¶
Start installs the OTel providers and returns the handle that retires them.
A disabled configuration is not an error: it returns a provider that reports itself disabled and does nothing, which is what every caller wants at the one place this is wired.
func (*Provider) Shutdown ¶
Shutdown flushes and retires every exporter, bounded by [shutdownTimeout] or by the caller's deadline, whichever comes first. The caller's cancellation is deliberately detached (a dying request must not abort the final flush), but a deadline the caller chose is a bound to honor: before this, the internal five seconds silently overrode any tighter one, which made cmd/server's own shutdown bound dead code and held every test that pointed an exporter at an unreachable collector for the full five seconds.
Errors are joined rather than returned on the first failure: each exporter owns a connection of its own, and stopping the rest matters more than reporting the first one that could not.
type Redactor ¶
type Redactor struct {
// contains filtered or unexported fields
}
Redactor turns an identity into the attributes a signal may carry.
The zero value redacts everything, so a caller that never configured one cannot leak by forgetting.
func NewRedactor ¶
func NewRedactor(policy IdentityPolicy, keys *Keyring) *Redactor
NewRedactor builds the redactor for a policy, over the process's keyring.
A nil keyring is accepted and yields no digest, which is the same answer the zero value gives: a caller that never wired one records nothing rather than emitting something that looks like a pseudonym and is not. Nothing here can fail: the policy was validated where it was parsed, and the keyring is whatever the caller built, nil included.
func (*Redactor) Attributes ¶
Attributes returns what may be recorded about a caller.
userID and username are the fields the stderr log line already carries. An empty userID means an unauthenticated call and yields nothing under every policy: there is no identity to redact or to publish.
func (*Redactor) Policy ¶
func (r *Redactor) Policy() IdentityPolicy
Policy reports what this redactor was built for.
type ResourceRedactor ¶
type ResourceRedactor struct {
// contains filtered or unexported fields
}
ResourceRedactor decides how much a span may say about which resource a request named.
Why this follows the identity policy ¶
A resource URI in this server embeds project and group identifiers, so it says what a caller is working on. That is the same class of disclosure as saying who the caller is, and an operator has already made exactly that decision once by choosing an identity policy. Asking them to make it twice, through a second flag with its own defaults, would produce deployments where the two disagree and nobody meant them to.
So the mapping is the one the policy already implies:
none a keyed digest: correlates polls and reads of one resource,
names none of them
pseudonymous the same digest, for the same reason it gives a user one
full the URI itself, beside the user.id and user.name that policy
already records
The digest is the floor rather than nothing, which is where this differs from the identity treatment, and the difference is deliberate. A user digest still follows a person, so recording none is a meaningful default. A resource digest follows a thing the operator's own server was asked to watch, and without it "one subscription is failing every poll" and "every subscription is failing" are the same picture.
func NewResourceRedactor ¶
func NewResourceRedactor(policy IdentityPolicy, keys *Keyring) *ResourceRedactor
NewResourceRedactor builds the redactor for a policy.
func (*ResourceRedactor) ResourceAttributes ¶
func (r *ResourceRedactor) ResourceAttributes(uri string) []attribute.KeyValue
ResourceAttributes returns what may be recorded about one resource URI.
A nil receiver returns nothing, so a caller that never wired one records nothing rather than panicking, and an empty URI returns nothing, so an absent value is absent rather than a digest of the empty string that would look like a real resource.
type Signals ¶
Signals selects which OTel signals are exported.
Separable because their costs differ: traces are per call, metrics are aggregated, and logs duplicate a stream the operator may already collect from stderr. An operator shipping stderr to a log pipeline already wants the first two and not the third.
type Snapshot ¶
type Snapshot struct {
Enabled bool `json:"enabled"`
// Protocol is the transport every enabled signal uses, and is empty when
// they do not all use the same one.
//
// Empty rather than one of them. This field used to hold the traces
// protocol unconditionally, so a metrics-only deployment exporting over
// gRPC published "http/protobuf": a value that was not merely imprecise but
// false, in a document a client reads. A consumer now sees either an answer
// that holds for the whole process or no answer, and SignalProtocols has
// the detail when there is a disagreement to describe.
Protocol string `json:"protocol,omitempty"`
Signals []string `json:"signals,omitempty"`
// Endpoint is the collector this deployment exports to, when the standard
// environment names one and every enabled signal names the same one. It is
// the operator's own address and is reported so somebody connecting to a
// shared endpoint can see where the record of their calls goes, which is
// the point of announcing this at all.
Endpoint string `json:"endpoint,omitempty"`
// SignalProtocols and SignalEndpoints carry the per-signal answers, keyed
// by signal name.
//
// Never serialized: the server card is a public document and its shape is
// worth changing deliberately rather than as a side effect of fixing a
// wrong value. These exist for the startup log, where an operator is
// looking at their own deployment and detail costs nothing.
SignalProtocols map[string]string `json:"-"`
SignalEndpoints map[string]string `json:"-"`
}
Snapshot describes the running configuration for the server card.
It names what is on rather than what the binary can do, matching the subscriptions block: a consumer branches on this deployment's answer.
func CurrentSnapshot ¶
func CurrentSnapshot() Snapshot
CurrentSnapshot reports what telemetry this process is running.
The zero value means off, which is what a caller gets before Start, after Shutdown, and when telemetry was never enabled. No caller needs a nil check or a second branch.
type ToolNamePolicy ¶
type ToolNamePolicy string
ToolNamePolicy decides whether the tool name becomes a metric dimension.
const ( // ToolNameAuto keeps the tool name on the surfaces where it is cheap and // drops it where it is not. The default, and the only value most operators // should need. ToolNameAuto ToolNamePolicy = "auto" // ToolNameOn keeps it on every surface, for an operator who has decided // their backend can afford the series and wants per-tool latency. ToolNameOn ToolNamePolicy = "on" // ToolNameOff drops it everywhere, for the smallest possible metric // footprint. ToolNameOff ToolNamePolicy = "off" )
func ParseToolNamePolicy ¶
func ParseToolNamePolicy(value string) (ToolNamePolicy, error)
ParseToolNamePolicy validates an operator-supplied value.
An unrecognized value is an error rather than a silent default, for the same reason as everywhere else in this package: an operator who typed "yes" and got "auto" would find a dimension missing on one surface and present on another, with nothing anywhere saying why.