Skip to content
This repository has been archived by the owner on May 23, 2024. It is now read-only.

Add support for client configuration via env vars #275

Merged

Conversation

jpkrohling
Copy link
Contributor

@jpkrohling jpkrohling commented Mar 27, 2018

Status: I'm opening this PR mostly to get an early feedback on the general direction. I have the feeling that my Java background might be getting in the way, so, I wanted to catch any fundamental problem early on :)

As it is, this PR implements the configuration of the following properties, like from the Java Client:

  • JAEGER_SERVICE_NAME
  • JAEGER_AGENT_HOST
  • JAEGER_AGENT_PORT
  • JAEGER_REPORTER_LOG_SPANS
  • JAEGER_REPORTER_MAX_QUEUE_SIZE
  • JAEGER_REPORTER_FLUSH_INTERVAL
  • JAEGER_SAMPLER_TYPE
  • JAEGER_SAMPLER_PARAM
  • JAEGER_SAMPLER_MANAGER_HOST_PORT
  • JAEGER_TAGS

Additionally, these are the properties that are unique to the Go client:

  • JAEGER_DISABLED
  • JAEGER_RPC_METRICS
  • JAEGER_SAMPLER_MAX_OPERATIONS
  • JAEGER_SAMPLER_REFRESH_INTERVAL

The following properties will not be implemented, as the underlying feature is not available for the Go client:

  • JAEGER_ENDPOINT
  • JAEGER_AUTH_TOKEN
  • JAEGER_USER
  • JAEGER_PASSWORD
  • JAEGER_PROPAGATION

The following property will not be implemented, as the underlying feature behaves differently in the Go client than in the Java client:

  • JAEGER_DISABLE_GLOBAL_TRACER

Closes #206

Signed-off-by: Juraci Paixão Kröhling juraci@kroehling.de

@codecov
Copy link

codecov bot commented Mar 27, 2018

Codecov Report

Merging #275 into master will increase coverage by 0.39%.
The diff coverage is 97.05%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #275      +/-   ##
==========================================
+ Coverage   85.74%   86.14%   +0.39%     
==========================================
  Files          54       55       +1     
  Lines        2856     2952      +96     
