You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2022/04/04 12:05:59 UTC
[airflow] branch main updated: Print configuration on scheduler startup. (#22588)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 78586b45a0 Print configuration on scheduler startup. (#22588)
78586b45a0 is described below
commit 78586b45a0f6007ab6b94c35b33790a944856e5e
Author: Karthikeyan Singaravelan <ti...@gmail.com>
AuthorDate: Mon Apr 4 17:35:49 2022 +0530
Print configuration on scheduler startup. (#22588)
---
airflow/cli/commands/scheduler_command.py | 12 ++++++++-
airflow/utils/cli.py | 39 ++++++++++++++++++++++++++++
docs/apache-airflow/howto/set-config.rst | 4 +++
tests/cli/commands/test_scheduler_command.py | 18 +++++++++++++
4 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/airflow/cli/commands/scheduler_command.py b/airflow/cli/commands/scheduler_command.py
index bc6e983ee5..c4ecdf411a 100644
--- a/airflow/cli/commands/scheduler_command.py
+++ b/airflow/cli/commands/scheduler_command.py
@@ -26,7 +26,15 @@ from daemon.pidfile import TimeoutPIDLockFile
from airflow import settings
from airflow.jobs.scheduler_job import SchedulerJob
from airflow.utils import cli as cli_utils
-from airflow.utils.cli import process_subdir, setup_locations, setup_logging, sigint_handler, sigquit_handler
+from airflow.utils.cli import (
+ get_config_with_source,
+ process_subdir,
+ setup_locations,
+ setup_logging,
+ sigconf_handler,
+ sigint_handler,
+ sigquit_handler,
+)
def _create_scheduler_job(args):
@@ -53,6 +61,7 @@ def _run_scheduler_job(args):
def scheduler(args):
"""Starts Airflow Scheduler"""
print(settings.HEADER)
+ print(get_config_with_source())
if args.daemon:
pid, stdout, stderr, log_file = setup_locations(
@@ -72,6 +81,7 @@ def scheduler(args):
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigint_handler)
signal.signal(signal.SIGQUIT, sigquit_handler)
+ signal.signal(signal.SIGUSR1, sigconf_handler)
_run_scheduler_job(args=args)
diff --git a/airflow/utils/cli.py b/airflow/utils/cli.py
index a15e1b7f02..9629951a67 100644
--- a/airflow/utils/cli.py
+++ b/airflow/utils/cli.py
@@ -18,6 +18,7 @@
#
"""Utilities module for cli"""
import functools
+import io
import json
import logging
import os
@@ -28,12 +29,18 @@ import threading
import traceback
import warnings
from argparse import Namespace
+from contextlib import redirect_stdout
from datetime import datetime
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, cast
+import pygments
+from pygments.lexers.configs import IniLexer
+
from airflow import settings
+from airflow.configuration import conf
from airflow.exceptions import AirflowException
from airflow.utils import cli_action_loggers
+from airflow.utils.code_utils import get_terminal_formatter
from airflow.utils.log.non_caching_file_handler import NonCachingFileHandler
from airflow.utils.platform import getuser, is_terminal_support_colors
from airflow.utils.session import provide_session
@@ -271,6 +278,13 @@ def sigint_handler(sig, frame):
sys.exit(0)
+def sigconf_handler(sig, frame):
+ """Print configuration and source including default values."""
+ config = get_config_with_source(include_default=True)
+ log = logging.getLogger(__name__)
+ log.info(config)
+
+
def sigquit_handler(sig, frame):
"""
Helps debug deadlocks by printing stacktraces when this gets a SIGQUIT
@@ -328,3 +342,28 @@ def suppress_logs_and_warning(f: T) -> T:
logging.disable(logging.NOTSET)
return cast(T, _wrapper)
+
+
+def get_config_with_source(include_default: bool = False) -> str:
+ """Return configuration along with source for each option."""
+ config_dict = conf.as_dict(display_source=True)
+
+ with io.StringIO() as buf, redirect_stdout(buf):
+ for section, options in config_dict.items():
+ if not include_default:
+ options = {
+ key: (value, source) for key, (value, source) in options.items() if source != "default"
+ }
+
+ # Print the section only when there are options after filtering
+ if options:
+ print(f"[{section}]")
+ for key, (value, source) in options.items():
+ print(f"{key} = {value} [{source}]")
+ print()
+ code = buf.getvalue()
+
+ if is_terminal_support_colors():
+ code = pygments.highlight(code=code, formatter=get_terminal_formatter(), lexer=IniLexer())
+
+ return code
diff --git a/docs/apache-airflow/howto/set-config.rst b/docs/apache-airflow/howto/set-config.rst
index a31885b2da..75762c1a74 100644
--- a/docs/apache-airflow/howto/set-config.rst
+++ b/docs/apache-airflow/howto/set-config.rst
@@ -126,6 +126,10 @@ the example below.
work as expected. A good example for that is :ref:`secret_key<config:webserver__secret_key>` which
should be same on the Webserver and Worker to allow Webserver to fetch logs from Worker.
+ During startup the scheduler prints configuration options whose values differ from the default
+ values along with the source from which the value was loaded (i.e. airflow.cfg, environment variable, etc).
+ You can print all configuration options by sending ``SIGUSR1`` signal to the scheduler.
+
The webserver key is also used to authorize requests to Celery workers when logs are retrieved. The token
generated using the secret key has a short expiry time though - make sure that time on ALL the machines
that you run airflow components on is synchronized (for example using ntpd) otherwise you might get
diff --git a/tests/cli/commands/test_scheduler_command.py b/tests/cli/commands/test_scheduler_command.py
index 00b851f55a..6ef1013dcc 100644
--- a/tests/cli/commands/test_scheduler_command.py
+++ b/tests/cli/commands/test_scheduler_command.py
@@ -15,6 +15,9 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import contextlib
+import io
+import os
import unittest
from unittest import mock
@@ -87,3 +90,18 @@ class TestSchedulerCommand(unittest.TestCase):
scheduler_command.scheduler(args)
finally:
mock_process().terminate.assert_called()
+
+ @mock.patch.dict(os.environ, {'AIRFLOW__CORE__PARALLELISM': '35'}, clear=True)
+ @mock.patch("airflow.cli.commands.scheduler_command.SchedulerJob")
+ @mock.patch("airflow.cli.commands.scheduler_command.Process")
+ def test_scheduler_print_config(self, mock_process, mock_scheduler_job):
+ args = self.parser.parse_args(['scheduler'])
+ with conf_vars({("core", "sql_alchemy_conn_cmd"): 'echo hello'}):
+ with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
+ scheduler_command.scheduler(args)
+ output = temp_stdout.getvalue()
+
+ assert "sql_alchemy_conn = < hidden > [cmd]" in output
+ assert "max_active_tasks_per_dag = 16 [airflow.cfg]" in output
+ assert "parallelism = < hidden > [env var]" in output
+ assert "max_active_runs_per_dag = 16 [default]" not in output