Repository: meltano/meltano

Issue:
feature: Make installation progress messages use logging instead of `click.echo`
### Feature scope

CLI (options, error messages, logging, etc.)

### Description

We'd like to start reducing the amount of non-formatted and non-configurable output in Meltano's CLI. The installation result messages are good start:

```
Installing 1 plugin           
Installing extractor 'tap-getpocket'...
Installed extractor 'tap-getpocket'
```

### Related

* https://github.com/meltano/meltano/pull/8482#issuecomment-2042923721
* https://github.com/meltano/meltano/issues/6394


Discussion:
@edgarrmondragon Do you think it would make sense to eventually make `click.echo` a banned API using Ruff?
> @edgarrmondragon Do you think it would make sense to eventually make `click.echo` a banned API using Ruff?

Yeah, I would hope so. I want everything we output to go through our logging infra and probably make the default output format easier on the eyes.

Resolving patch:
diff --git a/src/meltano/cli/install.py b/src/meltano/cli/install.py
--- a/src/meltano/cli/install.py
+++ b/src/meltano/cli/install.py
@@ -95,7 +95,7 @@ def install(  # noqa: C901
         tracker.track_command_event(CliEvent.aborted)
         raise
 
-    click.echo(f"Installing {len(plugins)} plugins...")
+    logger.info("Installing %d plugins", len(plugins))
     tracker.add_contexts(
         PluginsTrackingContext([(candidate, None) for candidate in plugins]),
     )
diff --git a/src/meltano/cli/utils.py b/src/meltano/cli/utils.py
--- a/src/meltano/cli/utils.py
+++ b/src/meltano/cli/utils.py
@@ -20,6 +20,7 @@
 from meltano.core.plugin_install_service import (
     PluginInstallReason,
     PluginInstallService,
+    PluginInstallState,
     PluginInstallStatus,
 )
 from meltano.core.plugin_lock_service import LockfileAlreadyExistsError
@@ -402,7 +403,7 @@ def add_required_plugins(
     return added_plugins
 
 
-def install_status_update(install_state):
+def install_status_update(install_state: PluginInstallState):
     """
     Print the status of plugin installation.
 
@@ -414,16 +415,14 @@ def install_status_update(install_state):
         PluginInstallStatus.RUNNING,
         PluginInstallStatus.SKIPPED,
     }:
-        msg = f"{install_state.verb} {desc} '{plugin.name}'..."
-        click.secho(msg)
+        logger.info("%s %s '%s'", install_state.verb, desc, plugin.name)
     elif install_state.status is PluginInstallStatus.ERROR:
-        click.secho(install_state.message, fg="red")
-        click.secho(install_state.details, err=True)
-    elif install_state.status is PluginInstallStatus.WARNING:
-        click.secho(f"Warning! {install_state.message}.", fg="yellow")
+        logger.error(install_state.message)
+        logger.info(install_state.details)
+    elif install_state.status is PluginInstallStatus.WARNING:  # pragma: no cover
+        logger.warning(install_state.message)
     elif install_state.status is PluginInstallStatus.SUCCESS:
-        msg = f"{install_state.verb} {desc} '{plugin.name}'"
-        click.secho(msg, fg="green")
+        logger.info("%s %s '%s'", install_state.verb, desc, plugin.name)
 
 
 def install_plugins(
@@ -447,23 +446,26 @@ def install_plugins(
     num_skipped = len([status for status in install_results if status.skipped])
     num_failed = len(install_results) - num_successful
 
-    fg = "green"
+    level = logging.INFO
     if num_failed >= 0 and num_successful == 0:
-        fg = "red"
+        level = logging.ERROR
     elif num_failed > 0 and num_successful > 0:
-        fg = "yellow"
+        level = logging.WARNING
 
     if len(plugins) > 1:
-        verb = "Updated" if reason == PluginInstallReason.UPGRADE else "Installed"
-        click.secho(
-            f"{verb} {num_successful-num_skipped}/{num_successful+num_failed} plugins",
-            fg=fg,
+        logger.log(
+            level,
+            "%s %d/%d plugins",
+            "Updated" if reason == PluginInstallReason.UPGRADE else "Installed",
+            num_successful - num_skipped,
+            num_successful + num_failed,
         )
-    if num_skipped:
-        verb = "Skipped installing"
-        click.secho(
-            f"{verb} {num_skipped}/{num_successful+num_failed} plugins",
-            fg=fg,
+    if num_skipped:  # pragma: no cover
+        logger.log(
+            level,
+            "Skipped installing %d/%d plugins",
+            num_skipped,
+            num_successful + num_failed,
         )
 
     return num_failed == 0
diff --git a/src/meltano/core/plugin/project_plugin.py b/src/meltano/core/plugin/project_plugin.py
--- a/src/meltano/core/plugin/project_plugin.py
+++ b/src/meltano/core/plugin/project_plugin.py
@@ -49,6 +49,7 @@ class ProjectPlugin(PluginRef):  # noqa: WPS230, WPS214 # too many attrs and met
 
     VARIANT_ATTR = "variant"
 
+    name: str
     variant: str | None
 
     def __init__(


Test patch: