Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/logs/startlivetail.py
1567 views
1
# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
from functools import partial
14
from threading import Thread
15
import contextlib
16
import signal
17
import sys
18
import time
19
20
from awscli.compat import get_stdout_text_writer
21
from awscli.customizations.commands import BasicCommand
22
from awscli.utils import is_a_tty, create_nested_client
23
24
25
DESCRIPTION = (
26
"Starts a Live Tail streaming session for one or more log groups. "
27
"A Live Tail session provides a near real-time streaming of "
28
"log events as they are ingested into selected log groups. "
29
"A session can go on for a maximum of 3 hours.\n\n"
30
"You must have logs:StartLiveTail permission to perform this operation. "
31
"If the log events matching the filters are more than 500 events per second, "
32
"we sample the events to provide the real-time tailing experience.\n\n"
33
"If you are using CloudWatch cross-account observability, "
34
"you can use this operation in a monitoring account and start tailing on "
35
"Log Group(s) present in the linked source accounts. "
36
"For more information, see "
37
"https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html.\n\n"
38
"Live Tail sessions incur charges by session usage time, per minute. "
39
"For pricing details, please refer to "
40
"https://aws.amazon.com/cloudwatch/pricing/."
41
)
42
43
LIST_SCHEMA = {"type": "array", "items": {"type": "string"}}
44
45
LOG_GROUP_IDENTIFIERS = {
46
"name": "log-group-identifiers",
47
"required": True,
48
"positional_arg": False,
49
"nargs": "+",
50
"schema": LIST_SCHEMA,
51
"help_text": (
52
"The Log Group Identifiers are the ARNs for the CloudWatch Logs groups to tail. "
53
"You can provide up to 10 Log Group Identifiers.\n\n"
54
"Logs can be filtered by Log Stream(s) by providing "
55
"--log-stream-names or --log-stream-name-prefixes. "
56
"If more than one Log Group is provided "
57
"--log-stream-names and --log-stream-name-prefixes is disabled. "
58
"--log-stream-names and --log-stream-name-prefixes can't be provided simultaneously.\n\n"
59
"Note - The Log Group ARN must be in the following format. "
60
"Replace REGION and ACCOUNT_ID with your Region and account ID. "
61
"``arn:aws:logs:REGION :ACCOUNT_ID :log-group:LOG_GROUP_NAME``. "
62
"A ``:*`` after the ARN is prohibited."
63
"For more information about ARN format, "
64
'see <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html">CloudWatch Logs resources and operations</a>.'
65
),
66
}
67
68
LOG_STREAM_NAMES = {
69
"name": "log-stream-names",
70
"positional_arg": False,
71
"nargs": "+",
72
"schema": LIST_SCHEMA,
73
"help_text": (
74
"The list of stream names to filter logs by.\n\n This parameter cannot be "
75
"specified when --log-stream-name-prefixes are also specified. "
76
"This parameter cannot be specified when multiple log-group-identifiers are specified"
77
),
78
}
79
80
LOG_STREAM_NAME_PREFIXES = {
81
"name": "log-stream-name-prefixes",
82
"positional_arg": False,
83
"nargs": "+",
84
"schema": LIST_SCHEMA,
85
"help_text": (
86
"The prefix to filter logs by. Only events from log streams with names beginning "
87
"with this prefix will be returned. \n\nThis parameter cannot be specified when "
88
"--log-stream-names is also specified. This parameter cannot be specified when "
89
"multiple log-group-identifiers are specified"
90
),
91
}
92
93
LOG_EVENT_FILTER_PATTERN = {
94
"name": "log-event-filter-pattern",
95
"positional_arg": False,
96
"cli_type_name": "string",
97
"help_text": (
98
"The filter pattern to use. "
99
'See <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html">Filter and Pattern Syntax</a> '
100
"for details. If not provided, all the events are matched. "
101
"This option can be used to include or exclude log events patterns. "
102
"Additionally, when multiple filter patterns are provided, they must be encapsulated by quotes."
103
),
104
}
105
106
107
def signal_handler(printer, signum, frame):
108
printer.interrupt_session = True
109
110
111
@contextlib.contextmanager
112
def handle_signal(printer):
113
signal_list = [signal.SIGINT, signal.SIGTERM]
114
if sys.platform != "win32":
115
signal_list.append(signal.SIGPIPE)
116
actual_signals = []
117
for user_signal in signal_list:
118
actual_signals.append(
119
signal.signal(user_signal, partial(signal_handler, printer))
120
)
121
try:
122
yield
123
finally:
124
for sig, user_signal in enumerate(signal_list):
125
signal.signal(user_signal, actual_signals[sig])
126
127
128
class LiveTailSessionMetadata:
129
def __init__(self) -> None:
130
self._session_start_time = time.time()
131
self._is_sampled = False
132
133
@property
134
def session_start_time(self):
135
return self._session_start_time
136
137
@property
138
def is_sampled(self):
139
return self._is_sampled
140
141
def update_metadata(self, session_metadata):
142
self._is_sampled = session_metadata["sampled"]
143
144
145
class PrintOnlyPrinter:
146
def __init__(self, output, log_events) -> None:
147
self._output = output
148
self._log_events = log_events
149
self.interrupt_session = False
150
151
def _print_log_events(self):
152
for log_event in self._log_events:
153
self._output.write(log_event + "\n")
154
self._output.flush()
155
156
self._log_events.clear()
157
158
def run(self):
159
try:
160
while True:
161
self._print_log_events()
162
163
if self.interrupt_session:
164
break
165
166
time.sleep(1)
167
except (BrokenPipeError, KeyboardInterrupt):
168
pass
169
170
171
class PrintOnlyUI:
172
def __init__(self, output, log_events) -> None:
173
self._log_events = log_events
174
self._printer = PrintOnlyPrinter(output, self._log_events)
175
176
def exit(self):
177
self._printer.interrupt_session = True
178
179
def run(self):
180
with handle_signal(self._printer):
181
self._printer.run()
182
183
184
class LiveTailLogEventsCollector(Thread):
185
def __init__(
186
self,
187
output,
188
ui,
189
response_stream,
190
log_events: list,
191
session_metadata: LiveTailSessionMetadata,
192
) -> None:
193
super().__init__()
194
self._output = output
195
self._ui = ui
196
self._response_stream = response_stream
197
self._log_events = log_events
198
self._session_metadata = session_metadata
199
self._exception = None
200
201
def _collect_log_events(self):
202
try:
203
for event in self._response_stream:
204
if not "sessionUpdate" in event:
205
continue
206
207
session_update = event["sessionUpdate"]
208
self._session_metadata.update_metadata(
209
session_update["sessionMetadata"]
210
)
211
logEvents = session_update["sessionResults"]
212
for logEvent in logEvents:
213
self._log_events.append(logEvent["message"])
214
except Exception as e:
215
self._exception = e
216
217
self._ui.exit()
218
219
def stop(self):
220
if self._exception is not None:
221
self._output.write(str(self._exception) + "\n")
222
self._output.flush()
223
224
def run(self):
225
self._collect_log_events()
226
227
228
class StartLiveTailCommand(BasicCommand):
229
NAME = "start-live-tail"
230
DESCRIPTION = DESCRIPTION
231
ARG_TABLE = [
232
LOG_GROUP_IDENTIFIERS,
233
LOG_STREAM_NAMES,
234
LOG_STREAM_NAME_PREFIXES,
235
LOG_EVENT_FILTER_PATTERN,
236
]
237
238
def __init__(self, session):
239
super(StartLiveTailCommand, self).__init__(session)
240
self._output = get_stdout_text_writer()
241
242
def _get_client(self, parsed_globals):
243
return create_nested_client(
244
self._session, "logs",
245
region_name=parsed_globals.region,
246
endpoint_url=parsed_globals.endpoint_url,
247
verify=parsed_globals.verify_ssl,
248
)
249
250
def _get_start_live_tail_kwargs(self, parsed_args):
251
kwargs = {"logGroupIdentifiers": parsed_args.log_group_identifiers}
252
253
if parsed_args.log_stream_names is not None:
254
kwargs["logStreamNames"] = parsed_args.log_stream_names
255
if parsed_args.log_stream_name_prefixes is not None:
256
kwargs["logStreamNamePrefixes"] = parsed_args.log_stream_name_prefixes
257
if parsed_args.log_event_filter_pattern is not None:
258
kwargs["logEventFilterPattern"] = parsed_args.log_event_filter_pattern
259
260
return kwargs
261
262
def _is_color_allowed(self, color):
263
if color == "on":
264
return True
265
elif color == "off":
266
return False
267
return is_a_tty()
268
269
def _run_main(self, parsed_args, parsed_globals):
270
self._client = self._get_client(parsed_globals)
271
272
start_live_tail_kwargs = self._get_start_live_tail_kwargs(parsed_args)
273
response = self._client.start_live_tail(**start_live_tail_kwargs)
274
275
log_events = []
276
session_metadata = LiveTailSessionMetadata()
277
278
ui = PrintOnlyUI(self._output, log_events)
279
280
log_events_collector = LiveTailLogEventsCollector(
281
self._output, ui, response["responseStream"], log_events, session_metadata
282
)
283
log_events_collector.daemon = True
284
285
log_events_collector.start()
286
ui.run()
287
288
log_events_collector.stop()
289
sys.exit(0)
290
291