Skip to content

Commit f4a6f5d

Browse files
authored
Merge pull request #2 from Indicio-tech/feat/json-logging
Feat/json logging
2 parents aa3bbf6 + 4c737d4 commit f4a6f5d

4 files changed

Lines changed: 192 additions & 1 deletion

File tree

logging/config.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: 1
2+
formatters:
3+
jsonFormatter:
4+
(): jsonLog.JsonFormatter
5+
handlers:
6+
stdout:
7+
class: logging.StreamHandler
8+
level: DEBUG
9+
formatter: jsonFormatter
10+
stream: ext://sys.stdout
11+
loggers:
12+
jsonLogger:
13+
level: DEBUG
14+
handlers: [stdout]
15+
propagate: no
16+
root:
17+
level: DEBUG
18+
handlers: [stdout]

logging/jsonLog.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import json, logging
2+
import socket
3+
import uuid
4+
from datetime import datetime
5+
6+
hostname = socket.gethostname()
7+
8+
class JsonFormatter(logging.Formatter):
9+
10+
def format(self, record):
11+
jsonLog = {
12+
"timestamp": datetime.fromtimestamp(record.created).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', # Only 3 Milliseconds
13+
# "time": datetime.fromtimestamp(record.created).isoformat(),
14+
"level": record.levelname,
15+
"logId": str(uuid.uuid4()),
16+
"service": "postdock",
17+
"hostname": hostname,
18+
"pid": record.process,
19+
"file": record.filename,
20+
"function": record.funcName,
21+
"lineNumber": record.lineno,
22+
"message": record.msg,
23+
}
24+
25+
return json.dumps(jsonLog)
26+

socketdock/__main__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
from sanic import Sanic
66

77
from .api import api, backend_var
8+
from .loadlogger import LoggingConfigurator
9+
10+
11+
def configure_logging(args):
12+
"""Perform common app configuration."""
13+
# Set up logging
14+
log_config = args.log_config
15+
log_level = args.log_level
16+
log_file = args.log_file
17+
LoggingConfigurator.configure(
18+
log_config_path=log_config,
19+
log_level=log_level,
20+
log_file=log_file,
21+
)
822

923

1024
def config() -> argparse.Namespace:
@@ -21,9 +35,27 @@ def config() -> argparse.Namespace:
2135
parser.add_argument("--connect-uri")
2236
parser.add_argument(
2337
"--log-level",
38+
dest="log_level",
2439
default="INFO",
2540
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
2641
)
42+
parser.add_argument(
43+
"--log-file",
44+
dest="log_file",
45+
default=None,
46+
help=(
47+
"--log-file enables writing of logs to file, if a value is "
48+
"provided then it uses that as log file location, otherwise "
49+
"the default location in log config file is used."
50+
),
51+
)
52+
parser.add_argument(
53+
"--log-config",
54+
dest="log_config",
55+
default=None,
56+
help="Specifies a custom logging configuration file",
57+
)
58+
2759

2860
return parser.parse_args()
2961

@@ -46,7 +78,7 @@ def main():
4678

4779
backend_var.set(backend)
4880

49-
logging.basicConfig(level=args.log_level)
81+
configure_logging(args)
5082

5183
app = Sanic("SocketDock")
5284
app.config.WEBSOCKET_MAX_SIZE = 2**22

socketdock/loadlogger.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Logging Configurator for aca-py agent."""
2+
3+
import io
4+
import logging
5+
from importlib import resources
6+
from logging.config import (
7+
dictConfigClass,
8+
)
9+
from typing import Optional
10+
11+
import yaml
12+
13+
LOGGER = logging.getLogger(__name__)
14+
15+
def load_resource(path: str, encoding: Optional[str] = None):
16+
"""Open a resource file located in a python package or the local filesystem.
17+
18+
Args:
19+
path (str): The resource path in the form of `dir/file` or `package:dir/file`
20+
encoding (str, optional): The encoding to use when reading the resource file.
21+
Defaults to None.
22+
23+
Returns:
24+
file-like object: A file-like object representing the resource
25+
"""
26+
components = path.rsplit(":", 1)
27+
try:
28+
if len(components) == 1:
29+
# Local filesystem resource
30+
return open(components[0], encoding=encoding)
31+
else:
32+
# Package resource
33+
package, resource = components
34+
bstream = resources.files(package).joinpath(resource).open("rb")
35+
if encoding:
36+
return io.TextIOWrapper(bstream, encoding=encoding)
37+
return bstream
38+
except IOError:
39+
LOGGER.warning("Resource not found: %s", path)
40+
return None
41+
42+
43+
def dictConfig(config, new_file_path=None):
44+
"""Custom dictConfig, https://github.com/python/cpython/blob/main/Lib/logging/config.py."""
45+
if new_file_path:
46+
config["handlers"]["rotating_file"]["filename"] = f"{new_file_path}"
47+
dictConfigClass(config).configure()
48+
49+
50+
class LoggingConfigurator:
51+
"""Utility class used to configure logging and print an informative start banner."""
52+
53+
@classmethod
54+
def configure(
55+
cls,
56+
log_config_path: Optional[str] = None,
57+
log_level: Optional[str] = None,
58+
log_file: Optional[str] = None,
59+
):
60+
"""Configure logger.
61+
62+
:param logging_config_path: str: (Default value = None) Optional path to
63+
custom logging config
64+
65+
:param log_level: str: (Default value = None)
66+
67+
:param log_file: str: (Default value = None) Optional file name to write logs to
68+
"""
69+
70+
write_to_log_file = log_file is not None or log_file == ""
71+
72+
# This is a check that requires a log file path to be provided if
73+
# --log-file is specified on startup and a config file is not.
74+
if not log_config_path and write_to_log_file and not log_file:
75+
raise ValueError(
76+
"log_file (--log-file) must be provided in single-tenant mode "
77+
"using the default config since a log file path is not set."
78+
)
79+
80+
cls._configure_logging(
81+
log_config_path=log_config_path,
82+
log_level=log_level,
83+
log_file=log_file,
84+
)
85+
86+
@classmethod
87+
def _configure_logging(cls, log_config_path, log_level, log_file):
88+
# Setup log config and log file if provided
89+
cls._setup_log_config_file(log_config_path, log_file)
90+
91+
# Set custom file handler
92+
if log_file:
93+
logging.root.handlers.append(logging.FileHandler(log_file, encoding="utf-8"))
94+
95+
# Set custom log level
96+
if log_level:
97+
logging.root.setLevel(log_level.upper())
98+
99+
@classmethod
100+
def _setup_log_config_file(cls, log_config_path, log_file):
101+
log_config, is_dict_config = cls._load_log_config(log_config_path)
102+
103+
# Setup config
104+
if not log_config:
105+
logging.basicConfig(level=logging.WARNING)
106+
logging.root.warning(f"Logging config file not found: {log_config_path}")
107+
elif is_dict_config:
108+
dictConfig(log_config, new_file_path=log_file or None)
109+
110+
@classmethod
111+
def _load_log_config(cls, log_config_path):
112+
if ".yml" in log_config_path or ".yaml" in log_config_path:
113+
with open(log_config_path, "r") as stream:
114+
return yaml.safe_load(stream), True
115+
return load_resource(log_config_path, "utf-8"), False

0 commit comments

Comments
 (0)