-
Notifications
You must be signed in to change notification settings - Fork 2.1k
[docs/rfc] Add component interface guidelines #14532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
codeboten
merged 22 commits into
open-telemetry:main
from
jmacd:jmacd/component_interface_v3
Apr 22, 2026
+320
−5
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
43795f2
Component interface guidelines
jmacd 86d3c46
new ref
jmacd 0d34a9d
Update docs/rfcs/component-interfaces.md
codeboten ea358aa
Merge branch 'main' of github.com:open-telemetry/opentelemetry-collec…
jmacd 43560ed
revision
jmacd 393ed29
Merge branch 'jmacd/component_interface_v3' of github.com:jmacd/opent…
jmacd 87d475d
spelling
jmacd a4468d6
lint
jmacd 69068b3
Merge branch 'main' into jmacd/component_interface_v3
jmacd 3707ed7
link in CONTRIBUTING
jmacd 4442a53
Merge branch 'main' of github.com:open-telemetry/opentelemetry-collec…
jmacd 9f82840
apply feedback
jmacd 46b2fba
Merge branch 'main' into jmacd/component_interface_v3
jmacd 157c64c
Merge branch 'main' into jmacd/component_interface_v3
jmacd bfd768a
Merge branch 'main' into jmacd/component_interface_v3
jmacd 46a0b47
Merge branch 'main' into jmacd/component_interface_v3
jmacd 38ab4db
Merge branch 'main' of github.com:open-telemetry/opentelemetry-collec…
jmacd f7dbb1d
contributing lint
jmacd 49db080
Merge branch 'jmacd/component_interface_v3' of github.com:jmacd/opent…
jmacd 07527f9
Merge branch 'main' into jmacd/component_interface_v3
mx-psi 49cf137
clean up lint errors
codeboten 4c1c218
Merge branch 'main' into jmacd/component_interface_v3
codeboten File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,307 @@ | ||
| # Component Interface Guidelines | ||
|
|
||
| ## Overview | ||
|
|
||
| The OpenTelemetry Collector has a number of public interfaces and | ||
| extension points that require careful attention to avoid breaking | ||
| changes for users. | ||
|
|
||
| These guidelines describe how to achieve safe interface evolution in | ||
| Golang. This approach is recommended for all Golang modules that want | ||
| a safe approach to interface evolution. | ||
|
|
||
| When an interface type is exported for users outside of this | ||
| repository, the type MUST follow these guidelines. This does not | ||
| apply to internal packages, by definition. | ||
|
|
||
| ### Quick Reference | ||
|
|
||
| | Aspect | Sealed Interface | Open Interface | | ||
| |--------|-----------------|----------------| | ||
| | Purpose | Package-provided implementation | Capability detection | | ||
| | `private()` method | Yes | No | | ||
| | Constructor | Required (`NewX`) | Not used | | ||
| | External implementation | Prevented | Encouraged | | ||
| | Evolution | Add methods + options | Add companion interfaces | | ||
| | Example | `receiver.Factory` | `extensionmiddleware.GRPCClient` | | ||
|
|
||
| ## Background | ||
|
|
||
| As its most prominent feature, for every method in the public | ||
| interface, a corresponding `type <Method>Func func(...) ...` | ||
| declaration in the same package will exist. The method and the | ||
| function have the same signature, by design. | ||
|
|
||
| The motivation for this pattern is to support safe, incremental | ||
| evolution of public interfaces. By pairing each interface method with | ||
| a corresponding function type, we gain important properties: callers | ||
| can pass `nil` for no-op behavior, types can embed the function type | ||
| to automatically satisfy the interface, and new methods can be added | ||
| to sealed interfaces through new function types and constructor | ||
| options—all without breaking existing code. | ||
|
|
||
| Users of the [`net/http` package have seen this | ||
| pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` | ||
| can be seen as a prototype for this pattern, in this case for HTTP | ||
| servers. The public interface type `http.Handler`: | ||
|
|
||
| ```go | ||
| // A Handler responds to an HTTP request. | ||
| type Handler interface { | ||
| ServeHTTP(ResponseWriter, *Request) | ||
| } | ||
| ``` | ||
|
|
||
| has a corresponding function type: | ||
|
|
||
| ```go | ||
| // The HandlerFunc type is an adapter to allow the use of | ||
| // ordinary functions as HTTP handlers. If f is a function | ||
| // with the appropriate signature, HandlerFunc(f) is a | ||
| // [Handler] that calls f. | ||
| type HandlerFunc func(ResponseWriter, *Request) | ||
|
|
||
| // ServeHTTP calls f(w, r). | ||
| func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { | ||
| f(w, r) | ||
| } | ||
| ``` | ||
|
|
||
| We use this pattern extensively, however there are important | ||
| differences with ours and this specific case: | ||
|
|
||
| - Public interfaces can only refer to other public interfaces, not | ||
| concrete types. The `*Request` (pointer-to-struct) is not compatible | ||
| with safe interface evolution. | ||
| - Every function type must have a simple "no-op" behavior | ||
| corresponding with its nil value. The Golang `HandlerFunc` | ||
| implementation does not have a no-op implementation (callers are | ||
| required to pass `func(http.ResponseWriter, *http.Request) {}` for | ||
| no-op behavior). | ||
|
|
||
| In our version of this, the implementation is required to check for | ||
| nil and expose only other interfaces for parameter- and return-types, | ||
| themselves subject to the same safety requirements. | ||
|
|
||
| ```go | ||
| // SAFE VERSION satisfies requirements because arguments are interfaces | ||
| // and nil is checked. | ||
| // | ||
| // ServeHTTP calls f(w, r). | ||
| func (f HandlerFunc) ServeHTTP(w ResponseWriter, r Request) { | ||
| if f == nil { | ||
| return | ||
| } | ||
| f(w, r) | ||
| } | ||
| ``` | ||
|
|
||
| The function type, in this pattern, corresponds with a single method | ||
| of an exposed `interface` type and has several uses: | ||
|
|
||
| - When a sealed-interface constructor takes `<Method>Func` as its argument, | ||
| callers can pass `nil` for the no-op. | ||
| - When an object embeds the `<Method>Func`, they inherit an | ||
| implementation of the interface. Uninitialized types that embed `<Method>Func` | ||
| automatically gain a no-op implementation of the associated method. | ||
|
|
||
| ## Interface Implementation Patterns | ||
|
|
||
| Public interfaces in the Collector fall into two categories: **sealed | ||
| interfaces** and **open interfaces**. | ||
|
|
||
| Sealed interfaces are provided by a package with safe-evolution in | ||
| mind, not for external implementations. The use of sealed interfaces | ||
| allows a repository to stabilize public methods while ensuring the | ||
| ability to add future methods by preventing external | ||
| construction. External implementations are prohibited in this | ||
| case. Examples include the `Factory` interface and `NewFactory` | ||
| constructor used to register Collector components in each of the | ||
| `receiver`, `processor`, `exporter`, `connector`, and `extension` | ||
| sub-modules. In this example, new factory methods can be added in the | ||
| future, through functional options, without breaking existing consumers | ||
| or providers. | ||
|
|
||
| Open interfaces, also known as **optional interfaces**, are meant to | ||
| enable extension points, allowing capability detection on a | ||
| method-by-method basis. Open interfaces are implemented by components | ||
| to provide capabilities discovered through type assertions. While it | ||
| is not safe to add a new method to an open interface, as it will | ||
| break existing implementations, it is safe to add new companion | ||
| interfaces that serve equivalent functions. Examples include | ||
| `extensionmiddleware.HTTPClient` and `extensionmiddleware.GRPCServer`, | ||
| our middleware interfaces. In this example, new protocols can have | ||
| middleware support added in the future, and we can adjust to changes | ||
| in the existing HTTP and gRPC middleware. | ||
|
|
||
| ## Key Concepts | ||
|
|
||
| ### Two Categories of Public Interfaces | ||
|
|
||
| Public interfaces in the Collector fall into two categories based on | ||
| how they are discovered and used. | ||
|
|
||
| **Sealed interfaces** are provided by a package and consumed by users. | ||
| They use an unexported `private()` method to prevent external | ||
| implementations, guaranteeing all implementations come through | ||
| package-provided constructors. Following the [guidance in "working | ||
| with | ||
| interfaces"](https://go.dev/blog/module-compatibility#working-with-interfaces), | ||
| this practice enables safely evolving interfaces because users are | ||
| forced to use constructor functions. Examples include | ||
| `receiver.Factory` and `component.Host`. | ||
|
|
||
| **Open interfaces**, also called **optional interfaces**, are | ||
| implemented by components and discovered through type assertions at | ||
| runtime. Consumers check if a component implements a capability using | ||
| Go's type assertion syntax. Multiple independent implementations | ||
| exist, and components "opt in" to providing a feature. Examples | ||
| include `extensionmiddleware.GRPCClient` and `extensionauth.Client`. | ||
|
|
||
| The key distinction: sealed interfaces prevent external | ||
| implementation; open interfaces invite it. | ||
|
|
||
| ### When to Seal an Interface | ||
|
|
||
| Interfaces MUST be sealed when the package provides the canonical | ||
| implementation and needs guaranteed control over all instances. This | ||
| is appropriate when interface evolution requires adding methods that | ||
| all implementations must have, and when the Functional Option pattern | ||
| will be used to extend capabilities over time. | ||
|
|
||
| ### Evolving Sealed Interfaces | ||
|
|
||
| Sealed interfaces evolve by adding new methods with corresponding | ||
| function types and options. Because all implementations use | ||
| package-provided constructors, new methods can be added safely. | ||
|
|
||
| The `receiver.Factory` interface demonstrates this pattern: | ||
|
|
||
| ```go | ||
| // Factory is sealed—external implementations prevented | ||
| type Factory interface { | ||
| // Embed a sealed interface from another module | ||
| component.Factory | ||
|
|
||
| // Traces-specific methods | ||
| CreateTraces(...) (Traces, error) | ||
| TracesStability() component.StabilityLevel | ||
|
|
||
| // ... additional methods for other signals | ||
|
|
||
| unexportedFactoryFunc() // sealing method | ||
| } | ||
|
|
||
| // <Method>Func for CreateTraces | ||
| type CreateTracesFunc func(context.Context, Settings, component.Config, consumer.Traces) (Traces, error) | ||
|
|
||
| // Functional option for traces capability | ||
| func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) FactoryOption { ... } | ||
|
|
||
| // Constructor uses functional options | ||
| func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory | ||
| ``` | ||
|
|
||
| This pattern supports adding a new kind of signal to the interface, | ||
| safely, for example by adding `CreateProfiles()`, `CreateProfilesFunc`, and `WithProfiles`. | ||
|
|
||
| If the functional option pattern is already in use, new interface | ||
| methods require only new options. If there is not an existing | ||
| functional option pattern, it is still safe to extend the interface by | ||
| creating a new constructor. | ||
|
|
||
| Major interfaces SHOULD use the functional option pattern even when | ||
| there are initially no options. | ||
|
|
||
| ### Evolving Open Interfaces | ||
|
|
||
| Open interfaces evolve by creating new companion interfaces for new | ||
| capabilities. Open interfaces MUST remain unchanged until a major | ||
| version bump, to preserve compatibility. | ||
|
|
||
| Imagine a new RPC framework is introduced, with a new kind of client | ||
| configuration that middleware extensions can implement. We cannot | ||
| extend the existing interfaces, but we can create new ones, | ||
|
|
||
| ```go | ||
| // Imagine a new RPC framework called "Super". | ||
| type SuperClient interface { | ||
| GetSuperClientOptions(context.Context) ([]super.ClientOption, error) | ||
| } | ||
| ``` | ||
|
|
||
| We can also accommodate new interface types corresponding with | ||
| existing frameworks. For example, if gRPC decides to add a new sort of | ||
| configuration type, we can add an optional new interface, | ||
|
|
||
| ```go | ||
| // Imagine a V2 gRPC option type. | ||
| type GRPCClientV2 interface { | ||
| GetGRPCClientOptionsV2(context.Context) ([]grpc.ClientOptionV2, error) | ||
| } | ||
| ``` | ||
|
|
||
| The new middleware extension can be added without changing the existing | ||
| implementations, because only a user of the new framework needs to know | ||
| about the new kind of middleware. | ||
|
|
||
| This works because components generally know which specific extension | ||
| interface they want. When there is more than one viable extension | ||
| interface, callers can choose the one they prefer depending on the use. | ||
|
|
||
| ### Test Helpers with NewNop and NewErr | ||
|
|
||
| Every public and extension interface package SHOULD provide test | ||
| helpers in a `<package>test` subpackage using a dedicated Go module | ||
| (`go.mod`). These helpers generally can be composed of a | ||
| `<Method>Func` for every method in the extension. | ||
|
|
||
| For example, here is a type for use in testing that implements every | ||
| public interface method, making it easy for tests to override only one | ||
| method. | ||
|
|
||
| ```go | ||
| type baseExtension struct { | ||
| component.StartFunc | ||
| component.ShutdownFunc | ||
| extensionmiddleware.GetHTTPRoundTripperFunc | ||
| extensionmiddleware.GetGRPCClientOptionsFunc | ||
| extensionmiddleware.GetGRPCClientOptionsContextFunc | ||
|
|
||
| // ... new middleware interfaces can be added using the corresponding | ||
| // <Method>Funcs. | ||
| } | ||
| ``` | ||
|
|
||
| Two constructors are typically provided for standard testing: | ||
|
|
||
| `NewNop()` returns an extension where all methods have no-op behavior. | ||
| Because nil function types return zero values, `&baseExtension{}` is a | ||
| valid do-nothing implementation. | ||
|
|
||
| `NewErr(err error)` returns an extension where all methods return the | ||
| specified error, enabling testing of error handling paths. | ||
|
|
||
| ## Examples | ||
|
|
||
| The `extensionmiddleware` package demonstrates this pattern with open | ||
| interfaces for capability detection: | ||
|
|
||
| - **Interface definitions**: | ||
| [extension/extensionmiddleware/client.go](../../extension/extensionmiddleware/client.go) | ||
| defines `GRPCClient`, `GRPCClientContext`, and their corresponding | ||
| function types | ||
| - **Consumer uses both interfaces**: | ||
| [config/configmiddleware/configmiddleware.go](../../config/configmiddleware/configmiddleware.go) | ||
| shows cascading type assertions to detect capabilities | ||
| - **Test helpers**: | ||
| [extension/extensionmiddleware/extensionmiddlewaretest/err.go](../../extension/extensionmiddleware/extensionmiddlewaretest/err.go) | ||
| demonstrates `baseExtension` struct embedding all function types | ||
| with `NewNop()` and `NewErr()` | ||
|
|
||
| ## References | ||
|
|
||
| - [Go Blog: Working with Interfaces](https://go.dev/blog/module-compatibility#working-with-interfaces) | ||
| - [http.HandlerFunc](https://pkg.go.dev/net/http#HandlerFunc)—prior art | ||
| - [Functional Options Pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) | ||
| - [Function Types in Go](https://kinbiko.com/posts/2021-01-10-function-types-in-go/) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.