Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sam logs help text #5397

Merged
merged 3 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"])))