Skip to content

Commit f4da09e

Browse files
authored
Merge pull request #3 from Indicio-tech/feat/json-logging
Feat/json logging
2 parents aa3bbf6 + a7f1a37 commit f4da09e

File tree

6 files changed

+292
-73
lines changed

6 files changed

+292
-73
lines changed

poetry.lock

Lines changed: 110 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "socketdock"
3-
version = "0.1.0a0"
3+
version = "0.1.1a0"
44
description = "Websocket relay service for use with clustered mediators"
55
authors = [
66
"Sam Curren <[email protected]>",
@@ -14,6 +14,7 @@ readme = "README.md"
1414
python = "^3.8.1"
1515
aiohttp = "^3.9.4"
1616
sanic = "^22.12.0"
17+
pyyaml = "^6.0.2"
1718

1819
[tool.poetry.group.dev.dependencies]
1920
websocket = "^0.2.1"

socketdock/__main__.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
"""Run the SocketDock server."""
22

3-
import logging
43
import argparse
54
from sanic import Sanic
65

76
from .api import api, backend_var
7+
from .loadlogger import LoggingConfigurator
8+
9+
10+
def configure_logging(args):
11+
"""Perform common app configuration."""
12+
# Set up logging
13+
log_config = args.log_config
14+
log_level = args.log_level
15+
16+
try:
17+
LoggingConfigurator.configure(
18+
log_config_path=log_config,
19+
log_level=log_level,
20+
)
21+
22+
except Exception as e:
23+
raise Exception("Logger configuration failed: ", e)
824

925

1026
def config() -> argparse.Namespace:
@@ -21,9 +37,16 @@ def config() -> argparse.Namespace:
2137
parser.add_argument("--connect-uri")
2238
parser.add_argument(
2339
"--log-level",
40+
dest="log_level",
2441
default="INFO",
2542
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
2643
)
44+
parser.add_argument(
45+
"--log-config",
46+
dest="log_config",
47+
default="/usr/src/app/socketdock/config/logging-config.yml",
48+
help="Specifies a custom logging configuration file",
49+
)
2750

2851
return parser.parse_args()
2952

@@ -46,7 +69,7 @@ def main():
4669

4770
backend_var.set(backend)
4871

49-
logging.basicConfig(level=args.log_level)
72+
configure_logging(args)
5073

5174
app = Sanic("SocketDock")
5275
app.config.WEBSOCKET_MAX_SIZE = 2**22

socketdock/config/jsonLog.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Custom json logging."""
2+
3+
# Python3 logging custom formatter.
4+
# For more information, please visit: https://docs.python.org/3/library/logging.html
5+
import json
6+
import logging
7+
import socket
8+
import uuid
9+
from datetime import datetime
10+
11+
hostname = socket.gethostname()
12+
13+
14+
class JsonFormatter(logging.Formatter):
15+
"""JsonFormatter Class."""
16+
17+
def format(self, record):
18+
"""Format class. Used to do custom json formatting."""
19+
# Interpolates record message properly
20+
record.msg = super().format(record)
21+
22+
jsonLog = {
23+
"timestamp": datetime.fromtimestamp(record.created).strftime(
24+
"%Y-%m-%dT%H:%M:%S.%f"
25+
)[:-3]
26+
+ "Z", # Only 3 Milliseconds
27+
"level": record.levelname,
28+
"logId": str(uuid.uuid4()),
29+
"service": "socketdock",
30+
"hostname": hostname,
31+
"pid": record.process,
32+
"file": record.filename,
33+
"function": record.funcName,
34+
"lineNumber": record.lineno,
35+
"message": record.msg,
36+
}
37+
38+
return json.dumps(jsonLog)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Python3 logging configuration.
2+
# For more information, please visit: https://docs.python.org/3/library/logging.html
3+
version: 1
4+
disable_existing_loggers: False
5+
6+
formatters:
7+
jsonFormatter:
8+
(): jsonLog.JsonFormatter
9+
handlers:
10+
stdout:
11+
class: logging.StreamHandler
12+
level: DEBUG
13+
formatter: jsonFormatter
14+
stream: ext://sys.stdout
15+
loggers:
16+
jsonLogger:
17+
level: DEBUG
18+
handlers: [stdout]
19+
propagate: no
20+
21+
root:
22+
level: DEBUG
23+
handlers: [stdout]

socketdock/loadlogger.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Logging Configurator for tails server."""
2+
3+
import io
4+
import logging
5+
from importlib import resources
6+
from logging.config import dictConfigClass
7+
import os
8+
import sys
9+
from typing import Optional
10+
11+
import yaml
12+
13+
sys.path.insert(1, os.path.realpath(os.path.dirname(__file__)) + "/config")
14+
15+
LOGGER = logging.getLogger(__name__)
16+
17+
18+
def load_resource(path: str, encoding: Optional[str] = None):
19+
"""Open a resource file located in a python package or the local filesystem.
20+
21+
Args:
22+
path (str): The resource path in the form of `dir/file` or `package:dir/file`
23+
encoding (str, optional): The encoding to use when reading the resource file.
24+
Defaults to None.
25+
26+
Returns:
27+
file-like object: A file-like object representing the resource
28+
"""
29+
components = path.rsplit(":", 1)
30+
try:
31+
if len(components) == 1:
32+
# Local filesystem resource
33+
return open(components[0], encoding=encoding)
34+
else:
35+
# Package resource
36+
package, resource = components
37+
bstream = resources.files(package).joinpath(resource).open("rb")
38+
if encoding:
39+
return io.TextIOWrapper(bstream, encoding=encoding)
40+
return bstream
41+
except IOError:
42+
LOGGER.warning(f"Resource not found: {path}")
43+
return None
44+
45+
46+
def dictConfig(config, new_file_path=None):
47+
"""Custom dictConfig, https://github.com/python/cpython/blob/main/Lib/logging/config.py."""
48+
if new_file_path:
49+
config["handlers"]["rotating_file"]["filename"] = f"{new_file_path}"
50+
dictConfigClass(config).configure()
51+
52+
53+
class LoggingConfigurator:
54+
"""Utility class used to configure logging and print an informative start banner."""
55+
56+
@classmethod
57+
def configure(
58+
cls, log_config_path: Optional[str] = None, log_level: 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+
cls._configure_logging(log_config_path=log_config_path, log_level=log_level)
68+
69+
@classmethod
70+
def _configure_logging(cls, log_config_path, log_level):
71+
# Setup log config and log file if provided
72+
cls._setup_log_config_file(log_config_path)
73+
74+
# Set custom log level
75+
if log_level:
76+
logging.root.setLevel(log_level.upper())
77+
78+
@classmethod
79+
def _setup_log_config_file(cls, log_config_path):
80+
log_config, is_dict_config = cls._load_log_config(log_config_path)
81+
82+
# Setup config
83+
if not log_config:
84+
logging.basicConfig(level=logging.WARNING)
85+
logging.root.warning(f"Logging config file not found: {log_config_path}")
86+
elif is_dict_config:
87+
dictConfig(log_config)
88+
89+
@classmethod
90+
def _load_log_config(cls, log_config_path):
91+
if ".yml" in log_config_path or ".yaml" in log_config_path:
92+
with open(log_config_path, "r") as stream:
93+
return yaml.safe_load(stream), True
94+
return load_resource(log_config_path, "utf-8"), False

0 commit comments

Comments
 (0)