diff --git a/components/creating-components/index.html b/components/creating-components/index.html index 5788ccb66e951..dcda49f96ef12 100644 --- a/components/creating-components/index.html +++ b/components/creating-components/index.html @@ -42,13 +42,13 @@ RegisterCollector(c prometheus.Collector) }
In the example above, every user of the telemetry
component would have to import github.com/prometheus/client_golang/prometheus
no matter which implementation they use.
In general, be mindful of using external types in the public interface of your component. For example, it would make sense to use Docker types in a docker
component, but not in a container
component.
The impl
folder is where the component implementation is written. The details of component implementation are up to the developer. The only requirements are that the package name follows the pattern <COMPONENT_NAME>impl
and that there is a public instantiation function called NewComponent
.
package compressionimpl
+internally by each component (more on this [here TODO]()). --> The impl folders¶
The impl
folder is where the component implementation is written. The details of component implementation are up to the developer. The only requirements are that the package name follows the pattern <COMPONENT_NAME>impl
for the regular implementation or <IMPL_NAME>impl
for the alternative implementation, and that there is a public instantiation function called NewComponent
.
package zstdimpl
// NewComponent returns a new ZSTD implementation for the compression component
func NewComponent(reqs Requires) Provides {
....
}
-
To require input arguments to the NewComponent
instantiation function, use a special struct named Requires
. The instantiation function returns a special stuct named Provides
. This internal nomenclature is used to handle the different component dependencies using Fx groups.
In this example, the compression component must access the configuration component and the log component. To express this, define a Requires
struct with two fields. The name of the fields is irrelevant, but the type must be the concrete type of interface that you require.
package compressionimpl
+
To require input arguments to the NewComponent
instantiation function, use a special struct named Requires
. The instantiation function returns a special stuct named Provides
. This internal nomenclature is used to handle the different component dependencies using Fx groups.
In this example, the compression component must access the configuration component and the log component. To express this, define a Requires
struct with two fields. The name of the fields is irrelevant, but the type must be the concrete type of interface that you require.
package zstdimpl
import (
"fmt"
@@ -65,7 +65,7 @@
Conf config.Component
Log log.Component
}
-
Using other components
If you want to use another component within your own, add it to the Requires
struct, and Fx
will give it to you at initialization. Be careful of circular dependencies.
For the output of the component, populate the Provides
struct with the return values.
package compressionimpl
+
Using other components
If you want to use another component within your own, add it to the Requires
struct, and Fx
will give it to you at initialization. Be careful of circular dependencies.
For the output of the component, populate the Provides
struct with the return values.
package zstdimpl
import (
// Always import the component def folder, so that you can return a 'compression.Component' type.
@@ -79,7 +79,7 @@
type Provides struct {
Comp compression.Component
}
-
All together, the component code looks like the following:
package compressionimpl
+
All together, the component code looks like the following:
package zstdimpl
import (
"fmt"
@@ -229,4 +229,4 @@
└── mock.go
6 directories, 6 files
-
This can seem like a lot for a single compression component, but this design answers the exponentially increasing complexity of the Agent ecosystem. Your component needs to behave correctly with many binaries composed of unique and shared components, outside repositories that want to pull only specific features, and everything in between.
Important
No components know how or where they will be used and MUST, therefore, respect all the rules above. It's a very common pattern for teams to work only on their use cases, thinking their code will not be used anywhere else. But customers want common behavior between all Datadog products (Agent, serverless, Agentless, Helm, Operator, etc.).
A key idea behind the component is to produce shareable and reusable code.
General consideration about designing components¶
Your component must:
- Be thread safe.
- Any public methods should be able to be used as soon as your constructor is called. It's OK if some do nothing or drop data as long as the Agent lifecycle is still in its init phase (see lifecycle section for more | TODO).
- Be clearly documented (see section below).
- Be tested.
Documentation¶
The documentation (both package-level and method-level) should include everything a user of the component needs to know. In particular, the documentation must address any assumptions that might lead to panic if violated by the user.
Detailed documentation of how to avoid bugs in using a component is an indicator of excessive complexity and should be treated as a bug. Simplifying the usage will improve the robustness of the Agent.
Documentation should include:
- Precise information on when each method may be called. Can methods be called concurrently?
-
Precise information about data ownership of passed values and returned values. Users can assume that any mutable value returned by a component will not be modified by the user or the component after it is returned. Similarly, any mutable value passed to a component will not be later modified, whether by the component or the caller. Any deviation from these defaults should be documented.
Note
It can be surprisingly hard to avoid mutating data -- for example, append(..)
surprisingly mutates its first argument. It is also hard to detect these bugs, as they are often intermittent, cause silent data corruption, or introduce rare data races. Where performance is not an issue, prefer to copy mutable input and outputs to avoid any potential bugs.
-
Precise information about goroutines and blocking. Users can assume that methods do not block indefinitely, so blocking methods should be documented as such. Methods that invoke callbacks should be clear about how the callback is invoked, and what it might do. For example, document whether the callback can block, and whether it might be called concurrently with other code.
- Precise information about channels. Is the channel buffered? What happens if the channel is not read from quickly enough, or if reading stops? Can the channel be closed by the sender, and if so, what does that mean?