-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
#1121 - configure metric/trace otlp connections #1202
Conversation
This allows metrics and traces to be sent to different endpoints. Changes include: * Export ConnectionConfiguration structure * Add NewConnectionConfig to easily create configurations * Change Exporter interface to take in different traces/metrics config * Move connection logic in otlpConnection object * Update tests
example/otel-collector/main.go
Outdated
otlp.WithAddress("localhost:30080"), | ||
otlp.WithGRPCDialOption(grpc.WithBlock()), // useful for testing | ||
) | ||
exp, err := otlp.NewExporter(config, config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NewExporter takes in two arguments: a configuration for the metrics connection and one for the traces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this use functional options instead? That would allow a user to choose to configure only metrics or traces and could also provide a convenience option for using the same connection configuration for both. E.g.:
// only provide a ConnectionConfiguration for metrics, traces would not be exported
exp, err := otlp.NewExporter(otlp.WithMetricsConnection(config))
or
// use the same ConnectionConfiguration for both signal types
exp, err := otlp.NewExporter(otlp.WithConnection(config))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True! But the NewExporter needs to take in a structure of the form:
type ConnConfigurations struct {
metrics config
traces config
}
And then you'd have more functional methods like:
func NewConnConfigurations() ConnConfigurations
func (occ ConnConfigurations) WithConnection(opts ...ExporterOption) ConnConfigurations
func (occ ConnConfigurations) WithMetricsConnection(opts ...ExporterOption) ConnConfigurations
func (occ ConnConfigurations) WithTracesConnection(opts ...ExporterOption) ConnConfigurations
Resulting in client calls like so:
exp, err := otlp.NewExporter(otlp.NewConnConfigurations().
WithTracesConnection(otlp.WithTLSCredentials(creds)))
Is this what you are thinking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Aneurysm9 a problem with the way you suggested is that if someone wants to configure different trace and metrics options, you'd still have a method like WithMetricsAndTracesConfigurations(config, config)
. Or is there something I am missing?
I update the interface a bit, to make it more declarative. I think that this works quite well.
It uses a structure holding configuration for both metrics and traces:
type ConnConfigurations struct {
metrics config
traces config
}
Then adds a declarative interface on top, allowing users to configure common settings and custom settings on the specific signals:
// NewConnections initializes a config struct with default values and applies
// any ExporterOptions provided for both metrics and traces.
func NewConnections(opts ...ExporterOption) ConnConfigurations
// SetMetricOptions updates the connection configuration for the metrics endpoint,
// overriding options set with NewConnections, or setting new metrics specific options.
func (occ ConnConfigurations) SetMetricOptions(opts ...ExporterOption) ConnConfigurations
// SetTraceOptions updates the connection configuration for the traces endpoint,
// overriding options set with NewConnections, or setting new trace specific options.
func (occ ConnConfigurations) SetTraceOptions(opts ...ExporterOption) ConnConfigurations
There's some test cases for this in exporters/otlp/options_test.go
showing the uses.
Looking forward to hearing your feedback!
) | ||
|
||
func (e *Exporter) lastConnectError() error { | ||
errPtr := (*error)(atomic.LoadPointer(&e.lastConnectErrPtr)) | ||
type otlpConnection struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All connection logic is contained in this object. The otlpConnection carries it's own configuration, and is responsible for dialing the collector, calling the newConnectionHandler callback and maintaining the connection alive.
Codecov Report
@@ Coverage Diff @@
## master #1202 +/- ##
======================================
Coverage 76.5% 76.6%
======================================
Files 134 134
Lines 5801 5860 +59
======================================
+ Hits 4442 4492 +50
- Misses 1110 1115 +5
- Partials 249 253 +4
|
e.metricExporter = colmetricpb.NewMetricsServiceClient(cc) | ||
e.mu.Unlock() | ||
|
||
// TODO @sprisca: would a signal on the stop chanel stop both connections? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what the stopCh is being used for. I have not seen any actual write to this channel, which would stop the background connections (maybe I'm missing something?)
Either way, now that there are two connections there should be two channels also, since one connection will miss the stop signal if the other one consumes it first. What do you think?
if !e.connected() { | ||
return nil | ||
if !e.tracesConnection.connected() { | ||
return errDisconnected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why this was returning nil instead of throwing an error?
I can't figure out why the precommit step has failed. The pointer offset alignment test is passing on my computer, but I don't understand why it's failing on the test machine, and I don't have access to debug the build itself. Any idea why this test would be failing with the following error?
Any help is appreciated! |
I haven't looked too deep into the changes yet, but to ensure 64-bit alignment of a struct field make sure that is the first field declared in the struct. |
@stefanprisca Thanks for the work on this PR! The CircleCI tests run in both 64-bit x86_64 and 32-bit i386 modes. On i386, various structures (particularly pointers to memory) are 32-bits long rather than 64-bits long, and this may result in subsequent fields in a struct not being 8-byte aligned. Most desktop machines these days are 64-bit, so you probably won't see this running on your machine at home. We recommend putting any values that need to have atomic operations performed on them at the beginning of the struct, since structs are guaranteed to be 64-bit aligned even on a 32-bit machine. For more than you probably ever wanted to know about Go memory alignment, you may want to read this page. |
@evantorrie Thanks for the clarification! I suspected that this would be the issue, but had no idea how golang treats memory. The article is really useful! Just out of curiosity, it seems a bit strange that the previous Exporter type also didn't have the unsafe pointer as the first field. So why was that 8Byte aligned, and this isn't? |
* update error pointer field so it's 64bit alligned * change to declarative configurations interface
* Set common options at configuration creation * Make SetMetrics/TracesOptions override the common options * add tests for setting common options, and overriding specific metrics/traces options
While I don't see a problem with this change, I also don't have a problem with the way it is now. The Lightstep OTel launcher creates two OTLP exporters, each with their own endpoint configured, then uses the two independent exporters to setup the metrics and tracing SDKs. There is no connection between the two exporters with their different signals and endpoints. What advantages do I get if a single exporter manages both? |
That's a fair question. The issue this was trying to solve is to have us conform to the specification which requires that the exporter be configurable to send different signals to different places. |
@jmacd that's true. But what does this mean for this PR? Should we continue with it, or reconsider? The changes are ready for reviewing if we decide to continue with this design. |
If specific signal connections are not set, then do not try starting a connection with default settings. Connections should not be started for non-existant collectors. Changes: * Exporter condition for starting connection if it has address set * Configuration options change to not set default options * Export DefaultConnectionOptions allowing users to set them * Update tests
|
||
// SetCommonOptions updates the connection configuration for both metrics and traces, | ||
// allowing to override default or custom options for both endpoints at once. | ||
func (occ ConnConfigurations) SetCommonOptions(opts ...ExporterOption) ConnConfigurations { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
e.metadata = metadata.New(e.c.headers) | ||
|
||
// Only create connections for the signals with configured addresses. | ||
if configuration.metrics.collectorAddr != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A connection for a signal is only created if an address has been given for that signal. Otherwise it's considered the client does not want to use that signal, so no connection is needed.
@@ -91,3 +97,88 @@ func TestExporterShutdownNoError(t *testing.T) { | |||
t.Errorf("shutdown errored: expected nil, got %v", err) | |||
} | |||
} | |||
|
|||
func TestExporterOnlyStartsMetricsConnection(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added new tests to ensure that connections are handled as expected.
The previous design required clients to always pass in DefaultOptions. As we don't want that (probably), this adds the defaults automatically. Specific metrics and trace connections are created by separate methods. * make DefaultOptions internal again * Add NewMetricsConnection * Add NewTracesConnection * New*Connection methods will apply default options automatically * Update tests
Changed the options interface again (see PR description).... @MrAlias I'll be working on some examples, but I think that this change needs more discussion as the interface is a bit tricky to get right. Considering this, if you feel like it would be better to work on it from the same timezone, please take it over :) |
Hi, unfortunately I cannot contribute to this change any longer as I'm starting a new job next week and my individual CLA becomes invalid (and will be taking a break from open source for a while). |
Thank you for your efforts and good luck with the new job! We appreciate the work you did on this and I'm sure we can find someone to push it over the finish line. |
I'd like to take it over to push #1085 forward. |
@krnowak sounds good 👍 . I'll leave it up to you if you want to close this PR and open another or build on to this. |
@krnowak is the plan to close this and start over? As a consumer it would be nice to see a more idiomatic solution. I would be happy to lend a hand as well. I had to implement this recently myself. We use https://github.com/sethvargo/go-envconfig so you can ignore the struct tags, but it allows me to initialize our exporter with a one-liner.
Here's what my configuration struct looks like.
|
@seanschade: My work so far is here: https://github.com/krnowak/opentelemetry-go/tree/otlp-conn (see What I have is more or less (writing from memory, so names may not be exactly same): type GRPCConnectionConfig struct {
// unexported fields here
}
type ConnectionOption func(*GRPCConnectionConfig)
func (c GRPCConnectionConfig) Apply(ConnectionOption ...opts) GRPCConnectionConfig { … }
func (c *GRPCConnectionConfig) ApplyInPlace(ConnectionOption ...opts) { … }
func WithCompressor(…) ConnectionOption { … }
// more options
// there will be more implementation of this interface, currently there is only for GRPC
type ConnectionManager interface { … }
func NewGRPCSingleConnectionManager(cfg GRPCConnectionConfig) ConnectionManager { … }
type MultiConnectionConfig struct {
Tracing GRPCConnectionConfig
Metrics GRPCConnectionConfig
}
func NewGRPCMultiConnectionManager(cfg MultiConnectionConfig) ConnectionManager { … }
type config struct {
// unexported fields here
}
type ExporterOption func(*config)
func NewExporter(cm ConnectionManager, ExporterOption ...opts) Exporter { … } so the API above would be used like: cfg := GRPCConnectionConfig{}.Apply(WithCompressor(…), WithFoo(…))
cm := NewGRPCSingleConnectionConfig(cfg)
exporter := NewExporter(cm)
// or
cfg := GRPCMultiConnectionConfig{}
cfg.Tracing.ApplyInPlace(WithFoo(…), WithBar(…))
cfg.Metrics.ApplyInPlace(WithFoo(…), WithStuff(…))
cm := NewGRPCMultiConnectionManager(cfg)
exporter := NewExporter(cm) This is what I have, maybe it will change. This is something to figure out as a part of #536. |
Changes for issue #1121
This allows metrics and traces to be sent to different endpoints.
Changes include:
The idea of the design is to have a configurable connection, which can be used by both metrics and traces.
By moving all the connection logic into a new otlpConnection object, the Exporter can make use of this to configure different connections for traces and metrics. Moreover, by using a newConnectionHandler callback, the Exporter can create specific metric/traces clients from the corresponding otlpConnections. These clients are then used to export the metrics and traces.
For example, creating the metrics connection is done as such:
The handleNewMetricsConnection is the newConnectionHandler for metrics, which will create a colmetricpb.NewMetricsServiceClient grpc client for sending metrics. The second argument is the metrics specific configuration.
This flow is also described in this diagram: https://github.com/stefanprisca/opentelemetry-go/blob/otlp-exporters-1121/exporters/otlp/1121/OtlpExporterDesign-Page-1.png
Moreover, in order to handle different configurations, the same ExporterOptions are used, but the NewExporter/NewUnstartedExporter interface changes to take a new structure containig both metrics and traces configurations:
NewExporter(config ConnConfigurations)
. In order to make it easier to construct these configurations, the ConnConfigurations structure is exported, and a new set of declarative functions introduced: