Skip to content

Commit

Permalink
add predefined log modes
Browse files Browse the repository at this point in the history
  • Loading branch information
SYangster committed Feb 17, 2025
1 parent 11c9331 commit 60745c8
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 139 deletions.
19 changes: 16 additions & 3 deletions docs/user_guide/configurations/logging_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Default Logging Configuration
=============================

The default logging configuration json file **log_config.json.default** is divided into 3 main sections: formatters, handlers, and loggers.
This file can be found at :github_nvflare_link:`log_config.json <nvflare/fuel/utils/log_config.json>`.
See the `configuration dictionary schema <(https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema)>`_ for more details.

.. code-block:: json
Expand Down Expand Up @@ -288,7 +289,7 @@ Modifying Logging Configurations
Simulator log configuration
===========================

Users can specify a log configuration file in the simulator command with the ``-l`` simulator argument:
Users can specify a log configuration in the simulator command with the ``-l`` simulator argument:

.. code-block:: shell
Expand All @@ -301,6 +302,13 @@ Or using the ``log_config`` argument of the Job API simulator run:
job.simulator_run("/tmp/nvflare/hello-numpy-sag", log_config="log_config.json")
The log config argument be one of the following:

- path to a log configuration json file (``/path/to/my_log_config.json``)
- log mode (``default``, ``concise``, ``verbose``)
- log level (``debug``, ``info``, ``warning``, ``error``, ``critical``)


