Skip to content

Commit

Permalink
feat: sam logs help text (#5397)
Browse files Browse the repository at this point in the history
* feat: `sam logs` help text

* fix: make ruff happy

* fix: address comments
  • Loading branch information
sriram-mv authored Jul 3, 2023
1 parent 5e8df69 commit 9877db2
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 29 deletions.
4 changes: 2 additions & 2 deletions samcli/cli/root/command_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"validate": "Validate an AWS SAM template.",
"build": "Build your AWS serverless function code.",
"local": "Run your AWS serverless function locally.",
"remote": "Invoke or send an event to cloud resources in your CFN stack",
"remote": "Invoke or send an event to cloud resources in your AWS Cloudformation stack.",
"package": "Package an AWS SAM application.",
"deploy": "Deploy an AWS SAM application.",
"delete": "Delete an AWS SAM application and the artifacts created by sam deploy.",
"logs": "Fetch AWS Cloudwatch logs for a function.",
"logs": "Fetch AWS Cloudwatch logs for AWS Lambda Functions or Cloudwatch Log groups.",
"publish": "Publish a packaged AWS SAM template to AWS Serverless Application Repository for easy sharing.",
"traces": "Fetch AWS X-Ray traces.",
"sync": "Sync an AWS SAM project to AWS.",
Expand Down
52 changes: 25 additions & 27 deletions samcli/commands/logs/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from samcli.cli.main import common_options as cli_framework_options
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.options import common_observability_options, generate_next_command_recommendation
from samcli.commands.logs.core.command import LogsCommand
from samcli.commands.logs.validation_and_exception_handlers import (
SAM_LOGS_ADDITIONAL_EXCEPTION_HANDLERS,
stack_name_cw_log_group_validation,
Expand All @@ -20,37 +21,34 @@

LOG = logging.getLogger(__name__)

SHORT_HELP = (
"Fetch logs for your AWS SAM Application or AWS Cloudformation stack - Lambda Functions/CloudWatch Log groups"
)

HELP_TEXT = """
Use this command to fetch logs generated by your Lambda function.\n
\b
When your functions are a part of a CloudFormation stack, you can fetch logs using the function's
LogicalID when you specify the stack name.
$ sam logs -n HelloWorldFunction --stack-name mystack \n
\b
Or, you can fetch logs using the function's name.
$ sam logs -n mystack-HelloWorldFunction-1FJ8PD36GML2Q \n
\b
You can view logs for a specific time range using the -s (--start-time) and -e (--end-time) options
$ sam logs -n HelloWorldFunction --stack-name mystack -s '10min ago' -e '2min ago' \n
\b
You can also add the --tail option to wait for new logs and see them as they arrive.
$ sam logs -n HelloWorldFunction --stack-name mystack --tail \n
\b
Use the --filter option to quickly find logs that match terms, phrases or values in your log events.
$ sam logs -n HelloWorldFunction --stack-name mystack --filter 'error' \n
\b
Fetch logs for all supported resources in your application, and additionally from the specified log groups.
$ sam logs --cw-log-group /aws/lambda/myfunction-123 --cw-log-group /aws/lambda/myfunction-456
\b
You can now fetch logs from supported resources, by only providing --stack-name parameter
$ sam logs --stack-name mystack \n
\b
You can also fetch logs from a resource which is defined in a nested stack.
$ sam logs --stack-name mystack -n MyNestedStack/HelloWorldFunction
The sam logs commands fetches logs of Lambda Functions/CloudWatch log groups
with additional filtering by options.
"""

DESCRIPTION = """
Fetch logs generated by Lambda functions or other Cloudwatch log groups with additional filtering.
"""

@click.command("logs", help=HELP_TEXT, short_help="Fetch logs for a function")

@click.command(
"logs",
short_help=SHORT_HELP,
context_settings={
"ignore_unknown_options": False,
"allow_interspersed_args": True,
"allow_extra_args": True,
"max_content_width": 120,
},
cls=LogsCommand,
help=HELP_TEXT,
description=DESCRIPTION,
requires_credentials=True,
)
@configuration_option(provider=TomlProvider(section="parameters"))
@click.option(
"--name",
Expand Down
Empty file.
119 changes: 119 additions & 0 deletions samcli/commands/logs/core/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from click import Context, style

from samcli.cli.core.command import CoreCommand
from samcli.cli.row_modifiers import RowDefinition, ShowcaseRowModifier
from samcli.commands.logs.core.formatters import LogsCommandHelpTextFormatter
from samcli.commands.logs.core.options import OPTIONS_INFO

COL_SIZE_MODIFIER = 38


class LogsCommand(CoreCommand):
class CustomFormatterContext(Context):
formatter_class = LogsCommandHelpTextFormatter

context_class = CustomFormatterContext

@staticmethod
def format_examples(ctx: Context, formatter: LogsCommandHelpTextFormatter):
with formatter.indented_section(name="Examples", extra_indents=1):
with formatter.indented_section(
name="Fetch logs with Lambda Function Logical ID and Cloudformation Stack Name"
):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(f"$ {ctx.command_path} -n HelloWorldFunction --stack-name mystack"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(name="View logs for specific time range"):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"$ {ctx.command_path} -n HelloWorldFunction --stack-name mystack -s "
f"'10min ago' -e '2min ago'"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(name="Tail new logs"):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(f"$ {ctx.command_path} -n HelloWorldFunction --stack-name " f"mystack --tail"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(name="Fetch from Cloudwatch log groups"):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"$ {ctx.command_path} --cw-log-group /aws/lambda/myfunction-123 "
f"--cw-log-group /aws/lambda/myfunction-456"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)

with formatter.indented_section(name="Fetch logs from supported resources in Cloudformation stack"):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(f"$ {ctx.command_path} ---stack-name mystack"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)

with formatter.indented_section(name="Fetch logs from resource defined in nested Cloudformation stack"):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"$ {ctx.command_path} ---stack-name mystack -n MyNestedStack/HelloWorldFunction"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)

def format_options(self, ctx: Context, formatter: LogsCommandHelpTextFormatter) -> None: # type:ignore
# `ignore` is put in place here for mypy even though it is the correct behavior,
# as the `formatter_class` can be set in subclass of Command. If ignore is not set,
# mypy raises argument needs to be HelpFormatter as super class defines it.

self.format_description(formatter)
LogsCommand.format_examples(ctx, formatter)

CoreCommand._format_options(
ctx=ctx,
params=self.get_params(ctx),
formatter=formatter,
formatting_options=OPTIONS_INFO,
write_rd_overrides={"col_max": COL_SIZE_MODIFIER},
)
19 changes: 19 additions & 0 deletions samcli/commands/logs/core/formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.cli.row_modifiers import BaseLineRowModifier
from samcli.commands.logs.core.options import ALL_OPTIONS


class LogsCommandHelpTextFormatter(RootCommandHelpTextFormatter):
# Picked an additive constant that gives an aesthetically pleasing look.
ADDITIVE_JUSTIFICATION = 22

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add Additional space after determining the longest option.
# However, do not justify with padding for more than half the width of
# the terminal to retain aesthetics.
self.left_justification_length = min(
max([len(option) for option in ALL_OPTIONS]) + self.ADDITIVE_JUSTIFICATION,
self.width // 2 - self.indent_increment,
)
self.modifiers = [BaseLineRowModifier()]
45 changes: 45 additions & 0 deletions samcli/commands/logs/core/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Logs Command Options related Datastructures for formatting.
"""
from typing import Dict, List

from samcli.cli.core.options import ALL_COMMON_OPTIONS, add_common_options_info
from samcli.cli.row_modifiers import RowDefinition

# The ordering of the option lists matter, they are the order in which options will be displayed.

LOG_IDENTIFIER_OPTIONS: List[str] = ["stack_name", "cw_log_group", "name"]

# Can be used instead of the options in the first list
ADDITIONAL_OPTIONS: List[str] = ["include_traces", "filter", "output", "tail", "start_time", "end_time"]

AWS_CREDENTIAL_OPTION_NAMES: List[str] = ["region", "profile"]

CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"]

ALL_OPTIONS: List[str] = (
LOG_IDENTIFIER_OPTIONS
+ AWS_CREDENTIAL_OPTION_NAMES
+ ADDITIONAL_OPTIONS
+ CONFIGURATION_OPTION_NAMES
+ ALL_COMMON_OPTIONS
)

OPTIONS_INFO: Dict[str, Dict] = {
"Log Identifier Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(LOG_IDENTIFIER_OPTIONS)}},
"AWS Credential Options": {
"option_names": {opt: {"rank": idx} for idx, opt in enumerate(AWS_CREDENTIAL_OPTION_NAMES)}
},
"Additional Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(ADDITIONAL_OPTIONS)}},
"Configuration Options": {
"option_names": {opt: {"rank": idx} for idx, opt in enumerate(CONFIGURATION_OPTION_NAMES)},
"extras": [
RowDefinition(name="Learn more about configuration files at:"),
RowDefinition(
name="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli"
"-config.html. "
),
],
},
}
add_common_options_info(OPTIONS_INFO)
Empty file.
73 changes: 73 additions & 0 deletions tests/unit/commands/logs/core/test_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import unittest
from unittest.mock import Mock, patch
from samcli.commands.logs.core.command import LogsCommand
from samcli.commands.logs.command import DESCRIPTION
from tests.unit.cli.test_command import MockFormatter


class MockParams:
def __init__(self, rv, name):
self.rv = rv
self.name = name

def get_help_record(self, ctx):
return self.rv


class TestLogsCommand(unittest.TestCase):
@patch.object(LogsCommand, "get_params")
def test_get_options_logs_command_text(self, mock_get_params):
ctx = Mock()
ctx.command_path = "sam logs"
ctx.parent.command_path = "sam"
formatter = MockFormatter(scrub_text=True)
# NOTE(sriram-mv): One option per option section.
mock_get_params.return_value = [
MockParams(rv=("--region", "Region"), name="region"),
MockParams(rv=("--debug", ""), name="debug"),
MockParams(rv=("--config-file", ""), name="config_file"),
MockParams(rv=("--stack-name", ""), name="stack_name"),
MockParams(rv=("--tail", ""), name="tail"),
MockParams(rv=("--beta-features", ""), name="beta_features"),
]

cmd = LogsCommand(name="logs", requires_credentials=True, description=DESCRIPTION)
expected_output = {
"AWS Credential Options": [("", ""), ("--region", ""), ("", "")],
"Additional Options": [("", ""), ("--tail", ""), ("", "")],
"Beta Options": [("", ""), ("--beta-features", ""), ("", "")],
"Configuration Options": [("", ""), ("--config-file", ""), ("", "")],
"Description": [(cmd.description + cmd.description_addendum, "")],
"Examples": [],
"Fetch from Cloudwatch log groups": [
("", ""),
(
"$ sam logs --cw-log-group "
"/aws/lambda/myfunction-123 "
"--cw-log-group "
"/aws/lambda/myfunction-456\x1b[0m",
"",
),
],
"Fetch logs from resource defined in nested Cloudformation stack": [
("", ""),
("$ sam " "logs " "---stack-name " "mystack " "-n " "MyNestedStack/HelloWorldFunction\x1b[0m", ""),
],
"Fetch logs from supported resources in Cloudformation stack": [
("", ""),
("$ sam logs " "---stack-name " "mystack\x1b[0m", ""),
],
"Fetch logs with Lambda Function Logical ID and Cloudformation Stack Name": [
("", ""),
("$ " "sam " "logs " "-n " "HelloWorldFunction " "--stack-name " "mystack\x1b[0m", ""),
],
"Log Identifier Options": [("", ""), ("--stack-name", ""), ("", "")],
"Other Options": [("", ""), ("--debug", ""), ("", "")],
"Tail new logs": [("", ""), ("$ sam logs -n HelloWorldFunction --stack-name mystack " "--tail\x1b[0m", "")],
"View logs for specific time range": [
("", ""),
("$ sam logs -n HelloWorldFunction " "--stack-name mystack -s '10min ago' " "-e '2min ago'\x1b[0m", ""),
],
}
cmd.format_options(ctx, formatter)
self.assertEqual(formatter.data, expected_output)
12 changes: 12 additions & 0 deletions tests/unit/commands/logs/core/test_formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from shutil import get_terminal_size
from unittest import TestCase

from samcli.cli.row_modifiers import BaseLineRowModifier
from samcli.commands.logs.core.formatters import LogsCommandHelpTextFormatter


class TestLogsCommandHelpTextFormatter(TestCase):
def test_logs_formatter(self):
self.formatter = LogsCommandHelpTextFormatter()
self.assertTrue(self.formatter.left_justification_length <= get_terminal_size().columns // 2)
self.assertIsInstance(self.formatter.modifiers[0], BaseLineRowModifier)
12 changes: 12 additions & 0 deletions tests/unit/commands/logs/core/test_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from unittest import TestCase

from click import Option

from samcli.commands.logs.command import cli
from samcli.commands.logs.core.options import ALL_OPTIONS


class TestOptions(TestCase):
def test_all_options_formatted(self):
command_options = [param.human_readable_name if isinstance(param, Option) else None for param in cli.params]
self.assertEqual(sorted(ALL_OPTIONS), sorted(filter(lambda item: item is not None, command_options + ["help"])))

0 comments on commit 9877db2

Please sign in to comment.