improve log handling

This commit is contained in:
checktheroads 2020-04-13 17:31:31 -07:00
parent fee998f168
commit 90725ed67f
3 changed files with 63 additions and 39 deletions

View file

@ -53,7 +53,7 @@ The following global settings can be set in `hyperglass.yaml`:
| `request_timeout` | Integer | `30` | Global timeout in seconds for all requests. The UI uses this field's exact value when submitting queries. The backend uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout. |
| `listen_address` | String | `'localhost'` | Local IPv4/IPv6 Address the hyperglass application listens on to serve web traffic. |
| `listen_port` | Integer | `8001` | Local TCP port the hyperglass application listens on to serve web traffic. |
| `log_file` | String | | Path to a log file to which hyperglass can write logs. If none is set, hyperglass will write logs to a file located at `/tmp/`, with a uniquely generated name for each time hyperglass is started. |
| `log_directory` | String | | Path to a directory, to which hyperglass can write logs. If none is set, hyperglass will write logs to a file located at `/tmp/`, with a uniquely generated name for each time hyperglass is started. |
| `cors_origins` | List | `[]` | Allowed [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) hosts. By default, no CORS hosts are allowed. |
| `netmiko_delay_factor` | Integer \| Float | `0.1` | Override the [Netmiko global delay factor](https://ktbyers.github.io/netmiko/docs/netmiko/index.html). |

View file

@ -6,6 +6,7 @@ import copy
import json
import math
from pathlib import Path
from datetime import datetime
# Third Party
import yaml
@ -16,12 +17,10 @@ from pydantic import ValidationError
from hyperglass.util import log, check_path, set_app_path
from hyperglass.constants import (
CREDIT,
LOG_LEVELS,
LOG_HANDLER,
DEFAULT_HELP,
DEFAULT_TERMS,
DEFAULT_DETAILS,
LOG_HANDLER_FILE,
SUPPORTED_QUERY_TYPES,
__version__,
)
@ -83,7 +82,7 @@ STATIC_PATH = CONFIG_PATH / "static"
CONFIG_MAIN, CONFIG_DEVICES, CONFIG_COMMANDS = _check_config_files(CONFIG_PATH)
def _set_log_level(debug, log_file=None):
def _set_log_level(debug):
"""Set log level based on debug state.
Arguments:
@ -93,27 +92,56 @@ def _set_log_level(debug, log_file=None):
{bool} -- True
"""
stdout_handler = LOG_HANDLER.copy()
file_handler = LOG_HANDLER_FILE.copy()
if debug:
log_level = "DEBUG"
stdout_handler["level"] = log_level
file_handler["level"] = log_level
os.environ["HYPERGLASS_LOG_LEVEL"] = log_level
log.configure(handlers=[stdout_handler])
if log_file is not None:
file_handler.update({"sink": log_file})
log_handlers = [stdout_handler, file_handler]
else:
log_handlers = [stdout_handler]
log.remove()
log.configure(handlers=log_handlers, levels=LOG_LEVELS)
if debug:
log.debug("Debugging enabled")
return True
def _set_file_logging(log_directory, log_format, log_max_size):
"""Set up file-based logging from configuration parameters."""
if log_format == "json":
log_file_name = "hyperglass_log.json"
structured = True
else:
log_file_name = "hyperglass_log.log"
structured = False
log_file = log_directory / log_file_name
if log_format == "text":
now_str = "hyperglass logs for " + datetime.utcnow().strftime(
"%B %d, %Y beginning at %H:%M:%S UTC"
)
now_str_y = len(now_str) + 6
now_str_x = len(now_str) + 4
log_break = (
"#" * now_str_y,
"\n#" + " " * now_str_x + "#\n",
"# ",
now_str,
" #",
"\n#" + " " * now_str_x + "#\n",
"#" * now_str_y,
)
with log_file.open("a+") as lf:
lf.write(f'\n\n{"".join(log_break)}\n\n')
log.add(log_file, rotation=log_max_size, serialize=structured)
log.debug("Logging to file enabled")
return True
def _config_required(config_path: Path) -> dict:
try:
with config_path.open("r") as cf:
@ -219,6 +247,12 @@ except ValidationError as validation_errors:
error_msg=error["msg"],
)
# Set up file logging once configuration parameters are initialized.
_set_file_logging(
log_directory=params.log_directory,
log_format=params.log_format,
log_max_size=params.log_max_size,
)
# Perform post-config initialization string formatting or other
# functions that require access to other config levels. E.g.,
@ -255,7 +289,7 @@ except KeyError:
# Re-evaluate debug state after config is validated
_set_log_level(params.debug, params.log_file)
_set_log_level(params.debug)
def _build_frontend_networks():

View file

@ -3,16 +3,16 @@
# Standard Library
from typing import List, Union, Optional
from pathlib import Path
from datetime import datetime
from ipaddress import ip_address
# Third Party
from pydantic import (
Field,
FilePath,
ByteSize,
StrictInt,
StrictStr,
StrictBool,
DirectoryPath,
IPvAnyAddress,
constr,
validator,
@ -98,10 +98,18 @@ class Params(HyperglassModel):
title="Listen Port",
description="Local TCP port the hyperglass application listens on to serve web traffic.",
)
log_file: Optional[FilePath] = Field(
None,
title="Log File",
description="Path to a log file to which hyperglass can write logs. If none is set, hyperglass will write logs to a file located at `/tmp/`, with a uniquely generated name for each time hyperglass is started.",
log_directory: DirectoryPath = Field(
Path("/tmp"), # noqa: S108
title="Log Directory",
description="Path to a directory, to which hyperglass can write logs. If none is set, hyperglass will write logs to a file located at `/tmp/`, with a uniquely generated name for each time hyperglass is started.",
)
log_format: constr(regex=r"(text|json)") = Field(
"text", title="Log Format", description="Format for logs written to a file."
)
log_max_size: ByteSize = Field(
"50MB",
title="Maximum Log File Size",
description="Maximum storage space log file may consume.",
)
cors_origins: List[StrictStr] = Field(
[],
@ -165,24 +173,6 @@ class Params(HyperglassModel):
"""
return value.format(org_name=values["org_name"])
@validator("log_file")
def validate_log_file(cls, value):
"""Set default logfile location if none is configured.
Arguments:
value {FilePath} -- Path to log file
Returns:
{Path} -- Logfile path object
"""
if value is None:
now = datetime.now()
now.isoformat
value = Path(
f'/tmp/hyperglass_{now.strftime(r"%Y%M%d_%H-%M-%S")}.log' # noqa: S108
)
return value
@validator("primary_asn")
def validate_primary_asn(cls, value):
"""Stringify primary_asn if passed as an integer.