Logchain
Python Logging based on blockchain
Logs get chained
The current log line contains the signature of the previous line with your secret.
- detect lines deleted / lost
- detect logs tampering
Philosophy
The package is intended to be a lightweight util for generating incorruptible logs.
For this pupose we rely as much as possible on standard packages: few dependencies, high quality.
The formatters are easy extensible by simply deriving from Basic
.
Usage
Install
pip install logchain
Choose your log type
Many types of logs are supported out-of-the-box:
-
Basic
raw text, relying on the standard formatter -
Json
structured log lines with static & dynamic fields -
CSV
work in progress
You can write a custom formatter in 20-ish lines.
Init once in main
from logchain import LogChainer
# Initialize a default chainer.
theLogger = LogChainer()
# Register the formatter to the logger.
theLogger.initLogging()
Have a look at the comprehensive guide of constructor parameters.
Use everywhere with python logging module
import logging
logging.debug("My message")
logging.info("Some information")
Check your logs integrity afterwards
from logchain import LogChainer
aLogChain = [
"2020-03-30 13:38:00.782|0ec90b9839fdd964|TestChaining.py:20 test_logging_happy_case hello gg",
"2020-03-30 13:38:00.782|2e3f1b4a7b946fb1|TestChaining.py:21 test_logging_happy_case voila1",
"2020-03-30 13:38:00.782|10d1ab606618492a|TestChaining.py:22 test_logging_happy_case voila2",
"2020-03-30 13:38:00.782|805757e144f4e385|TestChaining.py:23 test_logging_happy_case voila5",
"2020-03-30 13:38:00.782|3bda90b5af77d3fe|TestChaining.py:24 test_logging_happy_case voila4"
]
result = LogChainer.verify(aLogChain)
if not result:
print("Last good line", result.prevLine)
print("First bad line", result.line)
else:
print("All right")
Constructor parameters
They are passed as a dict and/or named arguments.
from logchain import LogChainer
theLogger = LogChainer(verbosity = 3, secret = "mySignatureKey")
params = {"verbosity": 3, "secret": "mySignatureKey"}
theLogger = LogChainer(params, timestampFmt = "iso")
Param | Default value | Description |
---|---|---|
formatterCls | formatters.Basic | Type of logging to perform, raw text, json, custom |
format | see below | Placeholder string used by raw-text loggers |
secret | secrets.token_urlsafe(128) | Signature key to compute the line signature |
seed | secrets.token_urlsafe() | Random string to sign into the first log line |
timestampFmt | "iso" | iso for 8601 or strftime compatible placeholders |
timestampPrecision | "milliseconds" |
timespec element used by the datetime library
|
timestampUtc | False | Transform the timestamp to its value in UTC |
stream | cout | Where the logs are sent, file/console/custom stream |
verbosity | 0 | Number [0..5] mapped to a logging.level |
The default format is %(timestamp)s %(levelLetters)s %(fileLine)-15s %(funcName)-15s %(message)-60s |%(signature)s
. It relies on some extra fields like the signature at its end.
Logchain extra logging fields
We enrich the standard logging record with some handy fields:
Name | Description |
---|---|
fileLine | Widespread filename:lineno
|
levelLetters | 4 first letters of logging level names: short and unambiguous |
signature | The digital signature of the previous line. Include it in all your lines to benefit from the chaining |
timestamp | Improved version of asctime , see below |
The timestamp
field offers more flexibility than asctime
in regards to:
- the precision; can go up to the micro seconds (
msecs
cannot) - the decimal separator; you choose, '.' by default
- utc or local timezone
- customize the format only in one place:
timestampFmt
Dynamic logging fields
The package is suitable for server/app logging which context changes from one transaction to another. Here is an example of setting contextual information throughout the lifecycle of an app:
App.py
class App:
def __init__(self, appName, logger):
self.logger = logger
self.logger.setField(appName = appName)
logging.info("Creating the app")
def handleTransaction(self, userId, callback):
with self.logger.managedField(uId = userId, trxId = secrets.token_urlsafe(8)):
callback()
def close(self):
logging.info("Closing the app")
Callbacks.py
# The log chain in transparent for the callbacks
def callback1():
logging.warn("Something happened")
def callback2():
logging.info("Serving a resource")
main.py
def main():
theLogger = logchain.LogChainer(formatterCls = logchain.formatters.Json)
theLogger.initLogging()
app = App("MyApp", theLogger)
app.handleTransaction("user1", callback1)
app.handleTransaction("user1", callback2)
app.close()
You can either use:
-
setField
: set a permanant field, remove it by setting it toNone
. -
managedField
: set a temporary field for the scope of thecontext manager
.
Contributing
Install
The code is hosted on Gitlab
Simply clone and submit pull requests.
Testing
The unit tests are located in the test folder
which contains the __main__.py
entrypoint.
# Run all
python test
# Get additional options
python test --help
Delivery
Use to the awesome Poetry tool for this purpose:
poetry build
poetry publish