1414# See the License for the specific language governing permissions and
1515# limitations under the License.
1616
17+ from __future__ import annotations
18+
1719import logging
20+ from dataclasses import dataclass
1821
1922from opentelemetry import trace
2023
2730
2831logger = logging .getLogger (__name__ )
2932
30- _LOG_LEVELS_MAP = {
33+ _LOG_LEVELS_MAP : dict [ str , int ] = {
3134 "trace" : 5 ,
3235 "debug" : logging .DEBUG ,
3336 "info" : logging .INFO ,
3841}
3942
4043DEFAULT_SAMPLING_RATE = 1.0
44+ DEFAULT_LOGGING_LEVEL = "info"
45+
46+ LOGGING_LEVEL_CONFIG_KEY = "logging_level"
47+ SAMPLING_RATE_CONFIG_KEY = "sampling_rate"
48+
49+
50+ @dataclass
51+ class ConfigItem :
52+ value : str
53+
54+
55+ @dataclass
56+ class ConfigUpdate :
57+ error_message : str = ""
58+
59+
60+ # TODO: this should grow into a proper configuration store initialized from env vars and so on
61+ @dataclass
62+ class Config :
63+ sampling_rate = ConfigItem (value = str (DEFAULT_SAMPLING_RATE ))
64+ logging_level = ConfigItem (value = DEFAULT_LOGGING_LEVEL )
65+
66+ def to_dict (self ):
67+ return {LOGGING_LEVEL_CONFIG_KEY : self .logging_level .value , SAMPLING_RATE_CONFIG_KEY : self .sampling_rate .value }
68+
4169
70+ _config = Config ()
4271
43- def _handle_logging_level (config ) -> str :
44- error_message = ""
72+
73+ def _handle_logging_level (remote_config ) -> ConfigUpdate :
74+ _config = _get_config ()
4575 # when config option has default value you don't get it so need to handle the default
46- config_logging_level = config .get ("logging_level" )
47- if config_logging_level is not None :
48- logging_level = _LOG_LEVELS_MAP .get (config_logging_level ) # type: ignore[reportArgumentType]
49- else :
50- logging_level = logging .INFO
76+ config_logging_level = remote_config .get (LOGGING_LEVEL_CONFIG_KEY , DEFAULT_LOGGING_LEVEL )
77+ logging_level = _LOG_LEVELS_MAP .get (config_logging_level )
5178
5279 if logging_level is None :
5380 logger .error ("Logging level not handled: %s" , config_logging_level )
@@ -56,11 +83,14 @@ def _handle_logging_level(config) -> str:
5683 # update upstream and distro logging levels
5784 logging .getLogger ("opentelemetry" ).setLevel (logging_level )
5885 logging .getLogger ("elasticotel" ).setLevel (logging_level )
59- return error_message
86+ _config .logging_level = ConfigItem (value = config_logging_level )
87+ error_message = ""
88+ return ConfigUpdate (error_message = error_message )
6089
6190
62- def _handle_sampling_rate (config ) -> str :
63- config_sampling_rate = config .get ("sampling_rate" )
91+ def _handle_sampling_rate (remote_config ) -> ConfigUpdate :
92+ _config = _get_config ()
93+ config_sampling_rate = remote_config .get (SAMPLING_RATE_CONFIG_KEY , str (DEFAULT_SAMPLING_RATE ))
6494 sampling_rate = DEFAULT_SAMPLING_RATE
6595 if config_sampling_rate is not None :
6696 try :
@@ -69,17 +99,17 @@ def _handle_sampling_rate(config) -> str:
6999 raise ValueError ()
70100 except ValueError :
71101 logger .error ("Invalid `sampling_rate` from config `%s`" , config_sampling_rate )
72- return f"Invalid sampling_rate { config_sampling_rate } "
102+ return ConfigUpdate ( error_message = f"Invalid sampling_rate { config_sampling_rate } " )
73103
74104 sampler = getattr (trace .get_tracer_provider (), "sampler" , None )
75105 if sampler is None :
76106 logger .debug ("Cannot get sampler from tracer provider." )
77- return ""
107+ return ConfigUpdate ()
78108
79109 # FIXME: this needs to be updated for the consistent probability samplers
80110 if not isinstance (sampler , ParentBasedTraceIdRatio ):
81111 logger .warning ("Sampler %s is not supported, not applying sampling_rate." , type (sampler ))
82- return ""
112+ return ConfigUpdate ()
83113
84114 # since sampler is parent based we need to update its root sampler
85115 root_sampler = sampler ._root # type: ignore[reportAttributeAccessIssue]
@@ -88,32 +118,55 @@ def _handle_sampling_rate(config) -> str:
88118 root_sampler ._rate = sampling_rate # type: ignore[reportAttributeAccessIssue]
89119 root_sampler ._bound = root_sampler .get_bound_for_rate (root_sampler ._rate ) # type: ignore[reportAttributeAccessIssue]
90120 logger .debug ("Updated sampler rate to %s" , sampling_rate )
91- return ""
121+ _config .sampling_rate = ConfigItem (value = config_sampling_rate )
122+ return ConfigUpdate ()
123+
124+
125+ def _report_full_state (message : opamp_pb2 .ServerToAgent ):
126+ return message .flags & opamp_pb2 .ServerToAgentFlags_ReportFullState
127+
128+
129+ def _get_config ():
130+ global _config
131+ return _config
92132
93133
94134def opamp_handler (agent : OpAMPAgent , client : OpAMPClient , message : opamp_pb2 .ServerToAgent ):
135+ # server wants us to report full state as it cannot recognize us as agent because
136+ # e.g it may have been restarted and lost state.
137+ if _report_full_state (message ):
138+ # here we're not returning explicitly but usually we don't get a remote config when we get the flag set
139+ payload = client ._build_full_state_message ()
140+ agent .send (payload = payload )
141+
95142 # we check config_hash because we need to track last received config and remote_config seems to be always truthy
96143 if not message .remote_config or not message .remote_config .config_hash :
97144 return
98145
146+ _config = _get_config ()
99147 error_messages = []
100- for config_filename , config in messages ._decode_remote_config (message .remote_config ):
148+ for config_filename , remote_config in messages ._decode_remote_config (message .remote_config ):
101149 # we don't have standardized config values so limit to configs coming from our backend
102150 if config_filename == "elastic" :
103- logger .debug ("Config %s: %s" , config_filename , config )
104- error_message = _handle_logging_level (config )
105- if error_message :
106- error_messages .append (error_message )
151+ logger .debug ("Config %s: %s" , config_filename , remote_config )
152+ config_update = _handle_logging_level (remote_config )
153+ if config_update . error_message :
154+ error_messages .append (config_update . error_message )
107155
108- error_message = _handle_sampling_rate (config )
109- if error_message :
110- error_messages .append (error_message )
156+ config_update = _handle_sampling_rate (remote_config )
157+ if config_update . error_message :
158+ error_messages .append (config_update . error_message )
111159
112160 error_message = "\n " .join (error_messages )
113161 status = opamp_pb2 .RemoteConfigStatuses_FAILED if error_message else opamp_pb2 .RemoteConfigStatuses_APPLIED
114162 updated_remote_config = client ._update_remote_config_status (
115163 remote_config_hash = message .remote_config .config_hash , status = status , error_message = error_message
116164 )
165+
166+ # update the cached effective config with what we updated
167+ effective_config = {"elastic" : _config .to_dict ()}
168+ client ._update_effective_config (effective_config )
169+
117170 # if we changed the config send an ack to the server so we don't receive the same config at every heartbeat response
118171 if updated_remote_config is not None :
119172 payload = client ._build_remote_config_status_response_message (updated_remote_config )
0 commit comments