POC log configurations
======================
If you search the POC workspace, you will find the following:
Expand Down Expand Up @@ -342,14 +350,19 @@ We also recommend using the :ref:`Dynamic Logging Configuration Commands <dynami
Dynamic Logging Configuration Commands
**************************************

We provide two admin commands to enable users to dynamically configure the site or job level logging.
When running the FLARE system (POC mode or production mode), there are two sets of logs: the site logs and job logs.
The current site log configuration will be used for the site logs as well as the log config of any new job started on that site.
In order to access the logs in the workspaces refer to :ref:`access_server_workspace` and :ref:`client_workspace`.

We provide two admin commands to enable users to dynamically configure the site or job level logging when running the FLARE system.

- **target**: ``server``, ``client <clients>...``, or ``all``
- **config**: log configuration

- path to a json log configuration file (``/path/to/my_log_config.json``)
- predefined log mode (``default``, ``concise``, ``verbose``)
- log level name/number (``debug``, ``INFO``, ``30``)
- read the current log configuration file (``reload``)
- read the current log configuration file from the workspace (``reload``)

To configure the target site logging (does not affect jobs):

Expand Down
89 changes: 59 additions & 30 deletions nvflare/fuel/utils/log_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@
import re
from logging import Logger
from logging.handlers import RotatingFileHandler
from typing import Union

from nvflare.apis.workspace import Workspace

DEFAULT_LOG_JSON = "log_config.json"


class LogMode:
RELOAD = "reload"
DEFAULT = "default"
CONCISE = "concise"
VERBOSE = "verbose"


class ANSIColor:
# Basic ANSI color codes
Expand Down Expand Up @@ -278,40 +288,59 @@ def apply_log_config(dict_config, dir_path: str = "", file_prefix: str = ""):
logging.config.dictConfig(dict_config)


def dynamic_log_config(config: str, workspace: Workspace, job_id: str = None):
# Dynamically configure log given a config (filepath, levelname, levelnumber, 'reload'), apply the config to the proper locations.
if not isinstance(config, str):
raise ValueError(
f"Unsupported config type. Expect config to be string filepath, levelname, levelnumber, or 'reload' but got {type(config)}"
)

if config == "reload":
config = workspace.get_log_config_file_path()

if os.path.isfile(config):
# Read confg file
with open(config, "r") as f:
dict_config = json.load(f)

if job_id:
dir_path = workspace.get_run_dir(job_id)
def dynamic_log_config(config: Union[dict, str], dir_path: str, reload_path: str):
# Dynamically configure log given a config (dict, filepath, LogMode, or level), apply the config to the proper locations.

with open(os.path.join(os.path.dirname(__file__), DEFAULT_LOG_JSON), "r") as f:
default_log_json = json.load(f)

if isinstance(config, dict):
apply_log_config(config, dir_path)
elif isinstance(config, str):
# Handle pre-defined LogModes
if config == LogMode.RELOAD:
config = reload_path
elif config == LogMode.DEFAULT:
apply_log_config(default_log_json, dir_path)
return
elif config == LogMode.CONCISE:
concise_dict = default_log_json
concise_dict["formatters"]["consoleFormatter"][
"fmt"
] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
concise_dict["handlers"]["consoleHandler"]["filters"] = ["FLFilter"]
apply_log_config(concise_dict, dir_path)
return
elif config == LogMode.VERBOSE:
verbose_dict = default_log_json
verbose_dict["formatters"]["consoleFormatter"][
"fmt"
] = "%(asctime)s - %(identity)s - %(name)s - %(levelname)s - %(message)s"
verbose_dict["loggers"]["root"]["level"] = "DEBUG"
apply_log_config(verbose_dict, dir_path)
return

if os.path.isfile(config):
# Read config file
with open(config, "r") as f:
dict_config = json.load(f)

apply_log_config(dict_config, dir_path)
else:
dir_path = workspace.get_root_dir()

apply_log_config(dict_config, dir_path)
# If logging is not yet configured, use default config
if not logging.getLogger().hasHandlers():
apply_log_config(default_log_json, dir_path)

else:
# Set level of root logger based on levelname or levelnumber
if config.isdigit():
level = int(config)
if not (0 <= level <= 50):
raise ValueError(f"Invalid logging level: {level}")
else:
level = getattr(logging, config.upper(), None)
if level is None:
# Set level of root logger based on levelname or levelnumber
level = int(config) if config.isdigit() else getattr(logging, config.upper(), None)
if level is None or not (0 <= level <= 50):
raise ValueError(f"Invalid logging level: {config}")

logging.getLogger().setLevel(level)
logging.getLogger().setLevel(level)
else:
raise ValueError(
f"Unsupported config type. Expect config to be a dict, filepath, level, or LogMode but got {type(config)}"
)


def add_log_file_handler(log_file_name):
Expand Down
77 changes: 0 additions & 77 deletions nvflare/private/fed/app/simulator/log_config.json

This file was deleted.

41 changes: 23 additions & 18 deletions nvflare/private/fed/app/simulator/simulator_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@
from nvflare.fuel.f3.stats_pool import StatsPoolManager
from nvflare.fuel.hci.server.authz import AuthorizationService
from nvflare.fuel.sec.audit import AuditService
from nvflare.fuel.utils import log_utils
from nvflare.fuel.utils.argument_utils import parse_vars
from nvflare.fuel.utils.config_service import ConfigService
from nvflare.fuel.utils.gpu_utils import get_host_gpu_ids
from nvflare.fuel.utils.log_utils import apply_log_config
from nvflare.fuel.utils.log_utils import dynamic_log_config
from nvflare.fuel.utils.network_utils import get_open_ports
from nvflare.fuel.utils.zip_utils import split_path, unzip_all_from_bytes, zip_directory_to_bytes
from nvflare.private.defs import AppFolderConstants
Expand Down Expand Up @@ -126,8 +127,10 @@ def __init__(
f" {os.path.join(running_dir, self.workspace)}"
)
self.workspace = os.path.join(running_dir, self.workspace)

if log_config:
self.log_config = os.path.join(running_dir, log_config)
log_config_path = os.path.join(running_dir, log_config)
self.log_config = log_config_path if os.path.isfile(log_config_path) else log_config

def _generate_args(
self,
Expand Down Expand Up @@ -172,18 +175,14 @@ def setup(self):
for i in range(self.args.n_clients):
self.client_names.append("site-" + str(i + 1))

log_config_file_path = os.path.join(self.args.workspace, "local", WorkspaceConstants.LOGGING_CONFIG)
if not os.path.isfile(log_config_file_path):
log_config_file_path = os.path.join(os.path.dirname(log_utils.__file__), WorkspaceConstants.LOGGING_CONFIG)

if self.args.log_config:
log_config_file_path = self.args.log_config
if not os.path.isfile(log_config_file_path):
self.logger.error(f"log_config: {log_config_file_path} is not a valid file path")
return False
log_config = self.args.log_config
else:
log_config_file_path = os.path.join(self.args.workspace, "local", WorkspaceConstants.LOGGING_CONFIG)
if not os.path.isfile(log_config_file_path):
log_config_file_path = os.path.join(os.path.dirname(__file__), WorkspaceConstants.LOGGING_CONFIG)

with open(log_config_file_path, "r") as f:
dict_config = json.load(f)
log_config = log_config_file_path

self.args.config_folder = "config"
self.args.job_id = SimulatorConstants.JOB_NAME
Expand All @@ -206,7 +205,11 @@ def setup(self):

os.makedirs(os.path.join(self.simulator_root, SiteType.SERVER))

apply_log_config(dict_config, os.path.join(self.simulator_root, SiteType.SERVER))
dynamic_log_config(
config=log_config,
dir_path=os.path.join(self.simulator_root, SiteType.SERVER),
reload_path=log_config_file_path,
)

try:
data_bytes, job_name, meta = self.validate_job_data()
Expand Down Expand Up @@ -694,14 +697,16 @@ def _pick_next_client(self):
def do_one_task(self, client, num_of_threads, gpu, lock, timeout=60.0, task_name=RunnerTask.TASK_EXEC):
open_port = get_open_ports(1)[0]
client_workspace = os.path.join(self.args.workspace, client.client_name)

log_config_file_path = os.path.join(self.args.workspace, "local", WorkspaceConstants.LOGGING_CONFIG)
if not os.path.isfile(log_config_file_path):
log_config_file_path = os.path.join(os.path.dirname(log_utils.__file__), WorkspaceConstants.LOGGING_CONFIG)

if self.args.log_config:
logging_config = self.args.log_config
if not os.path.isfile(logging_config):
raise ValueError(f"log_config: {logging_config} is not a valid file path")
else:
logging_config = os.path.join(
self.args.workspace, client.client_name, "local", WorkspaceConstants.LOGGING_CONFIG
)
logging_config = log_config_file_path

decomposer_module = ConfigService.get_str_var(
name=ConfigVarName.DECOMPOSER_MODULE, conf=SystemConfigs.RESOURCES_CONF
)
Expand Down
12 changes: 5 additions & 7 deletions nvflare/private/fed/app/simulator/simulator_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.

import argparse
import json
import os
import sys
import threading
Expand All @@ -30,7 +29,7 @@
from nvflare.fuel.f3.mpm import MainProcessMonitor as mpm
from nvflare.fuel.hci.server.authz import AuthorizationService
from nvflare.fuel.sec.audit import AuditService
from nvflare.fuel.utils.log_utils import apply_log_config
from nvflare.fuel.utils.log_utils import dynamic_log_config
from nvflare.private.fed.app.deployer.base_client_deployer import BaseClientDeployer
from nvflare.private.fed.app.utils import check_parent_alive, init_security_content_service
from nvflare.private.fed.client.client_engine import ClientEngine
Expand Down Expand Up @@ -240,18 +239,17 @@ def main(args):
thread = threading.Thread(target=check_parent_alive, args=(parent_pid, stop_event))
thread.start()

with open(args.logging_config, "r") as f:
dict_config = json.load(f)

apply_log_config(dict_config, args.workspace)

os.chdir(args.workspace)
startup = os.path.join(args.workspace, WorkspaceConstants.STARTUP_FOLDER_NAME)
os.makedirs(startup, exist_ok=True)
local = os.path.join(args.workspace, WorkspaceConstants.SITE_FOLDER_NAME)
os.makedirs(local, exist_ok=True)
workspace = Workspace(root_dir=args.workspace, site_name=args.client)

dynamic_log_config(
config=args.logging_config, dir_path=args.workspace, reload_path=workspace.get_log_config_file_path()
)

fobs_initialize(workspace, job_id=SimulatorConstants.JOB_NAME)
register_ext_decomposers(args.decomposer_module)
AuthorizationService.initialize(EmptyAuthorizer())
Expand Down
7 changes: 6 additions & 1 deletion nvflare/private/fed/client/admin_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,13 @@ def process(self, data: Shareable, fl_ctx: FLContext):
"""
engine = fl_ctx.get_engine()
workspace = engine.get_workspace()
try:
dynamic_log_config(data, engine.get_workspace(), fl_ctx.get_job_id())
dynamic_log_config(
config=data,
dir_path=workspace.get_run_dir(fl_ctx.get_job_id()),
reload_path=workspace.get_log_config_file_path(),
)
except Exception as e:
return secure_format_exception(e)

Expand Down
4 changes: 3 additions & 1 deletion nvflare/private/fed/client/sys_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ def process(self, req: Message, app_ctx) -> Message:
workspace = fl_ctx.get_prop(FLContextKey.WORKSPACE_OBJECT)

try:
dynamic_log_config(req.body, workspace)
dynamic_log_config(
config=req.body, dir_path=workspace.get_root_dir(), reload_path=workspace.get_log_config_file_path()
)
except Exception as e:
return error_reply(secure_format_exception(e))

Expand Down
Loading

0 comments on commit 60745c8

Please sign in to comment.