Path: blob/develop/awscli/customizations/logs/startlivetail.py
1567 views
# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12from functools import partial13from threading import Thread14import contextlib15import signal16import sys17import time1819from awscli.compat import get_stdout_text_writer20from awscli.customizations.commands import BasicCommand21from awscli.utils import is_a_tty, create_nested_client222324DESCRIPTION = (25"Starts a Live Tail streaming session for one or more log groups. "26"A Live Tail session provides a near real-time streaming of "27"log events as they are ingested into selected log groups. "28"A session can go on for a maximum of 3 hours.\n\n"29"You must have logs:StartLiveTail permission to perform this operation. "30"If the log events matching the filters are more than 500 events per second, "31"we sample the events to provide the real-time tailing experience.\n\n"32"If you are using CloudWatch cross-account observability, "33"you can use this operation in a monitoring account and start tailing on "34"Log Group(s) present in the linked source accounts. "35"For more information, see "36"https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html.\n\n"37"Live Tail sessions incur charges by session usage time, per minute. "38"For pricing details, please refer to "39"https://aws.amazon.com/cloudwatch/pricing/."40)4142LIST_SCHEMA = {"type": "array", "items": {"type": "string"}}4344LOG_GROUP_IDENTIFIERS = {45"name": "log-group-identifiers",46"required": True,47"positional_arg": False,48"nargs": "+",49"schema": LIST_SCHEMA,50"help_text": (51"The Log Group Identifiers are the ARNs for the CloudWatch Logs groups to tail. "52"You can provide up to 10 Log Group Identifiers.\n\n"53"Logs can be filtered by Log Stream(s) by providing "54"--log-stream-names or --log-stream-name-prefixes. "55"If more than one Log Group is provided "56"--log-stream-names and --log-stream-name-prefixes is disabled. "57"--log-stream-names and --log-stream-name-prefixes can't be provided simultaneously.\n\n"58"Note - The Log Group ARN must be in the following format. "59"Replace REGION and ACCOUNT_ID with your Region and account ID. "60"``arn:aws:logs:REGION :ACCOUNT_ID :log-group:LOG_GROUP_NAME``. "61"A ``:*`` after the ARN is prohibited."62"For more information about ARN format, "63'see <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html">CloudWatch Logs resources and operations</a>.'64),65}6667LOG_STREAM_NAMES = {68"name": "log-stream-names",69"positional_arg": False,70"nargs": "+",71"schema": LIST_SCHEMA,72"help_text": (73"The list of stream names to filter logs by.\n\n This parameter cannot be "74"specified when --log-stream-name-prefixes are also specified. "75"This parameter cannot be specified when multiple log-group-identifiers are specified"76),77}7879LOG_STREAM_NAME_PREFIXES = {80"name": "log-stream-name-prefixes",81"positional_arg": False,82"nargs": "+",83"schema": LIST_SCHEMA,84"help_text": (85"The prefix to filter logs by. Only events from log streams with names beginning "86"with this prefix will be returned. \n\nThis parameter cannot be specified when "87"--log-stream-names is also specified. This parameter cannot be specified when "88"multiple log-group-identifiers are specified"89),90}9192LOG_EVENT_FILTER_PATTERN = {93"name": "log-event-filter-pattern",94"positional_arg": False,95"cli_type_name": "string",96"help_text": (97"The filter pattern to use. "98'See <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html">Filter and Pattern Syntax</a> '99"for details. If not provided, all the events are matched. "100"This option can be used to include or exclude log events patterns. "101"Additionally, when multiple filter patterns are provided, they must be encapsulated by quotes."102),103}104105106def signal_handler(printer, signum, frame):107printer.interrupt_session = True108109110@contextlib.contextmanager111def handle_signal(printer):112signal_list = [signal.SIGINT, signal.SIGTERM]113if sys.platform != "win32":114signal_list.append(signal.SIGPIPE)115actual_signals = []116for user_signal in signal_list:117actual_signals.append(118signal.signal(user_signal, partial(signal_handler, printer))119)120try:121yield122finally:123for sig, user_signal in enumerate(signal_list):124signal.signal(user_signal, actual_signals[sig])125126127class LiveTailSessionMetadata:128def __init__(self) -> None:129self._session_start_time = time.time()130self._is_sampled = False131132@property133def session_start_time(self):134return self._session_start_time135136@property137def is_sampled(self):138return self._is_sampled139140def update_metadata(self, session_metadata):141self._is_sampled = session_metadata["sampled"]142143144class PrintOnlyPrinter:145def __init__(self, output, log_events) -> None:146self._output = output147self._log_events = log_events148self.interrupt_session = False149150def _print_log_events(self):151for log_event in self._log_events:152self._output.write(log_event + "\n")153self._output.flush()154155self._log_events.clear()156157def run(self):158try:159while True:160self._print_log_events()161162if self.interrupt_session:163break164165time.sleep(1)166except (BrokenPipeError, KeyboardInterrupt):167pass168169170class PrintOnlyUI:171def __init__(self, output, log_events) -> None:172self._log_events = log_events173self._printer = PrintOnlyPrinter(output, self._log_events)174175def exit(self):176self._printer.interrupt_session = True177178def run(self):179with handle_signal(self._printer):180self._printer.run()181182183class LiveTailLogEventsCollector(Thread):184def __init__(185self,186output,187ui,188response_stream,189log_events: list,190session_metadata: LiveTailSessionMetadata,191) -> None:192super().__init__()193self._output = output194self._ui = ui195self._response_stream = response_stream196self._log_events = log_events197self._session_metadata = session_metadata198self._exception = None199200def _collect_log_events(self):201try:202for event in self._response_stream:203if not "sessionUpdate" in event:204continue205206session_update = event["sessionUpdate"]207self._session_metadata.update_metadata(208session_update["sessionMetadata"]209)210logEvents = session_update["sessionResults"]211for logEvent in logEvents:212self._log_events.append(logEvent["message"])213except Exception as e:214self._exception = e215216self._ui.exit()217218def stop(self):219if self._exception is not None:220self._output.write(str(self._exception) + "\n")221self._output.flush()222223def run(self):224self._collect_log_events()225226227class StartLiveTailCommand(BasicCommand):228NAME = "start-live-tail"229DESCRIPTION = DESCRIPTION230ARG_TABLE = [231LOG_GROUP_IDENTIFIERS,232LOG_STREAM_NAMES,233LOG_STREAM_NAME_PREFIXES,234LOG_EVENT_FILTER_PATTERN,235]236237def __init__(self, session):238super(StartLiveTailCommand, self).__init__(session)239self._output = get_stdout_text_writer()240241def _get_client(self, parsed_globals):242return create_nested_client(243self._session, "logs",244region_name=parsed_globals.region,245endpoint_url=parsed_globals.endpoint_url,246verify=parsed_globals.verify_ssl,247)248249def _get_start_live_tail_kwargs(self, parsed_args):250kwargs = {"logGroupIdentifiers": parsed_args.log_group_identifiers}251252if parsed_args.log_stream_names is not None:253kwargs["logStreamNames"] = parsed_args.log_stream_names254if parsed_args.log_stream_name_prefixes is not None:255kwargs["logStreamNamePrefixes"] = parsed_args.log_stream_name_prefixes256if parsed_args.log_event_filter_pattern is not None:257kwargs["logEventFilterPattern"] = parsed_args.log_event_filter_pattern258259return kwargs260261def _is_color_allowed(self, color):262if color == "on":263return True264elif color == "off":265return False266return is_a_tty()267268def _run_main(self, parsed_args, parsed_globals):269self._client = self._get_client(parsed_globals)270271start_live_tail_kwargs = self._get_start_live_tail_kwargs(parsed_args)272response = self._client.start_live_tail(**start_live_tail_kwargs)273274log_events = []275session_metadata = LiveTailSessionMetadata()276277ui = PrintOnlyUI(self._output, log_events)278279log_events_collector = LiveTailLogEventsCollector(280self._output, ui, response["responseStream"], log_events, session_metadata281)282log_events_collector.daemon = True283284log_events_collector.start()285ui.run()286287log_events_collector.stop()288sys.exit(0)289290291