Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging stage consumes custom record formatter class #47

Merged
Merged
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Rohde & Schwarz GmbH & Co. KG
Copyright (c) 2022-present Rohde & Schwarz GmbH & Co. KG

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
63 changes: 62 additions & 1 deletion docs/usage/stage_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
Sometimes, the default settings are not enough in order to forward the test information as needed. Therefore, you can set custom stage settings
in order to fit your needs.

You can set specific values for `all` stages or specific values for any used stage. In order to do so, call your test run with the `--stage-settings=YourFileName.json` parameter. The following example stage settings JSON file content:
You can set specific values for `all` stages or specific values for any used stage. In order to do so, call your test run with the `--stage-settings=YourFileName.json` parameter. It is also possible to provide a `JSON` or `YAML` string by setting the format
with a separator e.g. `--stage-settings=json:{"json": "content}` or `--stage-settings=yaml:yaml: content`

The following example stage settings JSON file content:

```json
{
Expand Down Expand Up @@ -124,6 +127,7 @@ def test_base():

Custom settings for each supported stage can be easily setup. You have to create a file with
a `.json` or `.yaml` extension and call pytest with this additional parameter `--stage-settings`.
It's also possible to inject a serialized string with the following prefix `--stage-settings json;{...}`.

The file will be validated against a schema of supported values and in case of an error, a `jsonschema.ValidationError`
will be thrown.
Expand Down Expand Up @@ -213,6 +217,63 @@ At the moment, the following values can be changed
* `session`
* `testcase`

##### Use custom RecordFormatter class

If you want to use you own custom record formatter class, you can load you own file by setting in the `logging`
the `recordFormatter` key with the following object:

```json
{
...
"logging": {
"recordFormatter": {
"className": "RecordFormatter",
"filePath": "path/to/my/record/formatter.py"
}
}
}
```

which loads the following `RecordFormatter`

```python
# path/to/my/record/formatter.py
import logging
from pytest_fluent import get_session_uid, get_test_uid

class RecordFormatter(logging.Formatter):

def format(self, record) -> typing.Any:
"""Format the specified record as text."""
return {
"date": datetime.datetime.utcfromtimestamp(record.created).isoformat(),
"session_id": get_session_uid(),
"test_id": get_test_uid(),
"level": record.levelno,
"msg": record.msg,
"class": record.module,
"method": record.funcName,
"src": record.filename,
"line": record.lineno,
"pid": record.process,
"tid": record.thread,
}
```

You can also load it as a module from e.g. the site-packages

```json
{
...
"logging": {
"recordFormatter": {
"className": "RecordFormatter",
"module": "path.to.my.record.formatter"
}
}
}
```

##### Use values from ARGV and ENV

If you want to use data provided by the command line arguments or directly from environment variables,
Expand Down
35 changes: 16 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
[project]
dependencies = [
"pytest>=7.0.0",
"msgpack",
"six",
"fluent-logger",
"jsonschema",
"ruamel.yaml",
]
name = "pytest-fluent"
authors = [
{name = "Rohde & Schwarz GmbH & Co. KG", email = "[email protected]"},
{ name = "Rohde & Schwarz GmbH & Co. KG", email = "[email protected]" },
]
maintainers = [
{name = "Carsten Sauerbrey", email = "[email protected]"},
{name = "Nicola Lambiase", email = "[email protected]"}
{ name = "Carsten Sauerbrey", email = "[email protected]" },
{ name = "Nicola Lambiase", email = "[email protected]" },
]
description = "A pytest plugin in order to provide logs via fluentd"
readme = "README.md"
keywords = ["pytest", "logging", "fluent"]
license = {text = "MIT"}
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand All @@ -26,34 +34,23 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Pytest"
"Framework :: Pytest",
]
requires-python = ">=3.8"
dependencies = [
"pytest>=7.0.0",
"msgpack",
"six",
"fluent-logger",
"jsonschema",
"ruamel.yaml",
]
dynamic = ["version"]

[project.urls]
project = "https://github.com/Rohde-Schwarz/pytest-fluent"

[project.optional-dependencies]
docs = [
"sphinx",
"sphinx-rtd-theme",
"myst-parser"
]
docs = ["sphinx", "sphinx-rtd-theme", "myst-parser"]
test = [
"pytest",
"coverage[toml]",
"pytest-cov",
"pytest-xdist[psutil]",
"six"
"six",
"importlib-resources; python_version<='3.8'",
]

[build-system]
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[metadata]
copyright = Copyright © Rohde & Schwarz GmbH & Co. KG 2022
copyright = Copyright © Rohde & Schwarz GmbH & Co. KG 2022-present
platform =
Unix
Linux
Expand Down
1 change: 1 addition & 0 deletions src/pytest_fluent/additional_information.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Set additional information function handler."""

import inspect
import typing

Expand Down
1 change: 1 addition & 0 deletions src/pytest_fluent/content_patcher.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Patch content according to settings."""

import argparse
import enum
import inspect
Expand Down
75 changes: 73 additions & 2 deletions src/pytest_fluent/data/schema.stage.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
"type": "object",
"patternProperties": {
"all|pytest_sessionfinish|pytest_sessionstart|pytest_runtest_logstart|pytest_runtest_logreport|pytest_runtest_logfinish|logging": {
"$ref": "#/definitions/AdditionalProperties"
"anyOf": [
{
"$ref": "#/definitions/AdditionalProperties"
},
{
"$ref": "#/definitions/LogFormatter"
}
]
}
},
"anyOf": [
Expand All @@ -22,7 +29,22 @@
{
"type": "object",
"patternProperties": {
"pytest_sessionfinish|pytest_sessionstart|pytest_runtest_logstart|pytest_runtest_logreport|pytest_runtest_logfinish|logging": {
"logging": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalProperties"
},
{
"$ref": "#/definitions/LogFormatter"
}
]
}
}
},
{
"type": "object",
"patternProperties": {
"pytest_sessionfinish|pytest_sessionstart|pytest_runtest_logstart|pytest_runtest_logreport|pytest_runtest_logfinish": {
"$ref": "#/definitions/AdditionalProperties"
}
}
Expand Down Expand Up @@ -74,6 +96,55 @@
}
}
}
},
"LogFormatter": {
"type": "object",
"additionalProperties": false,
"properties": {
"recordFormatter": {
"type": "object",
"additionalProperties": false,
"properties": {
"module": {
"type": "string",
"regex": "^([a-z_][a-z0-9_]*)+(\\.[a-z_][a-z0-9_]*)*$"
},
"filePath": {
"type": "string"
},
"className": {
"type": "string"
}
},
"oneOf": [
{
"module": {
"type": "string",
"regex": "^([a-z_][a-z0-9_]*)+(\\.[a-z_][a-z0-9_]*)*$"
},
"required": [
"module",
"className"
]
},
{
"filePath": {
"type": "string"
},
"required": [
"filePath",
"className"
]
}
],
"required": [
"className"
]
}
},
"required": [
"recordFormatter"
]
}
},
"required": [
Expand Down
69 changes: 69 additions & 0 deletions src/pytest_fluent/importlib_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Utility for pathlib operations."""

import importlib.util
import logging
import pathlib
import types
import typing
from importlib.machinery import ModuleSpec
from os import PathLike

LOGGER = logging.getLogger(__name__)


def extract_function_from_module_string(
class_name: str,
module: typing.Optional[str] = None,
file_path: typing.Optional[str] = None,
) -> typing.Type[logging.Formatter]:
"""Extract record formatter from module.

Args:
class_name (str): The name of the record formatter class.
module (typing.Optional[str], optional): Module name string or more descriptive
dictionary. Defaults to None.
module (typing.Optional[str], optional): Module name string or more descriptive
dictionary. Defaults to None.

Returns:
typing.Type[logging.Formatter]: The extract record formatter class.
"""
# Use importlib to import the specified module dynamically
if module:
imported_module = importlib.import_module(module)
elif file_path:
imported_module = load_module_from_path(file_path)
else:
raise ValueError("Neither module nor file_path was set.")

# Retrieve the specified function from the dynamically imported module
if hasattr(imported_module, class_name):
record_formatter = getattr(imported_module, class_name)
if not issubclass(record_formatter, logging.Formatter):
raise ValueError(
"Provided record formatter is not derived from logging.Formatter"
)
return record_formatter
raise ImportError(f"Could not find {class_name} in {imported_module.__name__}")


def load_module_from_path(path: typing.Union[str, PathLike]) -> types.ModuleType:
"""Load module from absolute path.

Args:
path: typing.Union[str, PathLike]: Path to module file

Raises:
ModuleNotFoundError: If the file path was not found.

Returns:
types.ModuleType: Loaded module type.
"""
path_name = pathlib.Path(path)
module_name = path_name.name.replace(".py", "")
spec = importlib.util.spec_from_file_location(module_name, path_name.absolute())
if spec is None:
raise ModuleNotFoundError(f"Could not load module {path_name}")
module = importlib.util.module_from_spec(spec)
typing.cast(ModuleSpec, spec).loader.exec_module(module) # type: ignore
return module
Loading
Loading