==========================================
+ Hits         2449     2543      +94     
- Misses        287      288       +1     
- Partials      120      121       +1
Impacted Files Coverage Δ
transport_udp.go 92.59% <0%> (ø) ⬆️
config/config_env.go 100% <100%> (ø)
config/config.go 92.91% <81.81%> (-1.31%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3516374...2dd39a3. Read the comment docs.

@@ -70,6 +71,77 @@ func TestDefaultSampler(t *testing.T) {
require.Error(t, err)
}

func TestServiceNameFromEnv(t *testing.T) {
os.Setenv("JAEGER_SERVICE_NAME", "my-service")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use local constants for these ENV names?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Java, yes. In Go? I don't know... I have a feeling that creating a constant JAEGER_SERVICE_NAME with the value JAEGER_SERVICE_NAME is not idiomatic

Copy link
Member

@pavolloffay pavolloffay Mar 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant name can be different, go idiomatic. I am asking for it because there is some repetition - code, tests, logs, comments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, typically we try to avoid string constants directly in the code. Having them all in a single place as constants makes it easier to survey and document

config/config.go Outdated
@@ -207,6 +226,55 @@ func (c Configuration) New(
return tracer, closer, nil
}

// FromEnv uses environment variables to set the tracer's Configuration
func (c *Configuration) FromEnv() (*Configuration, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering whether:

func FromEnv() (*Configuration, error) {

would be better/cleaner. Then we don't have to care about overrides. It would also align more with java configuration (static fromEnv).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be, yes. This means that people can't load the env vars into an existing object. Is that OK?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can create configuration from ENV and then adjust behaviour in code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we then have SamplerConfigFromEnv() / ReporterConfigFromEnv() / ...FromEnv() ? Sounds good to me.

config/config.go Outdated
}

// NewFromEnv returns a new tracer configured using environment variables
func (c *Configuration) NewFromEnv(options ...Option) (opentracing.Tracer, io.Closer, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 on this method, also fromEnv should return the error if the service name is not valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one exists because it has a different semantic as the other. The idea of FromEnv() methods is to configure an existing object based on env vars. This one will create a new tracer (similar to the New() method), running FromEnv() before.

This means that it's still possible to use the New(serviceName, ...) after calling FromEnv(), so, FromEnv() should not fail if an env var with the service name is not available.

I'm open to change this behavior, though. What do others say?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I think it's redundant having FromEnv() that returns Config and NewFromEnv that returns a tracer, since the latter can be implemented as FromEnv().New()
  • the existing method called New() is poorly named, imo, it should've been NewTracer()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's redundant having FromEnv() that returns Config and NewFromEnv that returns a tracer, since the latter can be implemented as FromEnv().New()

jaegerconfig.FromEnv().New() would require a change in the signature of New() to remove the service name from it. Or did you mean jaegerconfig.FromEnv().New(string, ...Options)?

It's also not clear if it would be preferrable to have @pavolloffay's approach (jaegerconfig.FromEnv()) or the existing one (&Configuration{}.FromEnv()). I like @pavolloffay's suggestion, but I like consistency more :)

the existing method called New() is poorly named, imo, it should've been NewTracer()

Should I create a NewTracer(), and make the existing New() call it? How would deprecation work here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I wasn't suggesting changing the signature of New(), we should keep it. Go doesn't have an official way of deprecating packages afaik, but the convention is to add the last comment line as Deprecated: explanation

On static func vs. member func, I don't have a strong opinion - preferably we should be consistent with other clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the latter can be implemented as FromEnv().New()

This is the part that I'm not quite following. New(string, ...Option) requires a service name as the first parameter. Using FromEnv(), I assume we'd be getting the service name from the env var, but we would still require the caller to pass a service name.

One solution is to allow the caller to pass an empty value, but that doesn't sound that clean to me.

Should I just create a NewTracer(...Option), without serviceName?

@jpkrohling jpkrohling force-pushed the 206-ConfigureClientViaEnvVars branch from 1e1299d to e85b0c6 Compare March 28, 2018 09:58
func TestServiceNameFromEnv(t *testing.T) {
os.Setenv(envServiceName, "my-service")

_, c, err := FromEnv().New("")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite like that callers are required to pass an empty string if they expect the service name to be specified via env var, but perhaps it's acceptable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as the service name is a member of configuration, we could deprecate New and create newTracer without requiring serviceName.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh you had the same suggestion here #275 (comment)

I like it

@jpkrohling jpkrohling changed the title WIP - Closes #206 - Added support for client configuration via env vars Closes #206 - Added support for client configuration via env vars Mar 28, 2018
@jpkrohling
Copy link
Contributor Author

jpkrohling commented Mar 28, 2018

This is now ready for the next round of reviews, but please don't merge. I'll squash it into a single commit.

cc @yurishkuro, @pavolloffay

edit: just realized I did not add any documentation about this feature to the readme files... So, please do start the review, but keep in mind I'll work on the readme.

config/config.go Outdated
// Configuration configures and creates Jaeger Tracer
type Configuration struct {
Disabled bool `yaml:"disabled"`
// ServiceName specifies the service name to use on the tracer.
// Can be set by exporting an environment variable named JAEGER_SERVICE_NAME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can be provided via environment variable named JAEGER_SERVICE_NAME

config/config.go Outdated
// RPCMetrics can be set by exporting an environment variable named JAEGER_RPC_METRICS
RPCMetrics bool `yaml:"rpc_metrics"`

Tags []opentracing.Tag `yaml:"tags"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • add comment about env var?
  • add blank line after

config/config.go Outdated
}

// FromEnv uses environment variables to set the tracer's Configuration
func FromEnv() *Configuration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - this is making the file huge, I suggest moving to config_env.go (including constants) for a nice encapsulation

config/config.go Outdated
if e := os.Getenv(envServiceName); e != "" {
// We are ignoring any errors, so that we have a single return value
// This pattern repeats itself across all the "FromEnv" functions.
// TODO: should we at least log when something bad happens, instead of just ignoring?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best not to ignore errors. Many env vars are parsed (e.g. as boolean), it's better to fail if we can't parse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means then that FromEnv() has a multiple valued return, making it impossible to do FromEnv().NewTracer(), no ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, but it's better than swallowing errors. FromEnv().NewTracer() is not exactly the goal here. If people really want a one-liner, we could add NewTracerFromEnv()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, just wanted to make sure we were on the same page :)

config/config.go Outdated
}

// SamplerConfigFromEnv creates a new SamplerConfig based on the environment variables
func SamplerConfigFromEnv() *SamplerConfig {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about we keep these private for now? Can always expose later if someone actually needs that

config/config.go Outdated
// spec for this value:
// - comma separated list of key=value
// - value can be specified using the notation ${envVar:defaultValue}, where `envVar`
// is an environment variable and `defaultValue` is the value to use in case the env var is not set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to method comment

config/config.go Outdated
return nil, nil, errors.New("no service name provided")
}

return c.New(c.ServiceName, options...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it would be better to do the reverse - implement New via NewTracer

ts := parseTags(tags)
require.Equal(t, 4, len(ts))

require.Equal(t, "key", ts[0].Key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: require means you cannot continue the test further, should be rarely used, in most cases assert is better.

func TestNewTracerWithoutServiceName(t *testing.T) {
cfg := &Configuration{}
_, _, err := cfg.NewTracer(Metrics(metrics.NullFactory), Logger(log.NullLogger))
require.Error(t, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this test may succeed as false positive. Best to check not only that there was an error, but that the error contained a certain string, e.g. assert.Contains(t, err.Error(), "pattern")

@yurishkuro
Copy link
Member

Closes #206 - Added support for client configuration via env vars #275

Please follow commit comment guidelines from Contributing.md for PR titles.

In particular, the reference to the ticket being addressed must be in the description, not the title, otherwise github won't close the ticket on merge.

@jpkrohling jpkrohling force-pushed the 206-ConfigureClientViaEnvVars branch from 0a861a7 to c844e00 Compare March 28, 2018 15:52
@jpkrohling jpkrohling changed the title Closes #206 - Added support for client configuration via env vars Added support for client configuration via env vars Mar 28, 2018
@jpkrohling jpkrohling force-pushed the 206-ConfigureClientViaEnvVars branch from c844e00 to 7fe69a0 Compare March 28, 2018 16:15
@jpkrohling
Copy link
Contributor Author

I believe all the comments were addressed by this last change.

@yurishkuro yurishkuro changed the title Added support for client configuration via env vars Add support for client configuration via env vars Mar 28, 2018
README.md Outdated
JAEGER_SAMPLER_MAX_OPERATIONS | The maximum number of operations that the sampler will keep track of
JAEGER_SAMPLER_REFRESH_INTERVAL | How often the remotely controlled sampler will poll jaeger-agent for the appropriate sampling strategy
JAEGER_TAGS | A comma separated list of `name = value` tracer level tags, which get added to all reported spans. The value can also refer to an environment variable using the format `${envVarName:default}`, where the `:default` is optional, and identifies a value to be used if the environment variable cannot be found
JAEGER_DISABLED | Whether the tracer is disabled or not
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to clarify: if true, the default opentracing.NoopTracer is used.

config/config.go Outdated
@@ -115,9 +135,23 @@ func (c Configuration) New(
serviceName string,
options ...Option,
) (opentracing.Tracer, io.Closer, error) {
if serviceName == "" {
if serviceName == "" && c.ServiceName == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check is not necessary, since it will be done by NewTracer anyway

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but given that we have ServiceName as part of the Configuration object, this would cover the following scenario:

cfg := &Configuration{ServiceName: "my-service", ...}
_, closer, err := cfg.New("")

Without the right side of the check, the ServiceName from the Configuration is ignored and the error is returned ("no service name provided").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new test for this scenario.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I follow. If serviceName arg is not blank, it will override c.ServiceName, otherwise it wouldn't. In either case it will be followed by a call to c.NewTracer() which will return an error if c.ServiceName is blank.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you call NewTracer(), yes. But New("") might be called directly:

cfg := &Configuration{ServiceName: "my-service", ...}
_, closer, err := cfg.New("")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but New() delegates to NewTracer():

return c.NewTracer(options...)

Copy link
Contributor Author

@jpkrohling jpkrohling Mar 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see now what you mean. You mean to just entirely remove the first conditional. Agree :)

The diff colors confused me:

image

config/config.go Outdated
return c.NewTracer(options...)
}

// NewTracer returns a new tracer based on the current configuration, using the given options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add info about closer function (see comment for New)

@@ -115,9 +135,23 @@ func (c Configuration) New(
serviceName string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add // Deprecated: use NewTracer() function as the last line in the comment

if value, err := strconv.ParseInt(e, 10, 0); err == nil {
rc.QueueSize = int(value)
} else {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a better way would be to mention the field, e.g.

return nil, errors.Wraps(err, "Cannot parse env var %s=%s", envReporterMaxQueueSize, e)

// JAEGER_AGENT_HOST:JAEGER_AGENT_PORT, like: localhost:6831
if e := os.Getenv(envAgentHost); e != "" {
host := e
port := 6831 // TODO: can we somehow consume the default from the transport itself?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is const defaultUDPSpanServerHostPort = "localhost:6831", the port could be refactored into a public const (maybe even moved to constants.go)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The host as well then, for consistency :)

}

// JAEGER_AGENT_HOST:JAEGER_AGENT_PORT, like: localhost:6831
if e := os.Getenv(envAgentHost); e != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: so if only the port is provided via env it won't be read here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a moment, I thought this was in line with what the Java client was doing, but I'm clearly wrong.

// parseTags parses the given string into a collection of Tags.
// Spec for this value:
// - comma separated list of key=value
// - value can be specified using the notation ${envVar:defaultValue}, where `envVar`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${envVar:defaultValue} - do we really need this? I.e. couldn't this be delegated to the shells where the env vars are defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. This was added by @objectiser, IIRC, so, he might have a case for it. If we decide to remove, I'd do it in a separate PR, as we should also remove it from the Java client.

{
envVar: envRPCMetrics,
value: "NOT_A_BOOLEAN",
expectedErr: "strconv.ParseBool",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my comment about wrapping errors - if it's implemented, the tests could be written without dependency on the internal wording of Go errors, we can simply be checking for our own strings like Cannot parse env var JAEGER_RPC_METRICS= NOT_A_BOOLEAN

@jpkrohling
Copy link
Contributor Author

PR updated. I left the ${envVar:defaultValue} out of this, creating #279 to discuss it.

Adds support for configuring the tracer based on environment variables,
similar to what is currently supported by the Java client.

Closes jaegertracing#206

Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>
@jpkrohling jpkrohling force-pushed the 206-ConfigureClientViaEnvVars branch from b255cfa to 2dd39a3 Compare March 30, 2018 11:07
@black-adder black-adder merged commit 6cc524e into jaegertracing:master Apr 3, 2018
@yurishkuro
Copy link
Member

we should change JAEGER_SAMPLER_MANAGER_HOST_PORT to CONFIG_MGR_HOST_PORT, because that endpoint is used more than just for sampling configuration

@robdefeo
Copy link

what release version is the change scheduled for?

@yurishkuro
Copy link
Member

released as 2.13

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants