Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 110 additions & 70 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "socketdock"
version = "0.1.0a0"
version = "0.1.1a0"
description = "Websocket relay service for use with clustered mediators"
authors = [
"Sam Curren <[email protected]>",
Expand All @@ -14,6 +14,7 @@ readme = "README.md"
python = "^3.8.1"
aiohttp = "^3.9.4"
sanic = "^22.12.0"
pyyaml = "^6.0.2"

[tool.poetry.group.dev.dependencies]
websocket = "^0.2.1"
Expand Down
27 changes: 25 additions & 2 deletions socketdock/__main__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
"""Run the SocketDock server."""

import logging
import argparse
from sanic import Sanic

from .api import api, backend_var
from .loadlogger import LoggingConfigurator


def configure_logging(args):
"""Perform common app configuration."""
# Set up logging
log_config = args.log_config
log_level = args.log_level

try:
LoggingConfigurator.configure(
log_config_path=log_config,
log_level=log_level,
)

except Exception as e:
raise Exception("Logger configuration failed: ", e)


def config() -> argparse.Namespace:
Expand All @@ -21,9 +37,16 @@ def config() -> argparse.Namespace:
parser.add_argument("--connect-uri")
parser.add_argument(
"--log-level",
dest="log_level",
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
parser.add_argument(
"--log-config",
dest="log_config",
default="/usr/src/app/socketdock/config/logging-config.yml",
help="Specifies a custom logging configuration file",
)

return parser.parse_args()

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

backend_var.set(backend)

logging.basicConfig(level=args.log_level)
configure_logging(args)

app = Sanic("SocketDock")
app.config.WEBSOCKET_MAX_SIZE = 2**22
Expand Down
38 changes: 38 additions & 0 deletions socketdock/config/jsonLog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Custom json logging."""

# Python3 logging custom formatter.
# For more information, please visit: https://docs.python.org/3/library/logging.html
import json
import logging
import socket
import uuid
from datetime import datetime

hostname = socket.gethostname()


class JsonFormatter(logging.Formatter):
"""JsonFormatter Class."""

def format(self, record):
"""Format class. Used to do custom json formatting."""
# Interpolates record message properly
record.msg = super().format(record)

jsonLog = {
"timestamp": datetime.fromtimestamp(record.created).strftime(
"%Y-%m-%dT%H:%M:%S.%f"
)[:-3]
+ "Z", # Only 3 Milliseconds
"level": record.levelname,
"logId": str(uuid.uuid4()),
"service": "socketdock",
"hostname": hostname,
"pid": record.process,
"file": record.filename,
"function": record.funcName,
"lineNumber": record.lineno,
"message": record.msg,
}

return json.dumps(jsonLog)
23 changes: 23 additions & 0 deletions socketdock/config/logging-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Python3 logging configuration.
# For more information, please visit: https://docs.python.org/3/library/logging.html
version: 1
disable_existing_loggers: False

formatters:
jsonFormatter:
(): jsonLog.JsonFormatter
handlers:
stdout:
class: logging.StreamHandler
level: DEBUG
formatter: jsonFormatter
stream: ext://sys.stdout
loggers:
jsonLogger:
level: DEBUG
handlers: [stdout]
propagate: no

root:
level: DEBUG
handlers: [stdout]
94 changes: 94 additions & 0 deletions socketdock/loadlogger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Logging Configurator for tails server."""

import io
import logging
from importlib import resources
from logging.config import dictConfigClass
import os
import sys
from typing import Optional

import yaml

sys.path.insert(1, os.path.realpath(os.path.dirname(__file__)) + "/config")

LOGGER = logging.getLogger(__name__)


def load_resource(path: str, encoding: Optional[str] = None):
"""Open a resource file located in a python package or the local filesystem.

Args:
path (str): The resource path in the form of `dir/file` or `package:dir/file`
encoding (str, optional): The encoding to use when reading the resource file.
Defaults to None.

Returns:
file-like object: A file-like object representing the resource
"""
components = path.rsplit(":", 1)
try:
if len(components) == 1:
# Local filesystem resource
return open(components[0], encoding=encoding)
else:
# Package resource
package, resource = components
bstream = resources.files(package).joinpath(resource).open("rb")
if encoding:
return io.TextIOWrapper(bstream, encoding=encoding)
return bstream
except IOError:
LOGGER.warning(f"Resource not found: {path}")
return None


def dictConfig(config, new_file_path=None):
"""Custom dictConfig, https://github.com/python/cpython/blob/main/Lib/logging/config.py."""
if new_file_path:
config["handlers"]["rotating_file"]["filename"] = f"{new_file_path}"
dictConfigClass(config).configure()


class LoggingConfigurator:
"""Utility class used to configure logging and print an informative start banner."""

@classmethod
def configure(
cls, log_config_path: Optional[str] = None, log_level: Optional[str] = None
):
"""Configure logger.

:param logging_config_path: str: (Default value = None) Optional path to
custom logging config

:param log_level: str: (Default value = None)
"""
cls._configure_logging(log_config_path=log_config_path, log_level=log_level)

@classmethod
def _configure_logging(cls, log_config_path, log_level):
# Setup log config and log file if provided
cls._setup_log_config_file(log_config_path)

# Set custom log level
if log_level:
logging.root.setLevel(log_level.upper())

@classmethod
def _setup_log_config_file(cls, log_config_path):
log_config, is_dict_config = cls._load_log_config(log_config_path)

# Setup config
if not log_config:
logging.basicConfig(level=logging.WARNING)
logging.root.warning(f"Logging config file not found: {log_config_path}")
elif is_dict_config:
dictConfig(log_config)

@classmethod
def _load_log_config(cls, log_config_path):
if ".yml" in log_config_path or ".yaml" in log_config_path:
with open(log_config_path, "r") as stream:
return yaml.safe_load(stream), True
return load_resource(log_config_path, "utf-8"), False