client/commands/remote_logging.py (104 lines of code) (raw):
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import dataclasses
import os
import sys
from typing import Callable, Dict, Optional
from pyre_extensions import ParameterSpecification
from pyre_extensions.type_variable_operators import Concatenate
from typing_extensions import Protocol
from .. import (
configuration as configuration_module,
statistics_logger,
timer,
version,
)
from . import commands
TParams = ParameterSpecification("TParams")
@dataclasses.dataclass(frozen=True)
class ExitCodeWithAdditionalLogging:
exit_code: commands.ExitCode
additional_logging: Dict[str, Optional[str]] = dataclasses.field(
default_factory=dict
)
class _DecoratorWithDynamicLogging(Protocol):
def __call__(
self,
__f: "Callable[Concatenate[configuration_module.Configuration, TParams], ExitCodeWithAdditionalLogging]", # noqa: B950
) -> "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]": # noqa: B950
...
class _DecoratorWithoutDynamicLogging(Protocol):
def __call__(
self,
__f: "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]", # noqa: B950
) -> "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]": # noqa: B950
...
def log_usage_with_additional_info(command_name: str) -> _DecoratorWithDynamicLogging:
"""
This decorator is a variant of `log_usage`.
With `log_usage`, what to log needs to be determined prior to running the
decorated command. In contrast, `log_usage_with_additional_info` collects what
to log after the decorated command is done executing. Logging content can
therefore depend on the execution result after-the-fact.
In exchange for the flexibility, `log_usage_with_additional_info` requires slightly
more sophisticated setup: the decorated function should not only return a
`commands.ExitCode`, but also attach the dynamically-computed logging dictionary
alongside with the return code. The decorator itself will make sure that the
additional info will be faithfully forwarded when invoking the underlying logging
API.
"""
auxiliary_info: Dict[str, Optional[str]] = {
"cwd": os.getcwd(),
"client_version": version.__version__,
"command_line": " ".join(sys.argv),
"command": command_name,
}
def decorator(
__command: "Callable[Concatenate[configuration_module.Configuration, TParams], ExitCodeWithAdditionalLogging]", # noqa: B950
) -> "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]": # noqa: B950
def decorated(
configuration: configuration_module.Configuration,
*args: TParams.args,
**kwargs: TParams.kwargs
) -> commands.ExitCode:
command_timer: timer.Timer = timer.Timer()
def log_success(
exit_code: int, additional_logging: Dict[str, Optional[str]]
) -> None:
statistics_logger.log_with_configuration(
category=statistics_logger.LoggerCategory.USAGE,
configuration=configuration,
integers={
"exit_code": exit_code,
"runtime": int(command_timer.stop_in_millisecond()),
},
normals={**auxiliary_info, **additional_logging},
)
def log_failure(
error: Exception, exit_code: int = commands.ExitCode.FAILURE
) -> None:
statistics_logger.log_with_configuration(
category=statistics_logger.LoggerCategory.USAGE,
configuration=configuration,
integers={
"exit_code": exit_code,
"runtime": int(command_timer.stop_in_millisecond()),
},
normals={
**auxiliary_info,
"client_exception": str(error),
},
)
try:
result = __command(configuration, *args, **kwargs)
exit_code = result.exit_code
log_success(exit_code, result.additional_logging)
return exit_code
except configuration_module.InvalidConfiguration as error:
log_failure(error, exit_code=commands.ExitCode.CONFIGURATION_ERROR)
raise
except Exception as error:
log_failure(error)
raise
return decorated
# pyre-ignore[7]: (T84575843) This should be fine.
return decorator
def log_usage(command_name: str) -> _DecoratorWithoutDynamicLogging:
"""
This decorator is intended to be used on command-like functions that take
`Configuration` as the first argument and use `commands.ExitCode` as the return
type. It adds remote logging to the decorated function. What's included in the
logging records: exit code and running time of the given command, plus the
exception info if the command raises.
"""
def decorator(
__command: "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]", # noqa: B950
) -> "Callable[Concatenate[configuration_module.Configuration, TParams], commands.ExitCode]": # noqa: B950
def transformed_command(
configuration: configuration_module.Configuration,
*args: TParams.args,
**kwargs: TParams.kwargs
) -> ExitCodeWithAdditionalLogging:
return ExitCodeWithAdditionalLogging(
exit_code=__command(configuration, *args, **kwargs),
additional_logging={},
)
return log_usage_with_additional_info(command_name)(transformed_command)
# pyre-ignore[7]: (T84575843) This should be fine.
return decorator