Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changesets/ask-for-a-collector-in-the-installer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: patch
type: add
---

The `appsignal install` command asks for a collector endpoint and a service name, and adds them to the `__appsignal__.py` file it writes.
75 changes: 63 additions & 12 deletions src/appsignal/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,38 @@

appsignal = Appsignal(
active=True,
name="{name}",
# Please do not commit this key to your source control management system.
# Move this to your app's security credentials or environment variables.
# https://docs.appsignal.com/python/configuration/options.html#option-push_api_key
push_api_key="{push_api_key}",
)
{options})
"""

INSTALL_FILE_OPTION_COMMENTS = {
"push_api_key": [
"Please do not commit this key to your source control management system.",
"Move this to your app's security credentials or environment variables.",
"https://docs.appsignal.com/python/configuration/options.html"
"#option-push_api_key",
],
}

COLLECTOR_URL = (
"https://appsignal.com/redirect-to/organization?to=admin/hosted_collectors"
)

INSTALL_FILE_NAME = "__appsignal__.py"

WARNING_EMOJI = "\u26A0\ufe0f"


def install_file_contents(options: Options) -> str:
lines: list[str] = []
for key, value in options.items():
lines.extend(
f" # {comment}\n"
for comment in INSTALL_FILE_OPTION_COMMENTS.get(key, [])
)
lines.append(f' {key}="{value}",\n')
return INSTALL_FILE_TEMPLATE.format(options="".join(lines))


class InstallCommand(AppsignalCLICommand):
"""Generate Appsignal client integration code."""

Expand All @@ -48,6 +67,12 @@ def run(self) -> int:

print()

collector_endpoint = self._collector_endpoint()
if collector_endpoint:
options["collector_endpoint"] = collector_endpoint
options["service_name"] = self._service_name()
Comment on lines +72 to +73

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These only get asked for during as a prompt when you run the installer, right?
We should also add them to the CLI options, and help text output, so all the inputs for the installer can be given via both methods.

print()

if self._should_write_file():
print(f"Writing the {INSTALL_FILE_NAME} configuration file...")
self._write_file(options)
Expand All @@ -73,6 +98,37 @@ def run(self) -> int:

return 0

def _collector_endpoint(self) -> str | None:
while True:
endpoint = input(
f"Please enter your collector endpoint (create one at {COLLECTOR_URL}): "
)
if endpoint:
return endpoint
if self._input_should_continue_without_collector():
return None

def _input_should_continue_without_collector(self) -> bool:
response = input(
"Are you sure? Without a collector, logging and distributed tracing"
" won't work. Continue without one? (y/N): "
)
Comment on lines +111 to +115

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would want to flip the behavior for now so that collector mode is not the default, but when they accept the prompt if they want logging and distributed tracing we ask them for the collector details.

if len(response) == 0 or response[0].lower() == "n":
return False
if response[0].lower() == "y":
return True
print('Please answer "y" (yes) or "n" (no)')
return self._input_should_continue_without_collector()

def _service_name(self) -> str:
name = ""
while not name:
name = input(
"Please enter the name of this service"
" (such as web-server or background-worker): "
)
return name

def _should_write_file(self) -> bool:
if os.path.exists(INSTALL_FILE_NAME):
return self._input_should_overwrite_file()
Expand All @@ -92,12 +148,7 @@ def _input_should_overwrite_file(self) -> bool:

def _write_file(self, options: Options) -> None:
with open(INSTALL_FILE_NAME, "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name=options["name"],
push_api_key=options["push_api_key"],
)

f.write(file_contents)
f.write(install_file_contents(options))

def _requirements_file(self) -> str | None:
current_dir = os.getcwd()
Expand Down
13 changes: 6 additions & 7 deletions tests/cli/test_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import shutil

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE
from appsignal.cli.install import install_file_contents
from appsignal.config import Options

from .utils import mock_input

Expand Down Expand Up @@ -67,9 +68,8 @@ def test_demo_with_config_file(request, mocker, capfd):
os.chdir(test_dir)
# Add client file
with open(os.path.join(test_dir, "__appsignal__.py"), "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name="My app name",
push_api_key="000",
file_contents = install_file_contents(
Options(name="My app name", push_api_key="000")
)
f.write(file_contents)

Expand All @@ -92,9 +92,8 @@ def test_demo_with_config_file_and_cli_options(request, mocker, capfd):
os.chdir(test_dir)
# Add client file
with open(os.path.join(test_dir, "__appsignal__.py"), "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name="My app name",
push_api_key="000",
file_contents = install_file_contents(
Options(name="My app name", push_api_key="000")
)
f.write(file_contents)

Expand Down
8 changes: 4 additions & 4 deletions tests/cli/test_diagnose.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import shutil

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE
from appsignal.cli.install import install_file_contents
from appsignal.config import Options

from .utils import mock_input

Expand Down Expand Up @@ -65,9 +66,8 @@ def test_diagnose_with_config_file(request, mocker, capfd):
os.chdir(test_dir)
# Add client file
with open(os.path.join(test_dir, "__appsignal__.py"), "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name="My app name",
push_api_key="000",
file_contents = install_file_contents(
Options(name="My app name", push_api_key="000")
)
f.write(file_contents)

Expand Down
97 changes: 85 additions & 12 deletions tests/cli/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from unittest.mock import MagicMock

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE
from appsignal.cli.install import install_file_contents
from appsignal.config import Options

from .utils import mock_input

Expand All @@ -22,21 +23,52 @@
)
"""

