| # Contributing to opentelemetry-go | |
| The Go special interest group (SIG) meets regularly. See the | |
| OpenTelemetry | |
| [community](https://github.com/open-telemetry/community#golang-sdk) | |
| repo for information on this and other language SIGs. | |
| See the [public meeting | |
| notes](https://docs.google.com/document/d/1E5e7Ld0NuU1iVvf-42tOBpu2VBBLYnh73GJuITGJTTU/edit) | |
| for a summary description of past meetings. To request edit access, | |
| join the meeting or get in touch on the | |
| [#otel-go](https://cloud-native.slack.com/archives/C01NPAXACKT) | |
| channel on CNCF Slack. If you are new to the CNCF Slack community, | |
| you can [create an account](https://slack.cncf.io/). | |
| The meeting is open for all to join. We invite everyone to join our | |
| meeting, regardless of your experience level. Whether you're a | |
| seasoned OpenTelemetry developer, just starting your journey, or | |
| simply curious about the work we do, you're more than welcome to | |
| participate! | |
| ## Before Opening an Issue or Pull Request | |
| Before opening an issue or pull request, double-check whether the report or | |
| proposed change could involve a security vulnerability. This applies to all | |
| contributors, including AI agents. | |
| Do not open public issues or public pull requests (including draft PRs) for | |
| known or suspected security vulnerabilities or their fixes. Follow the | |
| [OpenTelemetry security policy](https://github.com/open-telemetry/.github/blob/main/SECURITY.md) | |
| and report them privately using | |
| [Report a vulnerability](https://github.com/open-telemetry/opentelemetry-go/security/advisories/new). | |
| If you are unsure whether a problem is a security vulnerability, use the private | |
| reporting process first. Keep vulnerability details, reproductions, and fixes | |
| private, and follow the maintainers' instructions for coordinated disclosure. | |
| ## Development | |
| You can view and edit the source code by cloning this repository: | |
| ```sh | |
| git clone https://github.com/open-telemetry/opentelemetry-go.git | |
| ``` | |
| Run `make test` to run the tests instead of `go test`. | |
| There are some generated files checked into the repo. To make sure | |
| that the generated files are up-to-date, run `make` (or `make | |
| precommit` - the `precommit` target is the default). | |
| The `precommit` target also fixes the formatting of the code and | |
| checks the status of the go module files. | |
| Additionally, there is a `codespell` target that checks for common | |
| typos in the code. It is not run by default, but you can run it | |
| manually with `make codespell`. It will set up a virtual environment | |
| in `venv` and install `codespell` there. | |
| If after running `make precommit` the output of `git status` contains | |
| `nothing to commit, working tree clean` then it means that everything | |
| is up-to-date and properly formatted. | |
| ## Pull Requests | |
| ### How to Send Pull Requests | |
| Everyone is welcome to contribute code to `opentelemetry-go` via | |
| GitHub pull requests (PRs). | |
| To create a new PR, fork the project in GitHub and clone the upstream | |
| repo: | |
| ```sh | |
| go get -d go.opentelemetry.io/otel | |
| ``` | |
| (This may print some warning about "build constraints exclude all Go | |
| files", just ignore it.) | |
| This will put the project in `${GOPATH}/src/go.opentelemetry.io/otel`. | |
| Alternatively, you can use `git` directly with: | |
| ```sh | |
| git clone https://github.com/open-telemetry/opentelemetry-go | |
| ``` | |
| (Note that `git clone` is *not* using the `go.opentelemetry.io/otel` name - | |
| that name is a kind of a redirector to GitHub that `go get` can | |
| understand, but `git` does not.) | |
| This will add the project as `opentelemetry-go` within the current directory. | |
| Enter the newly created directory and add your fork as a new remote: | |
| ```sh | |
| git remote add <YOUR_FORK> git@github.com:<YOUR_GITHUB_USERNAME>/opentelemetry-go | |
| ``` | |
| Check out a new branch, make modifications, run linters and tests, update | |
| `CHANGELOG.md`, and push the branch to your fork: | |
| ```sh | |
| git checkout -b <YOUR_BRANCH_NAME> | |
| # edit files | |
| # update changelog | |
| make precommit | |
| git add -p | |
| git commit | |
| git push <YOUR_FORK> <YOUR_BRANCH_NAME> | |
| ``` | |
| Open a pull request against the main `opentelemetry-go` repo. Be sure to add the pull | |
| request ID to the entry you added to `CHANGELOG.md`. | |
| Avoid rebasing and force-pushing to your branch to facilitate reviewing the pull request. | |
| Rewriting Git history makes it difficult to keep track of iterations during code review. | |
| All pull requests are squashed to a single commit upon merge to `main`. | |
| ### How to Receive Comments | |
| * If the PR is not ready for review, please put `[WIP]` in the title, | |
| tag it as `work-in-progress`, or mark it as | |
| [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). | |
| * Make sure CLA is signed and CI is clear. | |
| ### How to Get PRs Merged | |
| A PR is considered **ready to merge** when: | |
| * It has received two qualified approvals[^1]. | |
| This is not enforced through automation, but needs to be validated by the | |
| maintainer merging. | |
| * At least one of the qualified approvals needs to be from an | |
| [Approver]/[Maintainer] affiliated with a different company than the author | |
| of the PR. | |
| * PRs introducing changes that have already been discussed and consensus | |
| reached only need one qualified approval. The discussion and resolution | |
| needs to be linked to the PR. | |
| * Trivial changes[^2] only need one qualified approval. | |
| * All feedback has been addressed. | |
| * All PR comments and suggestions are resolved. | |
| * All GitHub Pull Request reviews with a status of "Request changes" have | |
| been addressed. Another review by the objecting reviewer with a different | |
| status can be submitted to clear the original review, or the review can be | |
| dismissed by a [Maintainer] when the issues from the original review have | |
| been addressed. | |
| * Any comments or reviews that cannot be resolved between the PR author and | |
| reviewers can be submitted to the community [Approver]s and [Maintainer]s | |
| during the weekly SIG meeting. If consensus is reached among the | |
| [Approver]s and [Maintainer]s during the SIG meeting the objections to the | |
| PR may be dismissed or resolved or the PR closed by a [Maintainer]. | |
| * Any substantive changes to the PR require existing Approval reviews be | |
| cleared unless the approver explicitly states that their approval persists | |
| across changes. This includes changes resulting from other feedback. | |
| [Approver]s and [Maintainer]s can help in clearing reviews and they should | |
| be consulted if there are any questions. | |
| * The PR branch is up to date with the base branch it is merging into. | |
| * To ensure this does not block the PR, it should be configured to allow | |
| maintainers to update it. | |
| * It has been open for review for at least one working day. This gives people | |
| reasonable time to review. | |
| * Trivial changes[^2] do not have to wait for one day and may be merged with | |
| a single [Maintainer]'s approval. | |
| * All required GitHub workflows have succeeded. | |
| * Urgent fix can take exception as long as it has been actively communicated | |
| among [Maintainer]s. | |
| Any [Maintainer] can merge the PR once the above criteria have been met. | |
| [^1]: A qualified approval is a GitHub Pull Request review with "Approve" | |
| status from an OpenTelemetry Go [Approver] or [Maintainer]. | |
| [^2]: Trivial changes include: typo corrections, cosmetic non-substantive | |
| changes, documentation corrections or updates, dependency updates, etc. | |
| ## Design Choices | |
| As with other OpenTelemetry clients, opentelemetry-go follows the | |
| [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel). | |
| It's especially valuable to read through the [library | |
| guidelines](https://opentelemetry.io/docs/specs/otel/library-guidelines). | |
| ### Focus on Capabilities, Not Structure Compliance | |
| OpenTelemetry is an evolving specification, one where the desires and | |
| use cases are clear, but the methods to satisfy those use cases are | |
| not. | |
| As such, Contributions should provide functionality and behavior that | |
| conforms to the specification, but the interface and structure are | |
| flexible. | |
| It is preferable to have contributions follow the idioms of the | |
| language rather than conform to specific API names or argument | |
| patterns in the spec. | |
| For a deeper discussion, see | |
| [this](https://github.com/open-telemetry/opentelemetry-specification/issues/165). | |
| ## Tests | |
| Each functionality should be covered by tests. | |
| Performance-critical functionality should also be covered by benchmarks. | |
| - Pull requests adding a performance-critical functionality | |
| should have `go test -bench` output in their description. | |
| - Pull requests changing a performance-critical functionality | |
| should have [`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) | |
| output in their description. | |
| ## Dependencies | |
| This project uses [Go Modules] for dependency management. All modules will use | |
| `go.mod` to explicitly list all direct and indirect dependencies, ensuring a | |
| clear dependency graph. The `go.sum` file for each module will be committed to | |
| the repository and used to verify the integrity of downloaded modules, | |
| preventing malicious tampering. | |
| This project uses automated dependency update tools (i.e. dependabot, | |
| renovatebot) to manage updates to dependencies. This ensures that dependencies | |
| are kept up-to-date with the latest security patches and features and are | |
| reviewed before being merged. If you would like to propose a change to a | |
| dependency it should be done through a pull request that updates the `go.mod` | |
| file and includes a description of the change. | |
| See the [versioning and compatibility](./VERSIONING.md) policy for more details | |
| about dependency compatibility. | |
| [Go Modules]: https://pkg.go.dev/cmd/go#hdr-Modules__module_versions__and_more | |
| ### Environment Dependencies | |
| This project does not partition dependencies based on the environment (i.e. | |
| `development`, `staging`, `production`). | |
| Only the dependencies explicitly included in the released modules have been | |
| tested and verified to work with the released code. No other guarantee is made | |
| about the compatibility of other dependencies. | |
| ## Documentation | |
| Each (non-internal, non-test) package must be documented using | |
| [Go Doc Comments](https://go.dev/doc/comment), | |
| preferably in a `doc.go` file. | |
| Prefer using [Examples](https://pkg.go.dev/testing#hdr-Examples) | |
| instead of putting code snippets in Go doc comments. | |
| In some cases, you can even create [Testable Examples](https://go.dev/blog/examples). | |
| You can install and run a "local Go Doc site" in the following way: | |
| ```sh | |
| go install golang.org/x/pkgsite/cmd/pkgsite@latest | |
| pkgsite | |
| ``` | |
| [`go.opentelemetry.io/otel/metric`](https://pkg.go.dev/go.opentelemetry.io/otel/metric) | |
| is an example of a very well-documented package. | |
| ### README files | |
| Each (non-internal, non-test, non-documentation) package must contain a | |
| `README.md` file containing at least a title, and a `pkg.go.dev` badge. | |
| The README should not be a repetition of Go doc comments. | |
| You can verify the presence of all README files with the `make verify-readmes` | |
| command. | |
| ## Style Guide | |
| One of the primary goals of this project is that it is actually used by | |
| developers. With this goal in mind the project strives to build | |
| user-friendly and idiomatic Go code adhering to the Go community's best | |
| practices. | |
| For a non-comprehensive but foundational overview of these best practices | |
| the [Effective Go](https://golang.org/doc/effective_go.html) documentation | |
| is an excellent starting place. | |
| We also recommend following the | |
| [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) | |
| that collects common comments made during reviews of Go code. | |
| As a convenience for developers building this project the `make precommit` | |
| will format, lint, validate, and in some cases fix the changes you plan to | |
| submit. This check will need to pass for your changes to be able to be | |
| merged. | |
| In addition to idiomatic Go, the project has adopted certain standards for | |
| implementations of common patterns. These standards should be followed as a | |
| default, and if they are not followed documentation needs to be included as | |
| to the reasons why. | |
| ### Configuration | |
| When creating an instantiation function for a complex `type T struct`, it is | |
| useful to allow variable number of options to be applied. However, the strong | |
| type system of Go restricts the function design options. There are a few ways | |
| to solve this problem, but we have landed on the following design. | |
| #### `config` | |
| Configuration should be held in a `struct` named `config`, or prefixed with | |
| specific type name this Configuration applies to if there are multiple | |
| `config` in the package. This type must contain configuration options. | |
| ```go | |
| // config contains configuration options for a thing. | |
| type config struct { | |
| // options ... | |
| } | |
| ``` | |
| In general the `config` type will not need to be used externally to the | |
| package and should be unexported. If, however, it is expected that the user | |
| will likely want to build custom options for the configuration, the `config` | |
| should be exported. Please, include in the documentation for the `config` | |
| how the user can extend the configuration. | |
| It is important that internal `config` are not shared across package boundaries. | |
| Meaning a `config` from one package should not be directly used by another. The | |
| one exception is the API packages. The configs from the base API, eg. | |
| `go.opentelemetry.io/otel/trace.TracerConfig` and | |
| `go.opentelemetry.io/otel/metric.InstrumentConfig`, are intended to be consumed | |
| by the SDK therefore it is expected that these are exported. | |
| When a config is exported we want to maintain forward and backward | |
| compatibility, to achieve this no fields should be exported but should | |
| instead be accessed by methods. | |
| Optionally, it is common to include a `newConfig` function (with the same | |
| naming scheme). This function wraps any defaults setting and looping over | |
| all options to create a configured `config`. | |
| ```go | |
| // newConfig returns an appropriately configured config. | |
| func newConfig(options ...Option) config { | |
| // Set default values for config. | |
| config := config{/* […] */} | |
| for _, option := range options { | |
| config = option.apply(config) | |
| } | |
| // Perform any validation here. | |
| return config | |
| } | |
| ``` | |
| If validation of the `config` options is also performed this can return an | |
| error as well that is expected to be handled by the instantiation function | |
| or propagated to the user. | |
| Given the design goal of not having the user need to work with the `config`, | |
| the `newConfig` function should also be unexported. | |
| #### `Option` | |
| To set the value of the options a `config` contains, a corresponding | |
| `Option` interface type should be used. | |
| ```go | |
| type Option interface { | |
| apply(config) config | |
| } | |
| ``` | |
| Having `apply` unexported makes sure that it will not be used externally. | |
| Moreover, the interface becomes sealed so the user cannot easily implement | |
| the interface on its own. | |
| The `apply` method should return a modified version of the passed config. | |
| This approach, instead of passing a pointer, is used to prevent the config from being allocated to the heap. | |
| The name of the interface should be prefixed in the same way the | |
| corresponding `config` is (if at all). | |
| #### Options | |
| All user configurable options for a `config` must have a related unexported | |
| implementation of the `Option` interface and an exported configuration | |
| function that wraps this implementation. | |
| The wrapping function name should be prefixed with `With*` (or in the | |
| special case of a boolean options `Without*`) and should have the following | |
| function signature. | |
| ```go | |
| func With*(…) Option { … } | |
| ``` | |
| ##### `bool` Options | |
| ```go | |
| type defaultFalseOption bool | |
| func (o defaultFalseOption) apply(c config) config { | |
| c.Bool = bool(o) | |
| return c | |
| } | |
| // WithOption sets a T to have an option included. | |
| func WithOption() Option { | |
| return defaultFalseOption(true) | |
| } | |
| ``` | |
| ```go | |
| type defaultTrueOption bool | |
| func (o defaultTrueOption) apply(c config) config { | |
| c.Bool = bool(o) | |
| return c | |
| } | |
| // WithoutOption sets a T to have Bool option excluded. | |
| func WithoutOption() Option { | |
| return defaultTrueOption(false) | |
| } | |
| ``` | |
| ##### Declared Type Options | |
| ```go | |
| type myTypeOption struct { | |
| MyType MyType | |
| } | |
| func (o myTypeOption) apply(c config) config { | |
| c.MyType = o.MyType | |
| return c | |
| } | |
| // WithMyType sets T to have include MyType. | |
| func WithMyType(t MyType) Option { | |
| return myTypeOption{t} | |
| } | |
| ``` | |
| ##### Functional Options | |
| ```go | |
| type optionFunc func(config) config | |
| func (fn optionFunc) apply(c config) config { | |
| return fn(c) | |
| } | |
| // WithMyType sets t as MyType. | |
| func WithMyType(t MyType) Option { | |
| return optionFunc(func(c config) config { | |
| c.MyType = t | |
| return c | |
| }) | |
| } | |
| ``` | |
| #### Instantiation | |
| Using this configuration pattern to configure instantiation with a `NewT` | |
| function. | |
| ```go | |
| func NewT(options ...Option) T {…} | |
| ``` | |
| Any required parameters can be declared before the variadic `options`. | |
| #### Dealing with Overlap | |
| Sometimes there are multiple complex `struct` that share common | |
| configuration and also have distinct configuration. To avoid repeated | |
| portions of `config`s, a common `config` can be used with the union of | |
| options being handled with the `Option` interface. | |
| For example. | |
| ```go | |
| // config holds options for all animals. | |
| type config struct { | |
| Weight float64 | |
| Color string | |
| MaxAltitude float64 | |
| } | |
| // DogOption apply Dog specific options. | |
| type DogOption interface { | |
| applyDog(config) config | |
| } | |
| // BirdOption apply Bird specific options. | |
| type BirdOption interface { | |
| applyBird(config) config | |
| } | |
| // Option apply options for all animals. | |
| type Option interface { | |
| BirdOption | |
| DogOption | |
| } | |
| type weightOption float64 | |
| func (o weightOption) applyDog(c config) config { | |
| c.Weight = float64(o) | |
| return c | |
| } | |
| func (o weightOption) applyBird(c config) config { | |
| c.Weight = float64(o) | |
| return c | |
| } | |
| func WithWeight(w float64) Option { return weightOption(w) } | |
| type furColorOption string | |
| func (o furColorOption) applyDog(c config) config { | |
| c.Color = string(o) | |
| return c | |
| } | |
| func WithFurColor(c string) DogOption { return furColorOption(c) } | |
| type maxAltitudeOption float64 | |
| func (o maxAltitudeOption) applyBird(c config) config { | |
| c.MaxAltitude = float64(o) | |
| return c | |
| } | |
| func WithMaxAltitude(a float64) BirdOption { return maxAltitudeOption(a) } | |
| func NewDog(name string, o ...DogOption) Dog {…} | |
| func NewBird(name string, o ...BirdOption) Bird {…} | |
| ``` | |
| ### Interfaces | |
| To allow other developers to better comprehend the code, it is important | |
| to ensure it is sufficiently documented. One simple measure that contributes | |
| to this aim is self-documenting by naming method parameters. Therefore, | |
| where appropriate, methods of every exported interface type should have | |
| their parameters appropriately named. | |
| #### Interface Stability | |
| All exported stable interfaces that include the following warning in their | |
| documentation are allowed to be extended with additional methods. | |
| > Warning: methods may be added to this interface in minor releases. | |
| These interfaces are defined by the OpenTelemetry specification and will be | |
| updated as the specification evolves. | |
| Otherwise, stable interfaces MUST NOT be modified. | |
| #### How to Change Specification Interfaces | |
| When an API change must be made, we will update the SDK with the new method one | |
| release before the API change. This will allow the SDK one version before the | |
| API change to work seamlessly with the new API. | |
| If an incompatible version of the SDK is used with the new API the application | |
| will fail to compile. | |
| #### How Not to Change Specification Interfaces | |
| We have explored using a v2 of the API to change interfaces and found that there | |
| was no way to introduce a v2 and have it work seamlessly with the v1 of the API. | |
| Problems happened with libraries that upgraded to v2 when an application did not, | |
| and would not produce any telemetry. | |
| More detail of the approaches considered and their limitations can be found in | |
| the [Use a V2 API to evolve interfaces](https://github.com/open-telemetry/opentelemetry-go/issues/3920) | |
| issue. | |
| #### How to Change Other Interfaces | |
| If new functionality is needed for an interface that cannot be changed it MUST | |
| be added by including an additional interface. That added interface can be a | |
| simple interface for the specific functionality that you want to add or it can | |
| be a super-set of the original interface. For example, if you wanted to a | |
| `Close` method to the `Exporter` interface: | |
| ```go | |
| type Exporter interface { | |
| Export() | |
| } | |
| ``` | |
| A new interface, `Closer`, can be added: | |
| ```go | |
| type Closer interface { | |
| Close() | |
| } | |
| ``` | |
| Code that is passed the `Exporter` interface can now check to see if the passed | |
| value also satisfies the new interface. E.g. | |
| ```go | |
| func caller(e Exporter) { | |
| /* ... */ | |
| if c, ok := e.(Closer); ok { | |
| c.Close() | |
| } | |
| /* ... */ | |
| } | |
| ``` | |
| Alternatively, a new type that is the super-set of an `Exporter` can be created. | |
| ```go | |
| type ClosingExporter struct { | |
| Exporter | |
| Close() | |
| } | |
| ``` | |
| This new type can be used similar to the simple interface above in that a | |
| passed `Exporter` type can be asserted to satisfy the `ClosingExporter` type | |
| and the `Close` method called. | |
| This super-set approach can be useful if there is explicit behavior that needs | |
| to be coupled with the original type and passed as a unified type to a new | |
| function, but, because of this coupling, it also limits the applicability of | |
| the added functionality. If there exist other interfaces where this | |
| functionality should be added, each one will need their own super-set | |
| interfaces and will duplicate the pattern. For this reason, the simple targeted | |
| interface that defines the specific functionality should be preferred. | |
| See also: | |
| [Keeping Your Modules Compatible: Working with interfaces](https://go.dev/blog/module-compatibility#working-with-interfaces). | |
| ### Testing | |
| We allow using [`testify`](https://github.com/stretchr/testify) even though | |
| it is seen as non-idiomatic according to | |
| the [Go Test Comments](https://go.dev/wiki/TestComments#assert-libraries) page. | |
| The tests should never leak goroutines. | |
| Use the term `ConcurrentSafe` in the test name when it aims to verify the | |
| absence of race conditions. The top-level tests with this term will be run | |
| many times in the `test-concurrent-safe` CI job to increase the chance of | |
| catching concurrency issues. This does not apply to subtests when this term | |
| is not in their root name. | |
| ### Internal packages | |
| The use of internal packages should be scoped to a single module. A sub-module | |
| should never import from a parent internal package. This creates a coupling | |
| between the two modules where a user can upgrade the parent without the child, | |
| and if the internal package API has changed, it will fail to upgrade[^3]. | |
| There are two known exceptions to this rule: | |
| - `go.opentelemetry.io/otel/internal/global` | |
| - This package manages global state for all of opentelemetry-go. It needs to | |
| be a single package in order to ensure the uniqueness of the global state. | |
| - `go.opentelemetry.io/otel/internal/baggage` | |
| - This package provides values in a `context.Context` that need to be | |
| recognized by `go.opentelemetry.io/otel/baggage` and | |
| `go.opentelemetry.io/otel/bridge/opentracing` but remain private. | |
| If you have duplicate code in multiple modules, make that code into a Go | |
| template stored in `go.opentelemetry.io/otel/internal/shared` and use [gotmpl] | |
| to render the templates in the desired locations. See [#4404] for an example of | |
| this. | |
| [^3]: https://github.com/open-telemetry/opentelemetry-go/issues/3548 | |
| ### Ignoring context cancellation | |
| OpenTelemetry API implementations need to ignore the cancellation of the context that is | |
| passed when recording a value (e.g. starting a span, recording a measurement, emitting a log). | |
| Recording methods should not return an error describing the cancellation state of the context | |
| when they complete, nor should they abort any work. | |
| This rule may not apply if the OpenTelemetry specification defines a timeout mechanism for | |
| the method. In that case the context cancellation can be used for the timeout with the | |
| restriction that this behavior is documented for the method. Otherwise, timeouts | |
| are expected to be handled by the user calling the API, not the implementation. | |
| Stoppage of the telemetry pipeline is handled by calling the appropriate `Shutdown` method | |
| of a provider. It is assumed the context passed from a user is not used for this purpose. | |
| Outside of the direct recording of telemetry from the API (e.g. exporting telemetry, | |
| force flushing telemetry, shutting down a signal provider) the context cancellation | |
| should be honored. This means all work done on behalf of the user provided context | |
| should be canceled. | |
| ### Observability | |
| OpenTelemetry Go SDK components should be instrumented to enable users observability for the health and performance of the telemetry pipeline itself. | |
| This allows operators to understand how well their observability infrastructure is functioning and to identify potential issues before they impact their applications. | |
| This section outlines the best practices for building instrumentation in OpenTelemetry Go SDK components. | |
| #### Environment Variable Activation | |
| Observability features are currently experimental. | |
| They should be disabled by default and activated through the `OTEL_GO_X_OBSERVABILITY` environment variable. | |
| This follows the established experimental feature pattern used throughout the SDK. | |
| Components should check for this environment variable using a consistent pattern: | |
| ```go | |
| import "go.opentelemetry.io/otel/*/internal/x" | |
| if x.Observability.Enabled() { | |
| // Initialize observability metrics | |
| } | |
| ``` | |
| **References**: | |
| - [stdouttrace exporter](./exporters/stdout/stdouttrace/internal/x/x.go) | |
| - [sdk](./sdk/internal/x/x.go) | |
| #### Encapsulation | |
| Instrumentation should be encapsulated within a dedicated `struct` (e.g. `instrumentation`). | |
| It should not be mixed into the instrumented component. | |
| Prefer this: | |
| ```go | |
| type SDKComponent struct { | |
| inst *instrumentation | |
| } | |
| type instrumentation struct { | |
| inflight otelconv.SDKComponentInflight | |
| exported otelconv.SDKComponentExported | |
| } | |
| ``` | |
| To this: | |
| ```go | |
| // ❌ Avoid this pattern. | |
| type SDKComponent struct { | |
| /* other SDKComponent fields... */ | |
| inflight otelconv.SDKComponentInflight | |
| exported otelconv.SDKComponentExported | |
| } | |
| ``` | |
| The instrumentation code should not bloat the code being instrumented. | |
| Likely, this means its own file, or its own package if it is complex or reused. | |
| #### Initialization | |
| Instrumentation setup should be explicit, side-effect free, and local to the relevant component. | |
| Avoid relying on global or implicit [side effects][side-effect] for initialization. | |
| Encapsulate setup in constructor functions, ensuring clear ownership and scope: | |
| ```go | |
| import ( | |
| "errors" | |
| semconv "go.opentelemetry.io/otel/semconv/v1.43.0" | |
| "go.opentelemetry.io/otel/semconv/v1.43.0/otelconv" | |
| ) | |
| type SDKComponent struct { | |
| inst *instrumentation | |
| } | |
| func NewSDKComponent(config Config) (*SDKComponent, error) { | |
| inst, err := newInstrumentation() | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &SDKComponent{inst: inst}, nil | |
| } | |
| type instrumentation struct { | |
| inflight otelconv.SDKComponentInflight | |
| exported otelconv.SDKComponentExported | |
| } | |
| func newInstrumentation() (*instrumentation, error) { | |
| if !x.Observability.Enabled() { | |
| return nil, nil | |
| } | |
| meter := otel.GetMeterProvider().Meter( | |
| "<component-package-name>", | |
| metric.WithInstrumentationVersion(sdk.Version()), | |
| metric.WithSchemaURL(semconv.SchemaURL), | |
| ) | |
| inst := &instrumentation{} | |
| var err, e error | |
| inst.inflight, e = otelconv.NewSDKComponentInflight(meter) | |
| err = errors.Join(err, e) | |
| inst.exported, e = otelconv.NewSDKComponentExported(meter) | |
| err = errors.Join(err, e) | |
| return inst, err | |
| } | |
| ``` | |
| ```go | |
| // ❌ Avoid this pattern. | |
| func (c *Component) initObservability() { | |
| // Initialize observability metrics | |
| if !x.Observability.Enabled() { | |
| return | |
| } | |
| // Initialize observability metrics | |
| c.inst = &instrumentation{/* ... */} | |
| } | |
| ``` | |
| [side-effect]: https://en.wikipedia.org/wiki/Side_effect_(computer_science) | |
| #### Performance | |
| When observability is disabled or the instrument is not `Enabled`, there should be little to no overhead. | |
| ```go | |
| func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { | |
| if e.inst != nil && e.inst.Enabled(ctx) { | |
| attrs := expensiveOperation() | |
| e.inst.recordSpanInflight(ctx, int64(len(spans)), attrs...) | |
| } | |
| // Export spans... | |
| } | |
| ``` | |
| ```go | |
| // ❌ Avoid this pattern. | |
| func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { | |
| attrs := expensiveOperation() | |
| e.inst.recordSpanInflight(ctx, int64(len(spans)), attrs...) | |
| // Export spans... | |
| } | |
| func (i *instrumentation) recordSpanInflight(ctx context.Context, count int64, attrs ...attribute.KeyValue) { | |
| if i == nil || i.inflight == nil || !i.inflight.Enabled(ctx) { | |
| return | |
| } | |
| i.inflight.Add(ctx, count, metric.WithAttributes(attrs...)) | |
| } | |
| ``` | |
| When observability is enabled, the instrumentation code paths should be optimized to reduce allocation and computation overhead. | |
| ##### Attribute and Option Allocation Management | |
| Pool attribute slices and options with [`sync.Pool`] to minimize allocations in measurement calls with dynamic attributes. | |
| ```go | |
| var ( | |
| attrPool = sync.Pool{ | |
| New: func() any { | |
| // Pre-allocate common capacity | |
| knownCap := 8 // Adjust based on expected usage | |
| s := make([]attribute.KeyValue, 0, knownCap) | |
| // Return a pointer to avoid extra allocation on Put(). | |
| return &s | |
| }, | |
| } | |
| addOptPool = &sync.Pool{ | |
| New: func() any { | |
| const n = 1 // WithAttributeSet | |
| o := make([]metric.AddOption, 0, n) | |
| // Return a pointer to avoid extra allocation on Put(). | |
| return &o | |
| }, | |
| } | |
| ) | |
| func (i *instrumentation) record(ctx context.Context, value int64, baseAttrs ...attribute.KeyValue) { | |
| if !i.counter.Enabled(ctx) { | |
| return | |
| } | |
| attrs := attrPool.Get().(*[]attribute.KeyValue) | |
| defer func() { | |
| clear(*attrs) // Clear references to strings/etc to let GC collect them. | |
| *attrs = (*attrs)[:0] // Reset. | |
| attrPool.Put(attrs) | |
| }() | |
| *attrs = append(*attrs, baseAttrs...) | |
| // Add any dynamic attributes. | |
| *attrs = append(*attrs, semconv.OTelComponentName("exporter-1")) | |
| addOpt := addOptPool.Get().(*[]metric.AddOption) | |
| defer func() { | |
| clear(*addOpt) | |
| *addOpt = (*addOpt)[:0] | |
| addOptPool.Put(addOpt) | |
| }() | |
| set := attribute.NewSet(*attrs...) | |
| *addOpt = append(*addOpt, metric.WithAttributeSet(set)) | |
| i.counter.Add(ctx, value, *addOpt...) | |
| } | |
| ``` | |
| Pools are most effective when there are many pooled objects of the same sufficiently large size, and the objects are repeatedly used. | |
| This amortizes the cost of allocation and synchronization. | |
| Ideally, the pools should be scoped to be used as widely as possible within the component to maximize this efficiency while still ensuring correctness. | |
| [`sync.Pool`]: https://pkg.go.dev/sync#Pool | |
| ##### Cache common attribute sets for repeated measurements | |
| If a static set of attributes are used for measurements and they are known at compile time, pre-compute and cache these attributes. | |
| ```go | |
| type spanLiveSetKey struct { | |
| sampled bool | |
| } | |
| var spanLiveSetCache = map[spanLiveSetKey]attribute.Set{ | |
| {true}: attribute.NewSet( | |
| otelconv.SDKSpanLive{}.AttrSpanSamplingResult( | |
| otelconv.SpanSamplingResultRecordAndSample, | |
| ), | |
| ), | |
| {false}: attribute.NewSet( | |
| otelconv.SDKSpanLive{}.AttrSpanSamplingResult( | |
| otelconv.SpanSamplingResultRecordOnly, | |
| ), | |
| ), | |
| } | |
| func spanLiveSet(sampled bool) attribute.Set { | |
| key := spanLiveSetKey{sampled: sampled} | |
| return spanLiveSetCache[key] | |
| } | |
| ``` | |
| ##### Benchmarking | |
| Always provide benchmarks when introducing or refactoring instrumentation. | |
| Demonstrate the impact (allocs/op, B/op, ns/op) in enabled/disabled scenarios: | |
| ```go | |
| func BenchmarkExportSpans(b *testing.B) { | |
| scenarios := []struct { | |
| name string | |
| obsEnabled bool | |
| }{ | |
| {"ObsDisabled", false}, | |
| {"ObsEnabled", true}, | |
| } | |
| for _, scenario := range scenarios { | |
| b.Run(scenario.name, func(b *testing.B) { | |
| b.Setenv( | |
| "OTEL_GO_X_OBSERVABILITY", | |
| strconv.FormatBool(scenario.obsEnabled), | |
| ) | |
| exporter := NewExporter() | |
| spans := generateTestSpans(100) | |
| b.ResetTimer() | |
| b.ReportAllocs() | |
| for i := 0; i < b.N; i++ { | |
| _ = exporter.ExportSpans(context.Background(), spans) | |
| } | |
| }) | |
| } | |
| } | |
| ``` | |
| #### Error Handling and Robustness | |
| Errors should be reported back to the caller if possible, and partial failures should be handled as gracefully as possible. | |
| ```go | |
| func newInstrumentation() (*instrumentation, error) { | |
| if !x.Observability.Enabled() { | |
| return nil, nil | |
| } | |
| m := otel.GetMeterProvider().Meter(/* initialize meter */) | |
| counter, err := otelconv.NewSDKComponentCounter(m) | |
| // Use the partially initialized counter if available. | |
| i := &instrumentation{counter: counter} | |
| // Return any error to the caller. | |
| return i, err | |
| } | |
| ``` | |
| ```go | |
| // ❌ Avoid this pattern. | |
| func newInstrumentation() *instrumentation { | |
| if !x.Observability.Enabled() { | |
| return nil, nil | |
| } | |
| m := otel.GetMeterProvider().Meter(/* initialize meter */) | |
| counter, err := otelconv.NewSDKComponentCounter(m) | |
| if err != nil { | |
| // ❌ Do not dump the error to the OTel Handler. Return it to the | |
| // caller. | |
| otel.Handle(err) | |
| // ❌ Do not return nil if we can still use the partially initialized | |
| // counter. | |
| return nil | |
| } | |
| return &instrumentation{counter: counter} | |
| } | |
| ``` | |
| If the instrumented component cannot report the error to the user, let it report the error to `otel.Handle`. | |
| #### Context Propagation | |
| Ensure observability measurements receive the correct context, especially for trace exemplars and distributed context: | |
| ```go | |
| func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { | |
| // Use the provided context for observability measurements | |
| if e.inst.Enabled(ctx) { | |
| e.inst.recordSpanExportStarted(ctx, len(spans)) | |
| } | |
| err := e.doExport(ctx, spans) | |
| if e.inst.Enabled(ctx) { | |
| if err != nil { | |
| e.inst.recordSpanExportFailed(ctx, len(spans), err) | |
| } else { | |
| e.inst.recordSpanExportSucceeded(ctx, len(spans)) | |
| } | |
| } | |
| return err | |
| } | |
| ``` | |
| ```go | |
| // ❌ Avoid this pattern. | |
| func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { | |
| // ❌ Do not break the context propagation. | |
| e.inst.recordSpanExportStarted(context.Background(), len(spans)) | |
| err := e.doExport(ctx, spans) | |
| /* ... */ | |
| return err | |
| } | |
| ``` | |
| #### Semantic Conventions Compliance | |
| All observability metrics should follow the [OpenTelemetry Semantic Conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/otel/sdk-metrics.md). | |
| Use the metric semantic conventions convenience package [otelconv](./semconv/v1.43.0/otelconv/metric.go). | |
| ##### Component Identification | |
| Component names and types should follow [semantic convention](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/registry/attributes/otel.md#otel-component-attributes). | |
| If a component is not a well-known type specified in the semantic conventions, use the package path scope type as a stable identifier. | |
| ```go | |
| componentType := "go.opentelemetry.io/otel/sdk/trace.Span" | |
| ``` | |
| ```go | |
| // ❌ Do not do this. | |
| componentType := "trace-span" | |
| ``` | |
| The component name should be a stable unique identifier for the specific instance of the component. | |
| Use a global counter to ensure uniqueness if necessary. | |
| ```go | |
| // Unique 0-based ID counter for component instances. | |
| var componentIDCounter atomic.Int64 | |
| // nextID returns the next unique ID for a component. | |
| func nextID() int64 { | |
| return componentIDCounter.Add(1) - 1 | |
| } | |
| // componentName returns a unique name for the component instance. | |
| func componentName() attribute.KeyValue { | |
| id := nextID() | |
| name := fmt.Sprintf("%s/%d", componentType, id) | |
| return semconv.OTelComponentName(name) | |
| } | |
| ``` | |
| The component ID will need to be resettable for deterministic testing. | |
| If tests are in a different package than the component being tested (i.e. a `<component package>_test` package name), use a generated `counter` internal package to manage the counter. | |
| See [stdouttrace exporter example](./exporters/stdout/stdouttrace/internal/gen.go) for reference. | |
| #### Testing | |
| Use deterministic testing with isolated state: | |
| ```go | |
| func TestObservability(t *testing.T) { | |
| // Restore state after test to ensure this does not affect other tests. | |
| prev := otel.GetMeterProvider() | |
| t.Cleanup(func() { otel.SetMeterProvider(prev) }) | |
| // Isolate the meter provider for deterministic testing | |
| reader := metric.NewManualReader() | |
| meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) | |
| otel.SetMeterProvider(meterProvider) | |
| // Use t.Setenv to ensure environment variable is restored after test. | |
| t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") | |
| // Reset component ID counter to ensure deterministic component names. | |
| componentIDCounter.Store(0) | |
| /* ... test code ... */ | |
| } | |
| ``` | |
| Test order should not affect results. | |
| Ensure that any global state (e.g. component ID counters) is reset between tests. | |
| ### Experimental Features | |
| To support the development of new features in the specification, we use the following patterns to implement in-development features without adding new public artifacts in stable modules. | |
| #### Experimental behavior with no API artifacts | |
| Features that change behavior without changing the API (e.g., exemplar collection, auto-generation of identifiers) are implemented behind a feature gate. | |
| The implementation resides in an `/internal/x` package and is activated through environment variables with the `OTEL_GO_X_` prefix (e.g., `OTEL_GO_X_OBSERVABILITY`). | |
| The feature must be documented in a `README.md` file in the `/internal/x` package. | |
| #### Experimental methods on SDK-only interfaces | |
| Features that require new methods on SDK interfaces are defined as a new interface in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`). | |
| The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces. | |
| The SDK must not depend on the experimental module. | |
| #### Experimental structs, functions, or interfaces | |
| Features that don't need any changes to the existing stable package are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`). | |
| #### Experimental signals and components | |
| New telemetry signals (e.g., Logs before stabilization) and components (e.g. bridges) are hosted in new, unstable modules (e.g., `go.opentelemetry.io/otel/log` before 1.0.0). | |
| The package should have the final name it will use once stabilized (i.e. not `/x`), and is released at a v0.x.y version to indicate it is not stable. | |
| Most new components are hosted in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib). | |
| #### Experimental options for API or SDK functions | |
| Experimental Options functions are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`). | |
| The return type of the Option function must embed the option's type (e.g. `metric.InstrumentOption`), and have an `Experimental()` method to prevent the API from panicking when the option is used. | |
| The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces. | |
| The SDK must not depend on the experimental module. | |
| For example: | |
| ```go | |
| type myOption struct { | |
| // Embed the stable option type. | |
| metric.InstrumentOption | |
| value string | |
| } | |
| // Experimental prevents the API from panicking when the option is used. | |
| func (o myOption) Experimental() {} | |
| // The SDK can use type assertions to use this function. | |
| func (o myOption) Value() string { return o.value } | |
| func WithMyOption(value string) metric.InstrumentOption { | |
| return myOption{value: value} | |
| } | |
| ``` | |
| #### Not Supported | |
| The following kinds of experimental features are **not currently supported** on stable interfaces: | |
| - Experimental methods on API interfaces | |
| - Experimental fields for API or SDK exported structs | |
| In some cases forks or long-lived branches may be used for prototyping these features. | |
| ## Approvers and Maintainers | |
| ### Maintainers | |
| - [Damien Mathieu](https://github.com/dmathieu), Elastic ([GPG](https://keys.openpgp.org/search?q=5A126B972A81A6CE443E5E1B408B8E44F0873832)) | |
| - [David Ashpole](https://github.com/dashpole), Google ([GPG](https://keys.openpgp.org/search?q=C0D1BDDCAAEAE573673085F176327DA4D864DC70)) | |
| - [Robert Pająk](https://github.com/pellared), Splunk ([GPG](https://keys.openpgp.org/search?q=CDAD3A60476A3DE599AA5092E5F7C35A4DBE90C2)) | |
| - [Sam Xie](https://github.com/XSAM), Splunk ([GPG](https://keys.openpgp.org/search?q=AEA033782371ABB18EE39188B8044925D6FEEBEA)) | |
| - [Tyler Yahn](https://github.com/MrAlias), Splunk ([GPG](https://keys.openpgp.org/search?q=0x46B0F3E1A8B1BA5A)) | |
| For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). | |
| ### Approvers | |
| - [Puneet Singh](https://github.com/ps-mir), Independent | |
| For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver). | |
| ### Triagers | |
| - [Marc Schäfer](https://github.com/marcschaeferger), T&A SYSTEME GmbH | |
| For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager). | |
| ### Emeritus | |
| - [Aaron Clawson](https://github.com/MadVikingGod) | |
| - [Alex Kats](https://github.com/akats7) | |
| - [Anthony Mirabella](https://github.com/Aneurysm9) | |
| - [Cheng-Zhen Yang](https://github.com/scorpionknifes) | |
| - [Chester Cheung](https://github.com/hanyuancheung) | |
| - [Evan Torrie](https://github.com/evantorrie) | |
| - [Flc゛](https://github.com/flc1125) | |
| - [Gustavo Silva Paiva](https://github.com/paivagustavo) | |
| - [Josh MacDonald](https://github.com/jmacd) | |
| - [Liz Fong-Jones](https://github.com/lizthegrey) | |
| For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager). | |
| ### Become an Approver or a Maintainer | |
| See the [community membership document in OpenTelemetry community | |
| repo](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md). | |
| [Approver]: #approvers | |
| [Maintainer]: #maintainers | |
| [gotmpl]: https://pkg.go.dev/go.opentelemetry.io/build-tools/gotmpl | |
| [#4404]: https://github.com/open-telemetry/opentelemetry-go/pull/4404 |