| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (C) 2022 Jérémie Galarneau <jeremie.galarneau@efficios.com> |
| 4 | # |
| 5 | # SPDX-License-Identifier: GPL-2.0-only |
| 6 | |
| 7 | |
| 8 | from . import lttngctl, logger, environment |
| 9 | import os |
| 10 | from typing import Callable, Optional, Type, Union, Iterator |
| 11 | import shlex |
| 12 | import subprocess |
| 13 | import enum |
| 14 | import xml.etree.ElementTree |
| 15 | |
| 16 | """ |
| 17 | Implementation of the lttngctl interface based on the `lttng` command line client. |
| 18 | """ |
| 19 | |
| 20 | |
| 21 | class Unsupported(lttngctl.ControlException): |
| 22 | def __init__(self, msg): |
| 23 | # type: (str) -> None |
| 24 | super().__init__(msg) |
| 25 | |
| 26 | |
| 27 | class InvalidMI(lttngctl.ControlException): |
| 28 | def __init__(self, msg): |
| 29 | # type: (str) -> None |
| 30 | super().__init__(msg) |
| 31 | |
| 32 | |
| 33 | class ChannelNotFound(lttngctl.ControlException): |
| 34 | def __init__(self, msg): |
| 35 | # type: (str) -> None |
| 36 | super().__init__(msg) |
| 37 | |
| 38 | |
| 39 | def _get_domain_option_name(domain): |
| 40 | # type: (lttngctl.TracingDomain) -> str |
| 41 | return { |
| 42 | lttngctl.TracingDomain.User: "userspace", |
| 43 | lttngctl.TracingDomain.Kernel: "kernel", |
| 44 | lttngctl.TracingDomain.Log4j: "log4j", |
| 45 | lttngctl.TracingDomain.Log4j2: "log4j2", |
| 46 | lttngctl.TracingDomain.Python: "python", |
| 47 | lttngctl.TracingDomain.JUL: "jul", |
| 48 | }[domain] |
| 49 | |
| 50 | |
| 51 | def _get_domain_xml_mi_name(domain): |
| 52 | # type: (lttngctl.TracingDomain) -> str |
| 53 | return { |
| 54 | lttngctl.TracingDomain.User: "UST", |
| 55 | lttngctl.TracingDomain.Kernel: "KERNEL", |
| 56 | lttngctl.TracingDomain.Log4j: "LOG4J", |
| 57 | lttngctl.TracingDomain.Log4j2: "LOG4J2", |
| 58 | lttngctl.TracingDomain.Python: "PYTHON", |
| 59 | lttngctl.TracingDomain.JUL: "JUL", |
| 60 | }[domain] |
| 61 | |
| 62 | |
| 63 | def _get_context_type_name(context): |
| 64 | # type: (lttngctl.ContextType) -> str |
| 65 | if isinstance(context, lttngctl.VgidContextType): |
| 66 | return "vgid" |
| 67 | elif isinstance(context, lttngctl.VuidContextType): |
| 68 | return "vuid" |
| 69 | elif isinstance(context, lttngctl.VpidContextType): |
| 70 | return "vpid" |
| 71 | elif isinstance(context, lttngctl.JavaApplicationContextType): |
| 72 | return "$app.{retriever}:{field}".format( |
| 73 | retriever=context.retriever_name, field=context.field_name |
| 74 | ) |
| 75 | else: |
| 76 | raise Unsupported( |
| 77 | "Context `{context_name}` is not supported by the LTTng client".format( |
| 78 | type(context).__name__ |
| 79 | ) |
| 80 | ) |
| 81 | |
| 82 | |
| 83 | def _get_log_level_argument_name(log_level): |
| 84 | # type: (lttngctl.LogLevel) -> str |
| 85 | if isinstance(log_level, lttngctl.UserLogLevel): |
| 86 | return { |
| 87 | lttngctl.UserLogLevel.EMERGENCY: "EMER", |
| 88 | lttngctl.UserLogLevel.ALERT: "ALERT", |
| 89 | lttngctl.UserLogLevel.CRITICAL: "CRIT", |
| 90 | lttngctl.UserLogLevel.ERROR: "ERR", |
| 91 | lttngctl.UserLogLevel.WARNING: "WARNING", |
| 92 | lttngctl.UserLogLevel.NOTICE: "NOTICE", |
| 93 | lttngctl.UserLogLevel.INFO: "INFO", |
| 94 | lttngctl.UserLogLevel.DEBUG_SYSTEM: "DEBUG_SYSTEM", |
| 95 | lttngctl.UserLogLevel.DEBUG_PROGRAM: "DEBUG_PROGRAM", |
| 96 | lttngctl.UserLogLevel.DEBUG_PROCESS: "DEBUG_PROCESS", |
| 97 | lttngctl.UserLogLevel.DEBUG_MODULE: "DEBUG_MODULE", |
| 98 | lttngctl.UserLogLevel.DEBUG_UNIT: "DEBUG_UNIT", |
| 99 | lttngctl.UserLogLevel.DEBUG_FUNCTION: "DEBUG_FUNCTION", |
| 100 | lttngctl.UserLogLevel.DEBUG_LINE: "DEBUG_LINE", |
| 101 | lttngctl.UserLogLevel.DEBUG: "DEBUG", |
| 102 | }[log_level] |
| 103 | elif isinstance(log_level, lttngctl.JULLogLevel): |
| 104 | return { |
| 105 | lttngctl.JULLogLevel.OFF: "OFF", |
| 106 | lttngctl.JULLogLevel.SEVERE: "SEVERE", |
| 107 | lttngctl.JULLogLevel.WARNING: "WARNING", |
| 108 | lttngctl.JULLogLevel.INFO: "INFO", |
| 109 | lttngctl.JULLogLevel.CONFIG: "CONFIG", |
| 110 | lttngctl.JULLogLevel.FINE: "FINE", |
| 111 | lttngctl.JULLogLevel.FINER: "FINER", |
| 112 | lttngctl.JULLogLevel.FINEST: "FINEST", |
| 113 | lttngctl.JULLogLevel.ALL: "ALL", |
| 114 | }[log_level] |
| 115 | elif isinstance(log_level, lttngctl.Log4jLogLevel): |
| 116 | return { |
| 117 | lttngctl.Log4jLogLevel.OFF: "OFF", |
| 118 | lttngctl.Log4jLogLevel.FATAL: "FATAL", |
| 119 | lttngctl.Log4jLogLevel.ERROR: "ERROR", |
| 120 | lttngctl.Log4jLogLevel.WARN: "WARN", |
| 121 | lttngctl.Log4jLogLevel.INFO: "INFO", |
| 122 | lttngctl.Log4jLogLevel.DEBUG: "DEBUG", |
| 123 | lttngctl.Log4jLogLevel.TRACE: "TRACE", |
| 124 | lttngctl.Log4jLogLevel.ALL: "ALL", |
| 125 | }[log_level] |
| 126 | elif isinstance(log_level, lttngctl.Log4j2LogLevel): |
| 127 | return { |
| 128 | lttngctl.Log4j2LogLevel.OFF: "OFF", |
| 129 | lttngctl.Log4j2LogLevel.FATAL: "FATAL", |
| 130 | lttngctl.Log4j2LogLevel.ERROR: "ERROR", |
| 131 | lttngctl.Log4j2LogLevel.WARN: "WARN", |
| 132 | lttngctl.Log4j2LogLevel.INFO: "INFO", |
| 133 | lttngctl.Log4j2LogLevel.DEBUG: "DEBUG", |
| 134 | lttngctl.Log4j2LogLevel.TRACE: "TRACE", |
| 135 | lttngctl.Log4j2LogLevel.ALL: "ALL", |
| 136 | }[log_level] |
| 137 | elif isinstance(log_level, lttngctl.PythonLogLevel): |
| 138 | return { |
| 139 | lttngctl.PythonLogLevel.CRITICAL: "CRITICAL", |
| 140 | lttngctl.PythonLogLevel.ERROR: "ERROR", |
| 141 | lttngctl.PythonLogLevel.WARNING: "WARNING", |
| 142 | lttngctl.PythonLogLevel.INFO: "INFO", |
| 143 | lttngctl.PythonLogLevel.DEBUG: "DEBUG", |
| 144 | lttngctl.PythonLogLevel.NOTSET: "NOTSET", |
| 145 | }[log_level] |
| 146 | |
| 147 | raise TypeError("Unknown log level type") |
| 148 | |
| 149 | |
| 150 | def _get_log_level_from_mi_log_level_name(mi_log_level_name): |
| 151 | # type: (str) -> lttngctl.LogLevel |
| 152 | return { |
| 153 | "TRACE_EMERG": lttngctl.UserLogLevel.EMERGENCY, |
| 154 | "TRACE_ALERT": lttngctl.UserLogLevel.ALERT, |
| 155 | "TRACE_CRIT": lttngctl.UserLogLevel.CRITICAL, |
| 156 | "TRACE_ERR": lttngctl.UserLogLevel.ERROR, |
| 157 | "TRACE_WARNING": lttngctl.UserLogLevel.WARNING, |
| 158 | "TRACE_NOTICE": lttngctl.UserLogLevel.NOTICE, |
| 159 | "TRACE_INFO": lttngctl.UserLogLevel.INFO, |
| 160 | "TRACE_DEBUG_SYSTEM": lttngctl.UserLogLevel.DEBUG_SYSTEM, |
| 161 | "TRACE_DEBUG_PROGRAM": lttngctl.UserLogLevel.DEBUG_PROGRAM, |
| 162 | "TRACE_DEBUG_PROCESS": lttngctl.UserLogLevel.DEBUG_PROCESS, |
| 163 | "TRACE_DEBUG_MODULE": lttngctl.UserLogLevel.DEBUG_MODULE, |
| 164 | "TRACE_DEBUG_UNIT": lttngctl.UserLogLevel.DEBUG_UNIT, |
| 165 | "TRACE_DEBUG_FUNCTION": lttngctl.UserLogLevel.DEBUG_FUNCTION, |
| 166 | "TRACE_DEBUG_LINE": lttngctl.UserLogLevel.DEBUG_LINE, |
| 167 | "TRACE_DEBUG": lttngctl.UserLogLevel.DEBUG, |
| 168 | "JUL_OFF": lttngctl.JULLogLevel.OFF, |
| 169 | "JUL_SEVERE": lttngctl.JULLogLevel.SEVERE, |
| 170 | "JUL_WARNING": lttngctl.JULLogLevel.WARNING, |
| 171 | "JUL_INFO": lttngctl.JULLogLevel.INFO, |
| 172 | "JUL_CONFIG": lttngctl.JULLogLevel.CONFIG, |
| 173 | "JUL_FINE": lttngctl.JULLogLevel.FINE, |
| 174 | "JUL_FINER": lttngctl.JULLogLevel.FINER, |
| 175 | "JUL_FINEST": lttngctl.JULLogLevel.FINEST, |
| 176 | "JUL_ALL": lttngctl.JULLogLevel.ALL, |
| 177 | "LOG4J_OFF": lttngctl.Log4jLogLevel.OFF, |
| 178 | "LOG4J_FATAL": lttngctl.Log4jLogLevel.FATAL, |
| 179 | "LOG4J_ERROR": lttngctl.Log4jLogLevel.ERROR, |
| 180 | "LOG4J_WARN": lttngctl.Log4jLogLevel.WARN, |
| 181 | "LOG4J_INFO": lttngctl.Log4jLogLevel.INFO, |
| 182 | "LOG4J_DEBUG": lttngctl.Log4jLogLevel.DEBUG, |
| 183 | "LOG4J_TRACE": lttngctl.Log4jLogLevel.TRACE, |
| 184 | "LOG4J_ALL": lttngctl.Log4jLogLevel.ALL, |
| 185 | "LOG4J2_OFF": lttngctl.Log4j2LogLevel.OFF, |
| 186 | "LOG4J2_FATAL": lttngctl.Log4j2LogLevel.FATAL, |
| 187 | "LOG4J2_ERROR": lttngctl.Log4j2LogLevel.ERROR, |
| 188 | "LOG4J2_WARN": lttngctl.Log4j2LogLevel.WARN, |
| 189 | "LOG4J2_INFO": lttngctl.Log4j2LogLevel.INFO, |
| 190 | "LOG4J2_DEBUG": lttngctl.Log4j2LogLevel.DEBUG, |
| 191 | "LOG4J2_TRACE": lttngctl.Log4j2LogLevel.TRACE, |
| 192 | "LOG4J2_ALL": lttngctl.Log4j2LogLevel.ALL, |
| 193 | "PYTHON_CRITICAL": lttngctl.PythonLogLevel.CRITICAL, |
| 194 | "PYTHON_ERROR": lttngctl.PythonLogLevel.ERROR, |
| 195 | "PYTHON_WARNING": lttngctl.PythonLogLevel.WARNING, |
| 196 | "PYTHON_INFO": lttngctl.PythonLogLevel.INFO, |
| 197 | "PYTHON_DEBUG": lttngctl.PythonLogLevel.DEBUG, |
| 198 | "PYTHON_NOTSET": lttngctl.PythonLogLevel.NOTSET, |
| 199 | }[mi_log_level_name] |
| 200 | |
| 201 | |
| 202 | def _get_tracepoint_event_rule_class_from_domain_type(domain_type): |
| 203 | # type: (lttngctl.TracingDomain) -> Type[lttngctl.UserTracepointEventRule] | Type[lttngctl.Log4jTracepointEventRule] | Type[lttngctl.Log4j2TracepointEventRule] | Type[lttngctl.JULTracepointEventRule] | Type[lttngctl.PythonTracepointEventRule] | Type[lttngctl.KernelTracepointEventRule] |
| 204 | return { |
| 205 | lttngctl.TracingDomain.User: lttngctl.UserTracepointEventRule, |
| 206 | lttngctl.TracingDomain.JUL: lttngctl.JULTracepointEventRule, |
| 207 | lttngctl.TracingDomain.Log4j: lttngctl.Log4jTracepointEventRule, |
| 208 | lttngctl.TracingDomain.Log4j2: lttngctl.Log4j2TracepointEventRule, |
| 209 | lttngctl.TracingDomain.Python: lttngctl.PythonTracepointEventRule, |
| 210 | lttngctl.TracingDomain.Kernel: lttngctl.KernelTracepointEventRule, |
| 211 | }[domain_type] |
| 212 | |
| 213 | |
| 214 | class _Channel(lttngctl.Channel): |
| 215 | def __init__( |
| 216 | self, |
| 217 | client, # type: LTTngClient |
| 218 | name, # type: str |
| 219 | domain, # type: lttngctl.TracingDomain |
| 220 | session, # type: _Session |
| 221 | ): |
| 222 | self._client = client # type: LTTngClient |
| 223 | self._name = name # type: str |
| 224 | self._domain = domain # type: lttngctl.TracingDomain |
| 225 | self._session = session # type: _Session |
| 226 | |
| 227 | def add_context(self, context_type): |
| 228 | # type: (lttngctl.ContextType) -> None |
| 229 | domain_option_name = _get_domain_option_name(self.domain) |
| 230 | context_type_name = _get_context_type_name(context_type) |
| 231 | self._client._run_cmd( |
| 232 | "add-context --{domain_option_name} --channel '{channel_name}' --type {context_type_name}".format( |
| 233 | domain_option_name=domain_option_name, |
| 234 | channel_name=self.name, |
| 235 | context_type_name=context_type_name, |
| 236 | ) |
| 237 | ) |
| 238 | |
| 239 | def add_recording_rule(self, rule): |
| 240 | # type: (Type[lttngctl.EventRule]) -> None |
| 241 | client_args = ( |
| 242 | "enable-event --session {session_name} --channel {channel_name}".format( |
| 243 | session_name=self._session.name, channel_name=self.name |
| 244 | ) |
| 245 | ) |
| 246 | if isinstance(rule, lttngctl.TracepointEventRule): |
| 247 | domain_option_name = ( |
| 248 | "userspace" |
| 249 | if isinstance(rule, lttngctl.UserTracepointEventRule) |
| 250 | else "kernel" |
| 251 | ) |
| 252 | client_args = client_args + " --{domain_option_name}".format( |
| 253 | domain_option_name=domain_option_name |
| 254 | ) |
| 255 | |
| 256 | if rule.name_pattern: |
| 257 | client_args = client_args + " " + rule.name_pattern |
| 258 | else: |
| 259 | client_args = client_args + " --all" |
| 260 | |
| 261 | if rule.filter_expression: |
| 262 | client_args = client_args + " " + rule.filter_expression |
| 263 | |
| 264 | if rule.log_level_rule: |
| 265 | if isinstance(rule.log_level_rule, lttngctl.LogLevelRuleAsSevereAs): |
| 266 | client_args = client_args + " --loglevel {log_level}".format( |
| 267 | log_level=_get_log_level_argument_name( |
| 268 | rule.log_level_rule.level |
| 269 | ) |
| 270 | ) |
| 271 | elif isinstance(rule.log_level_rule, lttngctl.LogLevelRuleExactly): |
| 272 | client_args = client_args + " --loglevel-only {log_level}".format( |
| 273 | log_level=_get_log_level_argument_name( |
| 274 | rule.log_level_rule.level |
| 275 | ) |
| 276 | ) |
| 277 | else: |
| 278 | raise Unsupported( |
| 279 | "Unsupported log level rule type `{log_level_rule_type}`".format( |
| 280 | log_level_rule_type=type(rule.log_level_rule).__name__ |
| 281 | ) |
| 282 | ) |
| 283 | |
| 284 | if rule.name_pattern_exclusions: |
| 285 | client_args = client_args + " --exclude " |
| 286 | for idx, pattern in enumerate(rule.name_pattern_exclusions): |
| 287 | if idx != 0: |
| 288 | client_args = client_args + "," |
| 289 | client_args = client_args + pattern |
| 290 | else: |
| 291 | raise Unsupported( |
| 292 | "event rule type `{event_rule_type}` is unsupported by LTTng client".format( |
| 293 | event_rule_type=type(rule).__name__ |
| 294 | ) |
| 295 | ) |
| 296 | |
| 297 | self._client._run_cmd(client_args) |
| 298 | |
| 299 | @property |
| 300 | def name(self): |
| 301 | # type: () -> str |
| 302 | return self._name |
| 303 | |
| 304 | @property |
| 305 | def domain(self): |
| 306 | # type: () -> lttngctl.TracingDomain |
| 307 | return self._domain |
| 308 | |
| 309 | @property |
| 310 | def recording_rules(self): |
| 311 | # type: () -> Iterator[lttngctl.EventRule] |
| 312 | list_session_xml = self._client._run_cmd( |
| 313 | "list '{session_name}'".format(session_name=self._session.name), |
| 314 | LTTngClient.CommandOutputFormat.MI_XML, |
| 315 | ) |
| 316 | |
| 317 | root = xml.etree.ElementTree.fromstring(list_session_xml) |
| 318 | command_output = LTTngClient._mi_get_in_element(root, "output") |
| 319 | sessions = LTTngClient._mi_get_in_element(command_output, "sessions") |
| 320 | |
| 321 | # The channel's session is supposed to be the only session returned by the command |
| 322 | if len(sessions) != 1: |
| 323 | raise InvalidMI( |
| 324 | "Only one session expected when listing with an explicit session name" |
| 325 | ) |
| 326 | session = sessions[0] |
| 327 | |
| 328 | # Look for the channel's domain |
| 329 | target_domain = None |
| 330 | target_domain_mi_name = _get_domain_xml_mi_name(self.domain) |
| 331 | for domain in LTTngClient._mi_get_in_element(session, "domains"): |
| 332 | if ( |
| 333 | LTTngClient._mi_get_in_element(domain, "type").text |
| 334 | == target_domain_mi_name |
| 335 | ): |
| 336 | target_domain = domain |
| 337 | |
| 338 | if target_domain is None: |
| 339 | raise ChannelNotFound( |
| 340 | "Failed to find channel `{channel_name}`: no channel in target domain".format( |
| 341 | channel_name=self.name |
| 342 | ) |
| 343 | ) |
| 344 | |
| 345 | target_channel = None |
| 346 | for channel in LTTngClient._mi_get_in_element(target_domain, "channels"): |
| 347 | if LTTngClient._mi_get_in_element(channel, "name").text == self.name: |
| 348 | target_channel = channel |
| 349 | break |
| 350 | |
| 351 | if target_channel is None: |
| 352 | raise ChannelNotFound( |
| 353 | "Failed to find channel `{channel_name}`: no such channel in target domain".format( |
| 354 | channel_name=self.name |
| 355 | ) |
| 356 | ) |
| 357 | |
| 358 | tracepoint_event_rule_class = None |
| 359 | |
| 360 | for event in LTTngClient._mi_get_in_element(target_channel, "events"): |
| 361 | # Note that the "enabled" property is ignored as it is not exposed by |
| 362 | # the EventRule interface. |
| 363 | pattern = LTTngClient._mi_get_in_element(event, "name").text |
| 364 | type = LTTngClient._mi_get_in_element(event, "type").text |
| 365 | |
| 366 | filter_expression = None |
| 367 | filter_expression_element = LTTngClient._mi_find_in_element( |
| 368 | event, "filter_expression" |
| 369 | ) |
| 370 | if filter_expression_element: |
| 371 | filter_expression = filter_expression_element.text |
| 372 | |
| 373 | exclusions = [] |
| 374 | for exclusion in LTTngClient._mi_get_in_element(event, "exclusions"): |
| 375 | exclusions.append(exclusion.text) |
| 376 | |
| 377 | exclusions = exclusions if len(exclusions) > 0 else None |
| 378 | |
| 379 | if type != "TRACEPOINT": |
| 380 | raise Unsupported( |
| 381 | "Non-tracepoint event rules are not supported by this Controller implementation" |
| 382 | ) |
| 383 | |
| 384 | tracepoint_event_rule_class = ( |
| 385 | _get_tracepoint_event_rule_class_from_domain_type(self.domain) |
| 386 | ) |
| 387 | event_rule = None |
| 388 | if self.domain != lttngctl.TracingDomain.Kernel: |
| 389 | log_level_element = LTTngClient._mi_find_in_element(event, "loglevel") |
| 390 | log_level_type_element = LTTngClient._mi_find_in_element( |
| 391 | event, "loglevel_type" |
| 392 | ) |
| 393 | |
| 394 | log_level_rule = None |
| 395 | if log_level_element is not None and log_level_type_element is not None: |
| 396 | if log_level_element.text is None: |
| 397 | raise InvalidMI("`loglevel` element of event rule has no text") |
| 398 | |
| 399 | if log_level_type_element.text == "RANGE": |
| 400 | log_level_rule = lttngctl.LogLevelRuleAsSevereAs( |
| 401 | _get_log_level_from_mi_log_level_name( |
| 402 | log_level_element.text |
| 403 | ) |
| 404 | ) |
| 405 | elif log_level_type_element.text == "SINGLE": |
| 406 | log_level_rule = lttngctl.LogLevelRuleExactly( |
| 407 | _get_log_level_from_mi_log_level_name( |
| 408 | log_level_element.text |
| 409 | ) |
| 410 | ) |
| 411 | |
| 412 | yield tracepoint_event_rule_class( |
| 413 | pattern, filter_expression, log_level_rule, exclusions |
| 414 | ) |
| 415 | else: |
| 416 | yield tracepoint_event_rule_class(pattern, filter_expression) |
| 417 | |
| 418 | |
| 419 | @enum.unique |
| 420 | class _ProcessAttribute(enum.Enum): |
| 421 | PID = "Process ID" |
| 422 | VPID = "Virtual Process ID" |
| 423 | UID = "User ID" |
| 424 | VUID = "Virtual User ID" |
| 425 | GID = "Group ID" |
| 426 | VGID = "Virtual Group ID" |
| 427 | |
| 428 | def __repr__(self): |
| 429 | return "<%s.%s>" % (self.__class__.__name__, self.name) |
| 430 | |
| 431 | |
| 432 | def _get_process_attribute_option_name(attribute): |
| 433 | # type: (_ProcessAttribute) -> str |
| 434 | return { |
| 435 | _ProcessAttribute.PID: "pid", |
| 436 | _ProcessAttribute.VPID: "vpid", |
| 437 | _ProcessAttribute.UID: "uid", |
| 438 | _ProcessAttribute.VUID: "vuid", |
| 439 | _ProcessAttribute.GID: "gid", |
| 440 | _ProcessAttribute.VGID: "vgid", |
| 441 | }[attribute] |
| 442 | |
| 443 | |
| 444 | class _ProcessAttributeTracker(lttngctl.ProcessAttributeTracker): |
| 445 | def __init__( |
| 446 | self, |
| 447 | client, # type: LTTngClient |
| 448 | attribute, # type: _ProcessAttribute |
| 449 | domain, # type: lttngctl.TracingDomain |
| 450 | session, # type: _Session |
| 451 | ): |
| 452 | self._client = client # type: LTTngClient |
| 453 | self._tracked_attribute = attribute # type: _ProcessAttribute |
| 454 | self._domain = domain # type: lttngctl.TracingDomain |
| 455 | self._session = session # type: _Session |
| 456 | if attribute == _ProcessAttribute.PID or attribute == _ProcessAttribute.VPID: |
| 457 | self._allowed_value_types = [int, str] # type: list[type] |
| 458 | else: |
| 459 | self._allowed_value_types = [int] # type: list[type] |
| 460 | |
| 461 | def _call_client(self, cmd_name, value): |
| 462 | # type: (str, Union[int, str]) -> None |
| 463 | if type(value) not in self._allowed_value_types: |
| 464 | raise TypeError( |
| 465 | "Value of type `{value_type}` is not allowed for process attribute {attribute_name}".format( |
| 466 | value_type=type(value).__name__, |
| 467 | attribute_name=self._tracked_attribute.name, |
| 468 | ) |
| 469 | ) |
| 470 | |
| 471 | process_attribute_option_name = _get_process_attribute_option_name( |
| 472 | self._tracked_attribute |
| 473 | ) |
| 474 | domain_name = _get_domain_option_name(self._domain) |
| 475 | self._client._run_cmd( |
| 476 | "{cmd_name} --session '{session_name}' --{domain_name} --{tracked_attribute_name} {value}".format( |
| 477 | cmd_name=cmd_name, |
| 478 | session_name=self._session.name, |
| 479 | domain_name=domain_name, |
| 480 | tracked_attribute_name=process_attribute_option_name, |
| 481 | value=value, |
| 482 | ) |
| 483 | ) |
| 484 | |
| 485 | def track(self, value): |
| 486 | # type: (Union[int, str]) -> None |
| 487 | self._call_client("track", value) |
| 488 | |
| 489 | def untrack(self, value): |
| 490 | # type: (Union[int, str]) -> None |
| 491 | self._call_client("untrack", value) |
| 492 | |
| 493 | |
| 494 | class _Session(lttngctl.Session): |
| 495 | def __init__( |
| 496 | self, |
| 497 | client, # type: LTTngClient |
| 498 | name, # type: str |
| 499 | output, # type: Optional[lttngctl.SessionOutputLocation] |
| 500 | ): |
| 501 | self._client = client # type: LTTngClient |
| 502 | self._name = name # type: str |
| 503 | self._output = output # type: Optional[lttngctl.SessionOutputLocation] |
| 504 | |
| 505 | @property |
| 506 | def name(self): |
| 507 | # type: () -> str |
| 508 | return self._name |
| 509 | |
| 510 | def add_channel( |
| 511 | self, |
| 512 | domain, |
| 513 | channel_name=None, |
| 514 | buffer_sharing_policy=lttngctl.BufferSharingPolicy.PerUID, |
| 515 | ): |
| 516 | # type: (lttngctl.TracingDomain, Optional[str], lttngctl.BufferSharingPolicy) -> lttngctl.Channel |
| 517 | channel_name = lttngctl.Channel._generate_name() |
| 518 | domain_option_name = _get_domain_option_name(domain) |
| 519 | self._client._run_cmd( |
| 520 | "enable-channel --session '{session_name}' --{domain_name} '{channel_name}' {buffer_sharing_policy}".format( |
| 521 | session_name=self.name, |
| 522 | domain_name=domain_option_name, |
| 523 | channel_name=channel_name, |
| 524 | buffer_sharing_policy=( |
| 525 | "--buffers-uid" |
| 526 | if buffer_sharing_policy == lttngctl.BufferSharingPolicy.PerUID |
| 527 | else "--buffers-pid" |
| 528 | ), |
| 529 | ) |
| 530 | ) |
| 531 | return _Channel(self._client, channel_name, domain, self) |
| 532 | |
| 533 | def add_context(self, context_type): |
| 534 | # type: (lttngctl.ContextType) -> None |
| 535 | pass |
| 536 | |
| 537 | @property |
| 538 | def output(self): |
| 539 | # type: () -> "Optional[Type[lttngctl.SessionOutputLocation]]" |
| 540 | return self._output # type: ignore |
| 541 | |
| 542 | def start(self): |
| 543 | # type: () -> None |
| 544 | self._client._run_cmd("start '{session_name}'".format(session_name=self.name)) |
| 545 | |
| 546 | def stop(self): |
| 547 | # type: () -> None |
| 548 | self._client._run_cmd("stop '{session_name}'".format(session_name=self.name)) |
| 549 | |
| 550 | def clear(self): |
| 551 | # type: () -> None |
| 552 | self._client._run_cmd("clear '{session_name}'".format(session_name=self.name)) |
| 553 | |
| 554 | def destroy(self): |
| 555 | # type: () -> None |
| 556 | self._client._run_cmd("destroy '{session_name}'".format(session_name=self.name)) |
| 557 | |
| 558 | def rotate(self, wait=True): |
| 559 | # type: (bool) -> None |
| 560 | self._client.rotate_session_by_name(self.name, wait) |
| 561 | |
| 562 | @property |
| 563 | def is_active(self): |
| 564 | # type: () -> bool |
| 565 | list_session_xml = self._client._run_cmd( |
| 566 | "list '{session_name}'".format(session_name=self.name), |
| 567 | LTTngClient.CommandOutputFormat.MI_XML, |
| 568 | ) |
| 569 | |
| 570 | root = xml.etree.ElementTree.fromstring(list_session_xml) |
| 571 | command_output = LTTngClient._mi_get_in_element(root, "output") |
| 572 | sessions = LTTngClient._mi_get_in_element(command_output, "sessions") |
| 573 | session_mi = LTTngClient._mi_get_in_element(sessions, "session") |
| 574 | |
| 575 | enabled_text = LTTngClient._mi_get_in_element(session_mi, "enabled").text |
| 576 | if enabled_text not in ["true", "false"]: |
| 577 | raise InvalidMI( |
| 578 | "Expected boolean value in element '{}': value='{}'".format( |
| 579 | session_mi.tag, enabled_text |
| 580 | ) |
| 581 | ) |
| 582 | |
| 583 | return enabled_text == "true" |
| 584 | |
| 585 | @property |
| 586 | def kernel_pid_process_attribute_tracker(self): |
| 587 | # type: () -> Type[lttngctl.ProcessIDProcessAttributeTracker] |
| 588 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.PID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 589 | |
| 590 | @property |
| 591 | def kernel_vpid_process_attribute_tracker(self): |
| 592 | # type: () -> Type[lttngctl.VirtualProcessIDProcessAttributeTracker] |
| 593 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VPID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 594 | |
| 595 | @property |
| 596 | def user_vpid_process_attribute_tracker(self): |
| 597 | # type: () -> Type[lttngctl.VirtualProcessIDProcessAttributeTracker] |
| 598 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VPID, lttngctl.TracingDomain.User, self) # type: ignore |
| 599 | |
| 600 | @property |
| 601 | def kernel_gid_process_attribute_tracker(self): |
| 602 | # type: () -> Type[lttngctl.GroupIDProcessAttributeTracker] |
| 603 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.GID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 604 | |
| 605 | @property |
| 606 | def kernel_vgid_process_attribute_tracker(self): |
| 607 | # type: () -> Type[lttngctl.VirtualGroupIDProcessAttributeTracker] |
| 608 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VGID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 609 | |
| 610 | @property |
| 611 | def user_vgid_process_attribute_tracker(self): |
| 612 | # type: () -> Type[lttngctl.VirtualGroupIDProcessAttributeTracker] |
| 613 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VGID, lttngctl.TracingDomain.User, self) # type: ignore |
| 614 | |
| 615 | @property |
| 616 | def kernel_uid_process_attribute_tracker(self): |
| 617 | # type: () -> Type[lttngctl.UserIDProcessAttributeTracker] |
| 618 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.UID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 619 | |
| 620 | @property |
| 621 | def kernel_vuid_process_attribute_tracker(self): |
| 622 | # type: () -> Type[lttngctl.VirtualUserIDProcessAttributeTracker] |
| 623 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VUID, lttngctl.TracingDomain.Kernel, self) # type: ignore |
| 624 | |
| 625 | @property |
| 626 | def user_vuid_process_attribute_tracker(self): |
| 627 | # type: () -> Type[lttngctl.VirtualUserIDProcessAttributeTracker] |
| 628 | return _ProcessAttributeTracker(self._client, _ProcessAttribute.VUID, lttngctl.TracingDomain.User, self) # type: ignore |
| 629 | |
| 630 | |
| 631 | class LTTngClientError(lttngctl.ControlException): |
| 632 | def __init__( |
| 633 | self, |
| 634 | command_args, # type: str |
| 635 | error_output, # type: str |
| 636 | ): |
| 637 | self._command_args = command_args # type: str |
| 638 | self._output = error_output # type: str |
| 639 | |
| 640 | |
| 641 | class LTTngClient(logger._Logger, lttngctl.Controller): |
| 642 | """ |
| 643 | Implementation of a LTTngCtl Controller that uses the `lttng` client as a back-end. |
| 644 | """ |
| 645 | |
| 646 | class CommandOutputFormat(enum.Enum): |
| 647 | MI_XML = 0 |
| 648 | HUMAN = 1 |
| 649 | |
| 650 | _MI_NS = "{https://lttng.org/xml/ns/lttng-mi}" |
| 651 | |
| 652 | def __init__( |
| 653 | self, |
| 654 | test_environment, # type: environment._Environment |
| 655 | log, # type: Optional[Callable[[str], None]] |
| 656 | ): |
| 657 | logger._Logger.__init__(self, log) |
| 658 | self._environment = test_environment # type: environment._Environment |
| 659 | |
| 660 | @staticmethod |
| 661 | def _namespaced_mi_element(property): |
| 662 | # type: (str) -> str |
| 663 | return LTTngClient._MI_NS + property |
| 664 | |
| 665 | def _run_cmd(self, command_args, output_format=CommandOutputFormat.MI_XML): |
| 666 | # type: (str, CommandOutputFormat) -> str |
| 667 | """ |
| 668 | Invoke the `lttng` client with a set of arguments. The command is |
| 669 | executed in the context of the client's test environment. |
| 670 | """ |
| 671 | args = [str(self._environment.lttng_client_path)] # type: list[str] |
| 672 | if output_format == LTTngClient.CommandOutputFormat.MI_XML: |
| 673 | args.extend(["--mi", "xml"]) |
| 674 | |
| 675 | args.extend(shlex.split(command_args)) |
| 676 | |
| 677 | self._log("lttng {command_args}".format(command_args=command_args)) |
| 678 | |
| 679 | client_env = os.environ.copy() # type: dict[str, str] |
| 680 | client_env["LTTNG_HOME"] = str(self._environment.lttng_home_location) |
| 681 | |
| 682 | process = subprocess.Popen( |
| 683 | args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=client_env |
| 684 | ) |
| 685 | |
| 686 | out = process.communicate()[0] |
| 687 | |
| 688 | if process.returncode != 0: |
| 689 | decoded_output = out.decode("utf-8") |
| 690 | for error_line in decoded_output.splitlines(): |
| 691 | self._log(error_line) |
| 692 | |
| 693 | raise LTTngClientError(command_args, decoded_output) |
| 694 | else: |
| 695 | return out.decode("utf-8") |
| 696 | |
| 697 | def create_session(self, name=None, output=None, live=False): |
| 698 | # type: (Optional[str], Optional[lttngctl.SessionOutputLocation], bool) -> lttngctl.Session |
| 699 | name = name if name else lttngctl.Session._generate_name() |
| 700 | |
| 701 | if isinstance(output, lttngctl.LocalSessionOutputLocation): |
| 702 | output_option = "--output '{output_path}'".format(output_path=output.path) |
| 703 | elif isinstance(output, lttngctl.NetworkSessionOutputLocation): |
| 704 | output_option = "--set-url '{}'".format(output.url) |
| 705 | elif output is None: |
| 706 | output_option = "--no-output" |
| 707 | else: |
| 708 | raise TypeError("LTTngClient only supports local or no output") |
| 709 | |
| 710 | self._run_cmd( |
| 711 | "create '{session_name}' {output_option} {live_option}".format( |
| 712 | session_name=name, |
| 713 | output_option=output_option, |
| 714 | live_option="--live" if live else "", |
| 715 | ) |
| 716 | ) |
| 717 | return _Session(self, name, output) |
| 718 | |
| 719 | def start_session_by_name(self, name): |
| 720 | # type: (str) -> None |
| 721 | self._run_cmd("start '{session_name}'".format(session_name=name)) |
| 722 | |
| 723 | def start_session_by_glob_pattern(self, pattern): |
| 724 | # type: (str) -> None |
| 725 | self._run_cmd("start --glob '{pattern}'".format(pattern=pattern)) |
| 726 | |
| 727 | def start_sessions_all(self): |
| 728 | # type: () -> None |
| 729 | self._run_cmd("start --all") |
| 730 | |
| 731 | def stop_session_by_name(self, name): |
| 732 | # type: (str) -> None |
| 733 | self._run_cmd("stop '{session_name}'".format(session_name=name)) |
| 734 | |
| 735 | def stop_session_by_glob_pattern(self, pattern): |
| 736 | # type: (str) -> None |
| 737 | self._run_cmd("stop --glob '{pattern}'".format(pattern=pattern)) |
| 738 | |
| 739 | def stop_sessions_all(self): |
| 740 | # type: () -> None |
| 741 | self._run_cmd("stop --all") |
| 742 | |
| 743 | def destroy_session_by_name(self, name): |
| 744 | # type: (str) -> None |
| 745 | self._run_cmd("destroy '{session_name}'".format(session_name=name)) |
| 746 | |
| 747 | def destroy_session_by_glob_pattern(self, pattern): |
| 748 | # type: (str) -> None |
| 749 | self._run_cmd("destroy --glob '{pattern}'".format(pattern=pattern)) |
| 750 | |
| 751 | def destroy_sessions_all(self): |
| 752 | # type: () -> None |
| 753 | self._run_cmd("destroy --all") |
| 754 | |
| 755 | def rotate_session_by_name(self, name, wait=True): |
| 756 | self._run_cmd( |
| 757 | "rotate '{session_name}' {wait_option}".format( |
| 758 | session_name=name, wait_option="-n" if wait is False else "" |
| 759 | ) |
| 760 | ) |
| 761 | |
| 762 | def schedule_size_based_rotation(self, name, size_bytes): |
| 763 | # type (str, int) -> None |
| 764 | self._run_cmd( |
| 765 | "enable-rotation --session '{session_name}' --size {size}".format( |
| 766 | session_name=name, size=size_bytes |
| 767 | ) |
| 768 | ) |
| 769 | |
| 770 | def schedule_time_based_rotation(self, name, period_seconds): |
| 771 | # type (str, int) -> None |
| 772 | self._run_cmd( |
| 773 | "enable-rotation --session '{session_name}' --timer {period_seconds}s".format( |
| 774 | session_name=name, period_seconds=period_seconds |
| 775 | ) |
| 776 | ) |
| 777 | |
| 778 | @staticmethod |
| 779 | def _mi_find_in_element(element, sub_element_name): |
| 780 | # type: (xml.etree.ElementTree.Element, str) -> Optional[xml.etree.ElementTree.Element] |
| 781 | return element.find(LTTngClient._namespaced_mi_element(sub_element_name)) |
| 782 | |
| 783 | @staticmethod |
| 784 | def _mi_get_in_element(element, sub_element_name): |
| 785 | # type: (xml.etree.ElementTree.Element, str) -> xml.etree.ElementTree.Element |
| 786 | result = LTTngClient._mi_find_in_element(element, sub_element_name) |
| 787 | if result is None: |
| 788 | raise InvalidMI( |
| 789 | "Failed to find element '{}' within command MI element '{}'".format( |
| 790 | element.tag, sub_element_name |
| 791 | ) |
| 792 | ) |
| 793 | |
| 794 | return result |
| 795 | |
| 796 | def list_sessions(self): |
| 797 | # type () -> List[Session] |
| 798 | list_sessions_xml = self._run_cmd( |
| 799 | "list", LTTngClient.CommandOutputFormat.MI_XML |
| 800 | ) |
| 801 | |
| 802 | root = xml.etree.ElementTree.fromstring(list_sessions_xml) |
| 803 | command_output = self._mi_get_in_element(root, "output") |
| 804 | sessions = self._mi_get_in_element(command_output, "sessions") |
| 805 | |
| 806 | ctl_sessions = [] # type: list[lttngctl.Session] |
| 807 | |
| 808 | for session_mi in sessions: |
| 809 | name = self._mi_get_in_element(session_mi, "name").text |
| 810 | path = self._mi_get_in_element(session_mi, "path").text |
| 811 | |
| 812 | if name is None: |
| 813 | raise InvalidMI( |
| 814 | "Invalid empty 'name' element in '{}'".format(session_mi.tag) |
| 815 | ) |
| 816 | if path is None: |
| 817 | raise InvalidMI( |
| 818 | "Invalid empty 'path' element in '{}'".format(session_mi.tag) |
| 819 | ) |
| 820 | if not path.startswith("/"): |
| 821 | raise Unsupported( |
| 822 | "{} does not support non-local session outputs".format(type(self)) |
| 823 | ) |
| 824 | |
| 825 | ctl_sessions.append( |
| 826 | _Session(self, name, lttngctl.LocalSessionOutputLocation(path)) |
| 827 | ) |
| 828 | |
| 829 | return ctl_sessions |