EXPECTED_COLLECTOR_FILE_CONTENTS = """from appsignal import Appsignal

appsignal = Appsignal(
active=True,
name="My app name",
# Please do not commit this key to your source control management system.
# Move this to your app's security credentials or environment variables.
# https://docs.appsignal.com/python/configuration/options.html#option-push_api_key
push_api_key="My push API key",
collector_endpoint="https://collector.example",
service_name="web-server",
)
"""

COLLECTOR_ENDPOINT_PROMPT = (
"Please enter your collector endpoint (create one at"
" https://appsignal.com/redirect-to/organization?to=admin/hosted_collectors): "
)

CONTINUE_WITHOUT_COLLECTOR_PROMPT = (
"Are you sure? Without a collector, logging and distributed tracing"
" won't work. Continue without one? (y/N): "
)

SERVICE_NAME_PROMPT = (
"Please enter the name of this service"
" (such as web-server or background-worker): "
)

NO_COLLECTOR = (
(COLLECTOR_ENDPOINT_PROMPT, ""),
(CONTINUE_WITHOUT_COLLECTOR_PROMPT, "y"),
)


def mock_file_operations(mocker, file_exists: bool = False):
mocker.patch("os.path.exists", return_value=file_exists)
mocker.patch("appsignal.cli.install.open")


def assert_wrote_file_contents(mocker):
def assert_wrote_file_contents(mocker, contents=EXPECTED_FILE_CONTENTS):
from appsignal.cli import install

builtins_open: MagicMock = install.open # type: ignore[attr-defined]
assert mocker.call("__appsignal__.py", "w") in builtins_open.mock_calls
assert (
mocker.call().__enter__().write(EXPECTED_FILE_CONTENTS)
in builtins_open.mock_calls
)
assert mocker.call().__enter__().write(contents) in builtins_open.mock_calls


def assert_did_not_write_file_contents(mocker):
Expand All @@ -52,9 +84,8 @@ def assert_did_not_write_file_contents(mocker):

def assert_wrote_real_file_contents(test_dir, name, push_api_key):
with open(os.path.join(test_dir, "__appsignal__.py")) as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name=name,
push_api_key=push_api_key,
file_contents = install_file_contents(
Options(name=name, push_api_key=push_api_key)
)
assert f.read() == file_contents

Expand All @@ -72,6 +103,7 @@ def test_install_command_run(mocker):
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
*NO_COLLECTOR,
):
main(["install"])

Expand All @@ -88,6 +120,7 @@ def test_install_command_when_empty_value_ask_again(mocker):
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", ""),
("Please enter your Push API key: ", "My push API key"),
*NO_COLLECTOR,
):
main(["install"])

Expand All @@ -101,6 +134,7 @@ def test_install_command_when_push_api_key_given(mocker):
with mock_input(
mocker,
("Please enter the name of your application: ", "My app name"),
*NO_COLLECTOR,
):
main(["install", "--push-api-key", "My push API key"])

Expand All @@ -115,6 +149,7 @@ def test_install_command_when_file_exists_overwrite(mocker, request):
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
*NO_COLLECTOR,
(
"The __appsignal__.py file already exists."
" Should it be overwritten? (y/N): ",
Expand All @@ -126,9 +161,8 @@ def test_install_command_when_file_exists_overwrite(mocker, request):
os.makedirs(test_dir)
# Add client file
with open(os.path.join(test_dir, "__appsignal__.py"), "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name="Existing app name",
push_api_key="Existing Push API key",
file_contents = install_file_contents(
Options(name="Existing app name", push_api_key="Existing Push API key")
)
f.write(file_contents)
os.chdir(test_dir)
Expand All @@ -149,6 +183,7 @@ def test_install_command_when_file_exists_no_overwrite(mocker):
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
*NO_COLLECTOR,
(
"The __appsignal__.py file already exists."
" Should it be overwritten? (y/N): ",
Expand All @@ -175,3 +210,41 @@ def test_install_command_when_invalid_api_key_ask_again(mocker):
main(["install"])

assert_did_not_write_file_contents(mocker)


def test_install_command_with_collector(mocker):
mock_file_operations(mocker)
mock_validate_push_api_key_request(mocker)

with mock_input(
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
(COLLECTOR_ENDPOINT_PROMPT, "https://collector.example"),
(SERVICE_NAME_PROMPT, "web-server"),
):
main(["install"])

assert_wrote_file_contents(mocker, EXPECTED_COLLECTOR_FILE_CONTENTS)


def test_install_command_when_empty_collector_endpoint_ask_again(mocker):
mock_file_operations(mocker)
mock_validate_push_api_key_request(mocker)

with mock_input(
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
(COLLECTOR_ENDPOINT_PROMPT, ""),
(CONTINUE_WITHOUT_COLLECTOR_PROMPT, ""),
(COLLECTOR_ENDPOINT_PROMPT, ""),
(CONTINUE_WITHOUT_COLLECTOR_PROMPT, "maybe"),
(CONTINUE_WITHOUT_COLLECTOR_PROMPT, "n"),
(COLLECTOR_ENDPOINT_PROMPT, "https://collector.example"),
(SERVICE_NAME_PROMPT, ""),
(SERVICE_NAME_PROMPT, "web-server"),
):
main(["install"])

assert_wrote_file_contents(mocker, EXPECTED_COLLECTOR_FILE_CONTENTS)
Loading