%%%------------------------------------------------------------------------ %% Copyright 2021, OpenTelemetry Authors %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% Merges environment variable configuration values with application %% configuration. The OS environment variables take precedence over the %% application environment. %% @private %%%------------------------------------------------------------------------- -module(otel_configuration). -export([merge_with_os/1, merge_list_with_environment/3, report_cb/1]). -define(BATCH_PROCESSOR_DEFAULTS, #{scheduled_delay_ms => 5000, exporting_timeout_ms => 30000, max_queue_size => 2048, exporter => {opentelemetry_exporter, #{}}}). -define(SIMPLE_PROCESSOR_DEFAULTS, #{exporting_timeout_ms => 30000, exporter => {opentelemetry_exporter, #{}}}). %% required configuration %% using a map instead of a record because there can be more values -type t() :: #{sdk_disabled := boolean(), log_level := atom(), register_loaded_applications := boolean() | undefined, create_application_tracers := boolean() | undefined, id_generator := module(), deny_list := [atom()], resource_detectors := [module()], resource_detector_timeout := integer(), bsp_scheduled_delay_ms := integer() | undefined, bsp_exporting_timeout_ms := integer() | undefined, bsp_max_queue_size := integer() | undefined, ssp_exporting_timeout_ms := integer() | undefined, text_map_propagators := [atom()], traces_exporter := {atom(), term()} | none | undefined, metrics_exporter := {atom(), term()} | none | undefined, views := list(), %% TODO: type should be `[otel_meter_server:view_config]' %% when Metrics are moved out of the experimental app readers := [#{id := atom(), module => module(), config => map()}], exemplars_enabled := boolean(), exemplar_filter := always_on | always_off | trace_based, metric_producers := [{module(), term()}], processors := list(), sampler := {atom(), term()}, sweeper := #{interval => integer() | infinity, strategy => atom() | fun(), span_ttl => integer() | infinity, storage_size => integer() | infinity}, attribute_count_limit := integer(), attribute_value_length_limit := integer() | infinity, event_count_limit := integer(), link_count_limit := integer(), attribute_per_event_limit := integer(), attribute_per_link_limit := integer()}. -export_type([t/0]). -include_lib("kernel/include/logger.hrl"). -include_lib("opentelemetry_api/include/gradualizer.hrl"). -spec new() -> t(). new() -> ?assert_type(#{sdk_disabled => false, log_level => info, register_loaded_applications => undefined, create_application_tracers => undefined, id_generator => otel_id_generator, deny_list => [], resource_detectors => [otel_resource_env_var, otel_resource_app_env], resource_detector_timeout => 5000, bsp_scheduled_delay_ms => undefined, bsp_exporting_timeout_ms => undefined, bsp_max_queue_size => undefined, ssp_exporting_timeout_ms => undefined, text_map_propagators => [trace_context, baggage], traces_exporter => {opentelemetry_exporter, #{}}, metrics_exporter => {opentelemetry_exporter, #{}}, views => [], readers => [], exemplars_enabled => false, exemplar_filter => trace_based, metric_producers => [], processors => [{otel_batch_processor, ?BATCH_PROCESSOR_DEFAULTS}], sampler => {parent_based, #{root => always_on}}, sweeper => #{interval => timer:minutes(10), strategy => drop, span_ttl => timer:minutes(30), storage_size => infinity}, attribute_count_limit => 128, attribute_value_length_limit => infinity, event_count_limit => 128, link_count_limit => 128, attribute_per_event_limit => 128, attribute_per_link_limit => 128}, t()). -spec merge_with_os(list()) -> t(). merge_with_os(AppEnv) -> ConfigMap = new(), lists:foldl(fun(F, Acc) -> F(AppEnv, Acc) end, ConfigMap, [fun span_limits/2, fun general/2, fun sampler/2, fun processors/2, fun sweeper/2]). -spec span_limits(list(), t()) -> t(). span_limits(AppEnv, ConfigMap) -> merge_list_with_environment(config_mappings(span_limits), AppEnv, ConfigMap). -spec general(list(), t()) -> t(). general(AppEnv, ConfigMap) -> Config = merge_list_with_environment(config_mappings(general_sdk), AppEnv, ConfigMap), %% merge the old `register_loaded_applications' with the new config key %% `create_application_tracers' that has replaced it Config1 = maps:update_with(create_application_tracers, fun(undefined) -> %% `create_application_tracers' isn't set so update %% with the `register_loaded_applications' value %% or `true' if it too isn't set case maps:get(register_loaded_applications, Config) of undefined -> true; Bool -> Bool end; (Bool) -> Bool end, Config), ?assert_type(Config1, t()). -spec sweeper(list(), t()) -> t(). sweeper(AppEnv, ConfigMap=#{sweeper := DefaultSweeperConfig}) -> AppEnvSweeper = proplists:get_value(sweeper, AppEnv, #{}), %% convert sweeper config to a list to utilize the merge_list_with_environment function SweeperConfig = merge_list_with_environment(config_mappings(sweeper), maps:to_list(AppEnvSweeper), DefaultSweeperConfig), ConfigMap#{sweeper => SweeperConfig}. -spec processors(list(), t()) -> t(). processors(AppEnv, ConfigMap) -> SpanProcessors = case transform(span_processor, proplists:get_value(span_processor, AppEnv)) of undefined -> Processors = proplists:get_value(processors, AppEnv, maps:get(processors, ConfigMap)), transform(span_processors, Processors); SpanProcessor -> case proplists:get_value(processors, AppEnv) of undefined -> ok; _ -> ?LOG_INFO("both processors and span_processor set in configuration but " "only one should be used. span_processor will be ignored.") end, [SpanProcessor] end, ProcessorsConfig = lists:map(fun({Name, Opts}) -> Opts1 = merge_processor_config(Name, Opts, ConfigMap, AppEnv), {Name, Opts1} end, SpanProcessors), ConfigMap#{processors := ProcessorsConfig}. %% use the top level app env and os env configuration to set/override processor config values merge_processor_config(otel_batch_processor, Opts, ConfigMap, AppEnv) -> BatchEnvMapping = [{bsp_scheduled_delay_ms, scheduled_delay_ms}, {bsp_exporting_timeout_ms, exporting_timeout_ms}, {bsp_max_queue_size, max_queue_size}, {traces_exporter, exporter}], maps:merge(?BATCH_PROCESSOR_DEFAULTS, merge_processor_config_(BatchEnvMapping, Opts, ConfigMap, AppEnv)); merge_processor_config(otel_simple_processor, Opts, ConfigMap, AppEnv) -> SimpleEnvMapping = [{ssp_exporting_timeout_ms, exporting_timeout_ms}, {traces_exporter, exporter}], maps:merge(?SIMPLE_PROCESSOR_DEFAULTS, merge_processor_config_(SimpleEnvMapping, Opts, ConfigMap, AppEnv)); merge_processor_config(_, Opts, _, _) -> Opts. merge_processor_config_(EnvMapping, Opts, ConfigMap, AppEnv) -> Mappings = config_mappings(general_sdk), lists:foldl(fun({K, V}, Acc) -> case maps:get(K, ConfigMap, undefined) of undefined -> Acc; Value -> %% use default only if the config isn't found in Opts %% but if the value in ConfigMap isn't the default use it IsDefault = is_default(K, AppEnv, Mappings), case (IsDefault andalso not maps:is_key(V, Opts)) orelse not IsDefault of true -> Acc#{V => Value}; false -> Acc end end end, Opts, EnvMapping). %% return true if the user (through application or os environment) configured %% a certain setting is_default(Key, AppEnv, Mappings) -> {OSVarName, Key, _TransformType} = lists:keyfind(Key, 2, Mappings), not lists:keymember(Key, 1, AppEnv) andalso os:getenv(OSVarName) =:= false. %% sampler configuration is unique since it has the _ARG that is a sort of %% sub-configuration of the sampler config, and isn't a list. -spec sampler(list(), t()) -> t(). sampler(AppEnv, ConfigMap) -> OSVar = "OTEL_TRACES_SAMPLER", Key = sampler, Transform = sampler, case os:getenv("OTEL_TRACES_SAMPLER") of false -> case proplists:get_value(Key, AppEnv) of undefined -> ConfigMap; Sampler -> update_config_map(OSVar, Key, Transform, Sampler, ConfigMap) end; OSEnvSampler-> SamplerTuple = {OSEnvSampler, os:getenv("OTEL_TRACES_SAMPLER_ARG")}, update_config_map(OSVar, Key, Transform, SamplerTuple, ConfigMap) end. %% requires `ConfigMap' contains every key and its default %% will replace the defaults with the value from either the OS environment or application %% environment, with the OS environment taking precedence. -spec merge_list_with_environment([{OSVar, Key, Transform}], AppEnv, ConfigMap) -> ConfigMap when OSVar :: string(), Key :: atom(), Transform :: atom(), AppEnv :: [{atom(), term()}], ConfigMap :: map(). merge_list_with_environment(ConfigMappings, AppEnv, ConfigMap) -> lists:foldl(fun({OSVar, Key, Transform}, Acc) -> case os:getenv(OSVar) of false -> %% not in the OS environment so check application environment case lists:keyfind(Key, 1, AppEnv) of false -> %% not in the application env so leave default Acc; {_, Value} -> %% transform even the value from the %% application environment to ensure it %% is of the right type/format update_config_map(OSVar, Key, Transform, Value, Acc) end; OSVal -> update_config_map(OSVar, Key, Transform, OSVal, Acc) end end, ConfigMap, ConfigMappings). update_config_map(OSVar, Key, Transform, Value, ConfigMap) -> try transform(Transform, Value) of TransformedValue -> ConfigMap#{Key := TransformedValue} catch Kind:Reason:StackTrace -> ?LOG_INFO(#{source => transform, kind => Kind, reason => Reason, os_var => OSVar, key => Key, transform => Transform, value => Value, stacktrace => StackTrace}, #{report_cb => fun ?MODULE:report_cb/1}), ConfigMap end. report_cb(#{source := transform, kind := Kind, reason := Reason, os_var := OSVar, key := Key, transform := Transform, value := Value, stacktrace := StackTrace}) -> {"Transforming configuration value failed: os_var=~ts key=~ts transform=~ts value=~ts exception=~ts", [OSVar, Key, Transform, Value, otel_utils:format_exception(Kind, Reason, StackTrace)]}. config_mappings(general_sdk) -> [{"OTEL_SDK_DISABLED", sdk_disabled, boolean}, {"OTEL_LOG_LEVEL", log_level, existing_atom}, %% `register_loaded_applications' is kept for backwards compatibility {"OTEL_REGISTER_LOADED_APPLICATIONS", register_loaded_applications, boolean}, {"OTEL_CREATE_APPLICATION_TRACERS", create_application_tracers, boolean}, {"OTEL_ID_GENERATOR", id_generator, existing_atom}, {"OTEL_DENY_LIST", deny_list, existing_atom_list}, {"OTEL_PROPAGATORS", text_map_propagators, propagators}, {"OTEL_TRACES_EXPORTER", traces_exporter, exporter}, {"OTEL_METRICS_EXPORTER", metrics_exporter, exporter}, {"OTEL_METRIC_VIEWS", views, views}, {"OTEL_METRIC_READERS", readers, readers}, {"OTEL_ERLANG_X_EXEMPLARS_ENABLED", exemplars_enabled, boolean}, {"OTEL_METRICS_EXEMPLAR_FILTER", exemplar_filter, exemplar_filter}, {"OTEL_ERLANG_X_METRIC_PRODUCERS", metric_producers, metric_producers}, {"OTEL_RESOURCE_DETECTORS", resource_detectors, existing_atom_list}, {"OTEL_RESOURCE_DETECTOR_TIMEOUT", resource_detector_timeout, integer}, {"OTEL_BSP_SCHEDULE_DELAY_MILLIS", bsp_scheduled_delay_ms, integer}, {"OTEL_BSP_EXPORT_TIMEOUT_MILLIS", bsp_exporting_timeout_ms, integer}, {"OTEL_BSP_MAX_QUEUE_SIZE", bsp_max_queue_size, integer}, %% the following is not supported yet %% {"OTEL_BSP_MAX_EXPORT_BATCH_SIZE", bsp_max_export_batch_size, integer} {"OTEL_SSP_EXPORT_TIMEOUT_MILLIS", ssp_exporting_timeout_ms, integer} ]; config_mappings(span_limits) -> [{"OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", attribute_count_limit, integer}, {"OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", attribute_value_length_limit, integer_infinity}, {"OTEL_SPAN_EVENT_COUNT_LIMIT", event_count_limit, integer}, {"OTEL_SPAN_LINK_COUNT_LIMIT", link_count_limit, integer}, {"OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", attribute_per_event_limit, integer}, {"OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", attribute_per_link_limit, integer}%% , %% {"OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", attribute_value_length_limit, integer}, %% {"OTEL_ATTRIBUTE_COUNT_LIMIT", attribute_per_link_limit, integer} ]; config_mappings(sweeper) -> [{"OTEL_SPAN_SWEEPER_INTERVAL", interval, integer_infinity}, {"OTEL_SPAN_SWEEPER_STRATEGY", strategy, atom_or_fun}, {"OTEL_SPAN_SWEEPER_SPAN_TTL", span_ttl, integer_infinity}, {"OTEL_SPAN_SWEEPER_STORAGE_SIZE", storage_size, integer_infinity} ]. transform(_, undefined) -> undefined; transform(existing_atom_list, [A | _]=List) when is_atom(A) -> List; transform(existing_atom_list, String) when is_list(String) -> List = string:split(String, ",", all), lists:filtermap(fun(A) -> try transform(existing_atom, string:trim(A)) of Value -> {true, Value} catch _:_ -> false end end, List); transform(exporter, Exporter) when Exporter =:= "otlp" ; Exporter =:= otlp -> {opentelemetry_exporter, #{}}; transform(exporter, Exporter) when Exporter =:= "jaeger" ; Exporter =:= jaeger -> ?LOG_WARNING("configuring jaeger exporter through OTEL_TRACES_EXPORTER is not yet supported ", []), none; transform(exporter, Exporter) when Exporter =:= "zipkin" ; Exporter =:= zipkin -> ?LOG_WARNING("configuring zipkin exporter through OTEL_TRACES_EXPORTER is not yet supported ", []), none; transform(exporter, Exporter) when Exporter =:= "none" ; Exporter =:= none -> none; transform(exporter, Value={Term, _}) when is_atom(Term) -> Value; transform(exporter, UnknownExporter) when is_list(UnknownExporter) -> ?LOG_WARNING("unknown exporter ~p. falling back to default otlp", [UnknownExporter]), {opentelemetry_exporter, #{}}; transform(otlp_protocol, Proto) when Proto =:= "grpc"; Proto =:= grpc -> grpc; transform(otlp_protocol, Proto) when Proto =:= "http/protobuf"; Proto =:= "http_protobuf"; Proto =:= http_protobuf -> http_protobuf; transform(otlp_protocol, Proto) when Proto =:= "http/json"; Proto =:= "http_json"; Proto =:= http_json -> ?LOG_WARNING("The http/json transport protocol is not supported by the OTLP exporter, spans will not be exported.", []), undefined; transform(otlp_protocol, Proto) -> ?LOG_WARNING("unknown ~ts transport protocol, spans will not be exported.", [Proto]), undefined; transform(integer_infinity, infinity) -> infinity; transform(integer_infinity, Value) -> transform(integer, Value); transform(integer, Value) when is_integer(Value) -> Value; transform(integer, Value) when is_list(Value) -> list_to_integer(Value); transform(existing_atom, Value) when is_atom(Value) -> Value; transform(existing_atom, Value) when is_list(Value) -> list_to_existing_atom(Value); transform(atom_or_fun, Value) when is_list(Value) -> list_to_existing_atom(Value); transform(atom_or_fun, Value) when is_atom(Value) -> Value; transform(atom_or_fun, Value) when is_function(Value) -> Value; transform(boolean, Value) when is_boolean(Value) -> Value; transform(boolean, "true") -> true; transform(boolean, "false") -> false; transform(url, Value=#{}) -> Value; transform(url, Value) -> uri_string:parse(Value); %% convert sampler string to usable configuration term transform(sampler, {"parentbased_always_on", _}) -> {parent_based, #{root => always_on}}; transform(sampler, {"parentbased_always_off", _}) -> {parent_based, #{root => always_off}}; transform(sampler, {"always_on", _}) -> always_on; transform(sampler, {"always_off", _}) -> always_off; transform(sampler, {"traceidratio", false}) -> {trace_id_ratio_based, 1.0}; transform(sampler, {"traceidratio", Probability}) -> {trace_id_ratio_based, probability_string_to_float(Probability)}; transform(sampler, {"parentbased_traceidratio", false}) -> {parent_based, #{root => {trace_id_ratio_based, 1.0}}}; transform(sampler, {"parentbased_traceidratio", Probability}) -> {parent_based, #{root => {trace_id_ratio_based, probability_string_to_float(Probability)}}}; transform(sampler, Value) -> Value; transform(key_value_list, Value) when is_list(Value) -> case io_lib:printable_unicode_list(Value) of true -> Pairs = string:split(Value, ",", all), lists:filtermap(fun(Pair) -> case string:split(Pair, "=", leading) of [K, V] -> V1 = re:replace(string:trim(V), "^\"|\"$", "", [global, {return, list}]), {true, {string:trim(K), V1}}; _ -> false end end, Pairs); false -> Value end; transform(propagators, [P | _]=Propagators) when is_atom(P) -> lists:filtermap(fun(Propagator) -> case transform(propagator, Propagator) of undefined -> false; Value -> {true, Value} end end, Propagators); transform(propagators, PropagatorsString) when is_list(PropagatorsString) -> Propagators = string:split(PropagatorsString, ",", all), lists:filtermap(fun(Propagator) when is_list(Propagator) -> case transform(propagator, string:trim(Propagator)) of undefined -> false; Value -> {true, Value} end end, Propagators); transform(propagator, Value) when Value =:= "tracecontext" ; Value =:= tracecontext ; Value =:= trace_context -> trace_context; transform(propagator, Value) when Value =:= "baggage" ; Value =:= baggage -> baggage; transform(propagator, Value) when Value =:= "b3multi" ; Value =:= b3multi -> b3multi; transform(propagator, Value) when Value =:= "b3" ; Value =:= b3 -> b3; %% TODO: support jager propagator format %% transform(propagator, "jaeger") -> %% jaeger; transform(propagator, Propagator) when is_atom(Propagator) -> Propagator; transform(propagator, Propagator) -> ?LOG_WARNING("Ignoring unknown propagator ~ts in OS environment variable $OTEL_PROPAGATORS", [Propagator]), undefined; transform(span_processors, Processors) when is_list(Processors) -> [transform(span_processor, Processor) || Processor <- Processors]; transform(span_processors, Unknown) -> ?LOG_WARNING("processors value must be a list, but ~ts given. No span processors will be used", [Unknown]), []; transform(span_processor, batch) -> {otel_batch_processor, ?BATCH_PROCESSOR_DEFAULTS}; transform(span_processor, simple) -> {otel_simple_processor, ?SIMPLE_PROCESSOR_DEFAULTS}; transform(span_processor, SpanProcessor) -> SpanProcessor; transform(readers, Readers) -> Readers; transform(exemplar_filter, ExemplarFilter) when ExemplarFilter =:= "always_on" ; ExemplarFilter =:= always_on -> always_on; transform(exemplar_filter, ExemplarFilter) when ExemplarFilter =:= "always_off" ; ExemplarFilter =:= always_off -> always_off; transform(exemplar_filter, _) -> trace_based; transform(metric_producers, MetricProducers) when is_list(MetricProducers) -> lists:filtermap(fun({ProducerModule, ProducerConfig}) when is_atom(ProducerModule) -> {true, {ProducerModule, ProducerConfig}}; (ProducerModule) when is_atom(ProducerModule) -> {true, {ProducerModule, []}}; (_) -> false end, MetricProducers); transform(views, Views) -> Views. probability_string_to_float(Probability) -> try list_to_float(Probability) of Float -> Float catch error:badarg when Probability =:= "1" -> 1.0; error:badarg when Probability =:= "0" -> 0.0; error:badarg -> ?LOG_WARNING("Unable to convert $OTEL_TRACE_SAMPLER_ARG string value ~ts to float, using 1.0"), 1.0 end.