-
Notifications
You must be signed in to change notification settings - Fork 191
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
experimental: config builder + nette schema #498
Conversation
Codecov Report
@@ Coverage Diff @@
## main #498 +/- ##
=========================================
Coverage 94.73% 94.73%
Complexity 961 961
=========================================
Files 94 94
Lines 2375 2375
=========================================
Hits 2250 2250
Misses 125 125 Continue to review full report at Codecov.
|
src/SDK/ConfigBuilder.php
Outdated
]), | ||
'propagators' => Expect::string('tracecontext,baggage'), | ||
'service' => Expect::structure([ | ||
'name' => Expect::string(), |
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.
we could add all defaults here, and remove from the various classes that look up env vars
Looks good on first sight. However I will add some feeback. One thing thing I found in the repos of other SIGs is, that they often have a Another things is, while creating the sdk bundle, I found it quite difficult to reason about all the config options and possibilities in theory. |
src/SDK/ConfigBuilder.php
Outdated
class ConfigBuilder | ||
{ | ||
//single-value env vars | ||
private array $single = [ |
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.
This array could go into a constant, and maybe even in a Interface
.
I'm definetly guilty of over-egnineering constant sometimes (cough), but the retionale is that there is only one place, where a static value is defined which can be referenced. So when a value needs to be changed, it only needs to be changed in one single place. (It's also a bit more memory efficient than an array property, but that would be micro-optimization as the ony reason.)
I'd also stumble upon the name of the property ($single
). I prefer "speaking variable names" over comments. (Id did not come up with this myself, I read it in a book a while ago)
src/SDK/ConfigBuilder.php
Outdated
//@phan-ignore PhanUndeclaredClassMethod | ||
$schema = Expect::structure([ | ||
'log' => Expect::structure([ | ||
'stream' => Expect::string('php://stdout'), |
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 think we should expect a PSR logger instance here, instead of a stream.
src/SDK/ConfigBuilder.php
Outdated
'attribute_count' => Expect::string()->castTo('int'), | ||
'attribute_value_length' => Expect::string()->castTo('int'), | ||
]), | ||
'attributes' => Expect::array(), |
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 found, one can even define the structure of arreays. So, in this case it could be:
'attributes' => Expect::arrayOf(
Expect::string(),
Expect::string()
),
src/SDK/ConfigBuilder.php
Outdated
'attributes' => Expect::array(), | ||
]), | ||
'trace' => Expect::structure([ | ||
'sampler' => Expect::string('parentbased_always_on'), |
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.
What's your reasoning for having those sampler
+ samplers
etc. entries. or how do you think this will work?
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 found this method easier to reason about, because the config options for different types of sampler (or exporter/processor/etc) are easily found within their respective key in "samplers", where "sampler" is the list of samplers to be used.
So, iterate over "sampler" to work out which to create, and then look up sampler-specific config in samplers[type] as required.
src/SDK/ConfigBuilder.php
Outdated
|
||
/** @phan-file-suppress PhanUndeclaredClassMethod */ | ||
|
||
class ConfigBuilder |
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 think this ConfigBuilder is doing to much. It's a) defining a configuration schema, and b) acting upon user definded values via env vars. I think the providers you outlined here are a cleaner approach to deal with user provided values and allow for single responsibility.
src/SDK/ConfigBuilder.php
Outdated
]), | ||
'exporter' => Expect::string('grpc'), | ||
'exporters' => Expect::structure([ | ||
'zipkin' => Expect::structure([ |
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 would prefer to not have the schemas for the contrib exporters here. This is of course not a direct dependency on the Contrib "package", but a indirect one. It's expecting something which may not be there.
Also this looks like it's always expecting configuration for all possible exporters, no?
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.
Yes, that makes sense. It does raise the question though - I'm starting to think that the two otlp exporters should not be in contrib
but rather in SDK
- this is the opentelementry project, after all.
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.
this is the opentelementry project, after all.
That's true, however from the perspective of the library the OTLP collectors are 3rd party applications, which just happen to live under the same umbrella organization. Moving the grpc exporter into the SDK would also make the corresponding extension a must have dpendency for the SDK again. And since there must be a solution to handle Zipkin & Jaeger and Co. in the contrin directory, it's not much of a hassle to do the same for the otlp exporters.
src/SDK/ConfigBuilder.php
Outdated
]), | ||
]); | ||
$env = $this->env(); | ||
$resourceAttributes = $this->resourceAttributes(); |
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 don't think temporary variables help readability.
src/SDK/ConfigBuilder.php
Outdated
return strpos($key, 'OTEL_') !== false; | ||
}, ARRAY_FILTER_USE_KEY); | ||
$output = []; | ||
foreach ($vars as $key => $val) { |
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 long time ago I read a book on code readebilty, so you can blame the authors for me complaininga about this stuff. :)
However while a lot of things there seam to be nit-picky at first, they make sense, when one thinks about and get used to them. One point is to not use "short variable names". While you and maybe me as well know what's meant with "$val", it might not be obviouss to everyone.
src/SDK/ConfigBuilder.php
Outdated
'stream' => Expect::string('php://stdout'), | ||
'level' => Expect::string('info'), | ||
]), | ||
'propagators' => Expect::string('tracecontext,baggage'), |
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.
Why do you expect a CSV here? I Think it's cleaner to expect an array and normalize values from the env var accordingly.
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 had two different techniques going during development, and originally CSV values were extracted into an array as you've suggested. I went back to the CSV method because arrays did not work with default values (ie, if you define default propagator(s) as array values, they are not removable).
So it looks like with nette/schema, we really need to choose between having it handle default values, or having it just enforce the schema (and responsibility for default values stays in the various factories/constructors). I'm happy for it to just enforce the structure.
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 don't understand. Why should there be default propagators and why should there be defaults for array items?
However I think it's better anyway to have classes or factories handle defaults than maintaining them in two places, including possible bugs.
(There's no plugin mechanism for propagators in the SDK yet anyway)
@tidal - a change of direction. I've created an
|
|
||
echo 'Creating Config From Environment and user config' . PHP_EOL; | ||
$config = (new ConfigBuilder()) | ||
->withExporterConfig(ZipkinConfig::class) |
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.
What's the point of expecting a class, when this has to be added programmatically anyway?
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.
My thinking here is to allow making contrib exporters available without directly referencing contrib
from sdk
, and potentially to make them settable from an env var (OTLP_PHP_EXPORTER_CLASS ?).
Without some sort of discovery mechanism I don't see a way to have something in sdk
that is capable of configuring classes from contrib
. Perhaps a class_exists
for something like Contrib\ExporterBuilder
, and the configurations for these guys actually does live in contrib
, and if that class does not exist then only the SDK SpanExporters are available.
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.
Ok, got you, I just find the constructor a bit clunky in that case. But I think you commented something regarding this below.
->withExporterConfig(ZipkinConfig::class) | ||
->withExporterConfig(NewRelicConfig::class) | ||
->withUserConfig([ | ||
'span.processor.batch.max_queue_size' => 333, |
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.
What's the point of this dot-notation? This has to be resolved on every request?
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.
that's a "todo" - it should require a proper nested config as might be created from yaml/json/whatever.
->withUserConfig([ | ||
'span.processor.batch.max_queue_size' => 333, | ||
'resource.limits.attribute_value_length' => 444, | ||
'exporter.new_relic.license_key' => 'secret', |
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.
So I can only have one newrelic exporter?
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.
It never occurred to me that somebody would want to have multiple instances of the same exporter. In fact, the Batch and Simple span processors only allow for 0..1 exporter. MultiSpanProcessor does look like it allows any number of span processors, each of which could have their own exporter though - so I guess you can!
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.
It never occurred to me that somebody would want to have multiple instances of the same exporter.
I guess you never had to develop packages/libraries to be used by customers/clients. They sometimes can come up with let's say kind of "exotic" requests or requierements. That's why I'm used to at least consider "exotic" requirements, to be prepared. ;)
putenv('OTEL_ATTRIBUTE_COUNT_LIMIT=111'); | ||
putenv('OTEL_PROPAGATORS=tracecontext'); | ||
|
||
echo 'Creating Config From Environment and user config' . PHP_EOL; |
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.
What is "user config"? are env vars not provided by users?
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, they are. I could merge them all together, but haven't because it requires sdk->contrib references - at least, the way I'm thinking of it does. As mentioned in an earlier comment, this might be resolved by either having that logic sit in contrib
, or even removing env vars from feature and keeping that knowledge where it is now, in the various constructors.
public array $service; | ||
public ResourceConfig $resource; | ||
|
||
public function __construct(array $userConfig, array $environmentConfig) |
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.
What's the point of having two arguments, istead of an array of configurations?
public function build(): object | ||
{ | ||
$config = new Config($this->userConfig, $this->environmentConfig); | ||
foreach ($this->buildExporters() as $name => $exporterConfig) { |
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.
What#s the point of passing the whole config tree into dedicated configs? What's the point of having them then in the first place, as this is hard coupling them with the global config schema.
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 think I see an improvement here. I think that the config builder should creates all of the different sub-configs and assemble them, passing to each only the config that they need.
|
||
class Config implements ConfigInterface, ExporterConfigInterface | ||
{ | ||
public ?string $endpoint; |
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.
This will create a runtime error without defaulting to null.
$this->attributeValueCount = (int) ($userConfig['resource.limits.attribute_value_length'] ?? $environmentConfig['OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'] ?? 128); | ||
} | ||
|
||
private function intOrNull($value): ?int |
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 this a question?
{ | ||
public function __construct(array $userConfig, array $environmentConfig) | ||
{ | ||
} |
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.
???
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.
It's because the constructor is defined in an interface...I will change it.
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.
@brettmc
I know, this was just a lazy comment from me. :)
src/SDK/ConfigBuilder.php
Outdated
'stream' => Expect::string('php://stdout'), | ||
'level' => Expect::string('info'), | ||
]), | ||
'propagators' => Expect::string('tracecontext,baggage'), |
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 don't understand. Why should there be default propagators and why should there be defaults for array items?
However I think it's better anyway to have classes or factories handle defaults than maintaining them in two places, including possible bugs.
(There's no plugin mechanism for propagators in the SDK yet anyway)
"Nos têtes sont rondes pour que nos pensées puissent changer de direction." (Francis Picabia) However your new approach looks a bit rough ,so I can only provide some "rough" comments, I'm afraid. And it looks like it's only tracing configuration in a way that metrics and logger can't be added. In general here is no need for the for league/config or nette/schema to be one big ball of mud, they can be
Not like it's implemented at the moment. This just adds an additional layer of indirection and possibly other dependency problems.
Maybe, but they seems to have dependencies on the complete config tree. |
Discussed in SIG, I will close this and put the related issue on hold. open-telemetry/opentelemetry-specification#2207 may help a lot here, by defining an SDK configuration schema. |
using nette/schema to generate a config from environment + user-supplied variables
fromConfig
(more to do)