# ff-logger
[](https://badge.fury.io/py/ff-logger)
[](https://pypi.org/project/ff-logger/)
[](https://opensource.org/licenses/MIT)
A scoped, instance-based logging package for Fenixflow applications. Unlike traditional Python logging which uses a global configuration, ff-logger provides self-contained logger instances that can be passed around as objects, with support for context binding and multiple output formats.
Created by **Ben Moag** at **[Fenixflow](https://fenixflow.com)**
## Quick Start
### Installation
#### From PyPI (when published)
```bash
pip install ff-logger
```
#### From GitLab (current)
```bash
pip install git+https://gitlab.com/fenixflow/fenix-packages.git#subdirectory=ff-logger
```
### Basic Usage
```python
from ff_logger import ConsoleLogger
import logging
# Create a logger instance with permanent context
logger = ConsoleLogger(
name="my_app",
level="INFO", # Can use strings now! (or logging.INFO)
context={"service": "api", "environment": "production"}
)
# Log messages with the permanent context
logger.info("Application started")
# Output: [2025-08-20 10:00:00] INFO [my_app] Application started | service="api" environment="production"
# Add runtime context with kwargs
logger.info("Request processed", request_id="req-123", duration=45)
# Output includes both permanent and runtime context
```
### Context Binding
Add permanent context fields to your logger instance:
```python
# Add context that will appear in all subsequent logs
logger.bind(
request_id="req-456",
user_id=789,
ip="192.168.1.1"
)
# All messages now include the bound context
logger.info("Processing payment")
logger.error("Payment failed", error_code="CARD_DECLINED")
# bind() returns self for chaining
logger.bind(session_id="xyz").info("Session started")
```
**Note:** As of v0.3.0, `bind()` modifies the logger instance in place rather than creating a new one. This is cleaner and more intuitive. The method validates that fields are not reserved and values are JSON-serializable.
## Logger Types
### ConsoleLogger
Outputs colored, human-readable logs to console:
```python
from ff_logger import ConsoleLogger
logger = ConsoleLogger(
name="app",
level="INFO", # String or int (logging.INFO)
colors=True, # Enable colored output
show_hostname=False # Optional hostname in logs
)
```
### JSONLogger
Outputs structured JSON lines, perfect for log aggregation:
```python
from ff_logger import JSONLogger
logger = JSONLogger(
name="app",
level="WARNING", # String or int levels supported
show_hostname=True,
include_timestamp=True
)
logger.info("Event occurred", event_type="user_login", user_id=123)
# Output: {"level":"INFO","logger":"app","message":"Event occurred","timestamp":"2025-08-20T10:00:00Z","event_type":"user_login","user_id":123,...}
```
### FileLogger
Writes to files with rotation support:
```python
from ff_logger import FileLogger
logger = FileLogger(
name="app",
filename="/var/log/app.log",
rotation_type="size", # "size", "time", or "none"
max_bytes=10*1024*1024, # 10MB
backup_count=5
)
```
### NullLogger
Zero-cost logger for testing or when logging is disabled:
```python
from ff_logger import NullLogger
# Preferred: Use directly as a class (no instantiation needed)
NullLogger.info("This does nothing") # No-op
NullLogger.debug("Debug message") # No-op
# As a default parameter (perfect for dependency injection)
def process_data(data, logger=NullLogger):
logger.info("Processing data: %s", data)
return data * 2
# Call without providing a logger
result = process_data([1, 2, 3])
# Backward compatibility: Can still instantiate if needed
logger = NullLogger() # All parameters are optional
logger.info("This also does nothing")
```
### DatabaseLogger
Writes logs to a database table (requires ff-storage):
```python
from ff_logger import DatabaseLogger
from ff_storage.db.postgres import PostgresPool
db = PostgresPool(...)
logger = DatabaseLogger(
name="app",
db_connection=db,
table_name="logs",
schema="public"
)
```
## Key Features
### v0.4.0 Features
#### Temporary Context Manager
Use the `temp_context()` context manager to add temporary fields that are automatically removed:
```python
logger = ConsoleLogger("app")
with logger.temp_context(request_id="123", user_id=456):
logger.info("Processing request") # Includes request_id and user_id
logger.info("Request complete") # Still includes the fields
# Fields automatically removed after context
logger.info("Next request") # request_id and user_id no longer present
```
#### Lazy Evaluation for Performance
Pass callables as kwargs to defer expensive computations until needed:
```python
logger = ConsoleLogger("app", level="ERROR") # Only ERROR and above
# This callable is NEVER executed (DEBUG is disabled)
logger.debug("Debug info", expensive_data=lambda: compute_expensive_data())
# This callable IS executed (ERROR is enabled)
logger.error("Error occurred", context=lambda: gather_error_context())
```
#### Robust JSON Serialization
JSON logger now handles complex Python types without crashing:
```python
from datetime import datetime
from decimal import Decimal
from uuid import uuid4
from pathlib import Path
logger = JSONLogger("app")
# All of these work automatically
logger.info("Event",
timestamp=datetime.now(), # → ISO format string
user_id=uuid4(), # → string representation
price=Decimal("19.99"), # → float
file_path=Path("/tmp/file"), # → string
status=Status.ACTIVE # → enum value
)
```
#### Thread-Safe Context Updates
All context operations are now thread-safe:
```python
logger = ConsoleLogger("app")
# Safe to call from multiple threads
def worker(worker_id):
logger.bind(worker_id=worker_id)
logger.info("Worker started")
threads = [Thread(target=worker, args=(i,)) for i in range(10)]
```
### v0.3.0 Features
#### Flexible Log Levels
Accepts both string and integer log levels for better developer experience:
- Strings: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`
- Case-insensitive: `"info"` works the same as `"INFO"`
- Integers: Traditional `logging.DEBUG`, `logging.INFO`, etc.
- Numeric values: `10`, `20`, `30`, `40`, `50`
### Instance-Based
Each logger is a self-contained instance with its own configuration:
```python
def process_data(logger):
"""Accept any logger instance."""
logger.info("Processing started")
# ... do work ...
logger.info("Processing complete")
# Use with different loggers
console = ConsoleLogger("console")
json_log = JSONLogger("json")
process_data(console) # Outputs to console
process_data(json_log) # Outputs as JSON
```
### Context Preservation
Permanent context fields appear in every log message:
```python
logger = ConsoleLogger(
name="worker",
context={
"worker_id": "w-1",
"datacenter": "us-east-1"
}
)
# Every log includes worker_id and datacenter
logger.info("Task started")
logger.error("Task failed")
```
### Zero Dependencies
Built entirely on Python's standard `logging` module - no external dependencies required for core functionality.
## Migration from Traditional Logging
```python
# Traditional Python logging (global)
import logging
logging.info("Message")
# ff-logger (instance-based)
from ff_logger import ConsoleLogger
logger = ConsoleLogger("app")
logger.info("Message")
```
## Advanced Usage
### Flexible Log Levels
```python
# All of these work now (v0.3.0+):
logger1 = ConsoleLogger("app", level="DEBUG") # String
logger2 = ConsoleLogger("app", level="info") # Case-insensitive
logger3 = ConsoleLogger("app", level=logging.INFO) # Traditional int
logger4 = ConsoleLogger("app", level=20) # Numeric value
logger5 = ConsoleLogger("app") # Default: "DEBUG"
# Supported string levels:
# "DEBUG", "INFO", "WARNING"/"WARN", "ERROR", "CRITICAL"
```
### Exception Logging
```python
try:
risky_operation()
except Exception:
logger.exception("Operation failed")
# Automatically includes full traceback
```
### Reserved Fields
Python's logging module reserves 23+ field names for LogRecord internals. If you use these as context fields in log calls, they're automatically prefixed with `x_` to prevent conflicts:
```python
# Constructor 'name' parameter - this works as expected
logger = ConsoleLogger("my_app") # ✅ Sets logger name
# Log method 'name' kwarg - automatically prefixed to avoid conflict
logger.info("Message", name="custom") # Becomes x_name="custom"
# Other reserved fields also prefixed
logger.info("Event",
module="auth", # Becomes x_module="auth"
process="worker", # Becomes x_process="worker"
thread="t-1" # Becomes x_thread="t-1"
)
```
**Reserved fields include:** `name`, `module`, `pathname`, `funcName`, `process`, `thread`, `levelname`, `msg`, `args`, and 15+ more. See [Python logging documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes) for the complete list.
**Why?** These fields are used internally by Python's LogRecord class. Overwriting them would cause crashes like "Attempt to overwrite 'name' in LogRecord".
## Testing
Use `NullLogger` in tests for zero overhead:
```python
def test_my_function():
# Option 1: Pass the class directly
result = my_function(logger=NullLogger) # No logging output
assert result == expected
# Option 2: Functions with NullLogger as default
def my_function(data, logger=NullLogger):
logger.info("Processing: %s", data)
return process(data)
# In tests, just call without logger parameter
result = my_function(test_data) # Silent by default
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request to the [GitLab repository](https://gitlab.com/fenixflow/fenix-packages).
## License
MIT License - see [LICENSE](LICENSE) file for details.
Copyright (c) 2024 Ben Moag / Fenixflow
Raw data
{
"_id": null,
"home_page": null,
"name": "ff-logger",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": "Fenixflow Team <dev@fenixflow.com>",
"keywords": "logging, structured-logging, json-logging, scoped-logger, instance-logger, fenixflow",
"author": null,
"author_email": "Ben Moag <dev@fenixflow.com>",
"download_url": "https://files.pythonhosted.org/packages/0e/1a/9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2/ff_logger-0.4.1.tar.gz",
"platform": null,
"description": "# ff-logger\n\n[](https://badge.fury.io/py/ff-logger)\n[](https://pypi.org/project/ff-logger/)\n[](https://opensource.org/licenses/MIT)\n\nA scoped, instance-based logging package for Fenixflow applications. Unlike traditional Python logging which uses a global configuration, ff-logger provides self-contained logger instances that can be passed around as objects, with support for context binding and multiple output formats.\n\nCreated by **Ben Moag** at **[Fenixflow](https://fenixflow.com)**\n\n## Quick Start\n\n### Installation\n\n#### From PyPI (when published)\n```bash\npip install ff-logger\n```\n\n#### From GitLab (current)\n```bash\npip install git+https://gitlab.com/fenixflow/fenix-packages.git#subdirectory=ff-logger\n```\n\n### Basic Usage\n\n```python\nfrom ff_logger import ConsoleLogger\nimport logging\n\n# Create a logger instance with permanent context\nlogger = ConsoleLogger(\n name=\"my_app\",\n level=\"INFO\", # Can use strings now! (or logging.INFO)\n context={\"service\": \"api\", \"environment\": \"production\"}\n)\n\n# Log messages with the permanent context\nlogger.info(\"Application started\")\n# Output: [2025-08-20 10:00:00] INFO [my_app] Application started | service=\"api\" environment=\"production\"\n\n# Add runtime context with kwargs\nlogger.info(\"Request processed\", request_id=\"req-123\", duration=45)\n# Output includes both permanent and runtime context\n```\n\n### Context Binding\n\nAdd permanent context fields to your logger instance:\n\n```python\n# Add context that will appear in all subsequent logs\nlogger.bind(\n request_id=\"req-456\",\n user_id=789,\n ip=\"192.168.1.1\"\n)\n\n# All messages now include the bound context\nlogger.info(\"Processing payment\")\nlogger.error(\"Payment failed\", error_code=\"CARD_DECLINED\")\n\n# bind() returns self for chaining\nlogger.bind(session_id=\"xyz\").info(\"Session started\")\n```\n\n**Note:** As of v0.3.0, `bind()` modifies the logger instance in place rather than creating a new one. This is cleaner and more intuitive. The method validates that fields are not reserved and values are JSON-serializable.\n\n## Logger Types\n\n### ConsoleLogger\nOutputs colored, human-readable logs to console:\n\n```python\nfrom ff_logger import ConsoleLogger\n\nlogger = ConsoleLogger(\n name=\"app\",\n level=\"INFO\", # String or int (logging.INFO)\n colors=True, # Enable colored output\n show_hostname=False # Optional hostname in logs\n)\n```\n\n### JSONLogger\nOutputs structured JSON lines, perfect for log aggregation:\n\n```python\nfrom ff_logger import JSONLogger\n\nlogger = JSONLogger(\n name=\"app\",\n level=\"WARNING\", # String or int levels supported\n show_hostname=True,\n include_timestamp=True\n)\n\nlogger.info(\"Event occurred\", event_type=\"user_login\", user_id=123)\n# Output: {\"level\":\"INFO\",\"logger\":\"app\",\"message\":\"Event occurred\",\"timestamp\":\"2025-08-20T10:00:00Z\",\"event_type\":\"user_login\",\"user_id\":123,...}\n```\n\n### FileLogger\nWrites to files with rotation support:\n\n```python\nfrom ff_logger import FileLogger\n\nlogger = FileLogger(\n name=\"app\",\n filename=\"/var/log/app.log\",\n rotation_type=\"size\", # \"size\", \"time\", or \"none\"\n max_bytes=10*1024*1024, # 10MB\n backup_count=5\n)\n```\n\n### NullLogger\nZero-cost logger for testing or when logging is disabled:\n\n```python\nfrom ff_logger import NullLogger\n\n# Preferred: Use directly as a class (no instantiation needed)\nNullLogger.info(\"This does nothing\") # No-op\nNullLogger.debug(\"Debug message\") # No-op\n\n# As a default parameter (perfect for dependency injection)\ndef process_data(data, logger=NullLogger):\n logger.info(\"Processing data: %s\", data)\n return data * 2\n\n# Call without providing a logger\nresult = process_data([1, 2, 3])\n\n# Backward compatibility: Can still instantiate if needed\nlogger = NullLogger() # All parameters are optional\nlogger.info(\"This also does nothing\")\n```\n\n### DatabaseLogger\nWrites logs to a database table (requires ff-storage):\n\n```python\nfrom ff_logger import DatabaseLogger\nfrom ff_storage.db.postgres import PostgresPool\n\ndb = PostgresPool(...)\nlogger = DatabaseLogger(\n name=\"app\",\n db_connection=db,\n table_name=\"logs\",\n schema=\"public\"\n)\n```\n\n## Key Features\n\n### v0.4.0 Features\n\n#### Temporary Context Manager\nUse the `temp_context()` context manager to add temporary fields that are automatically removed:\n\n```python\nlogger = ConsoleLogger(\"app\")\n\nwith logger.temp_context(request_id=\"123\", user_id=456):\n logger.info(\"Processing request\") # Includes request_id and user_id\n logger.info(\"Request complete\") # Still includes the fields\n\n# Fields automatically removed after context\nlogger.info(\"Next request\") # request_id and user_id no longer present\n```\n\n#### Lazy Evaluation for Performance\nPass callables as kwargs to defer expensive computations until needed:\n\n```python\nlogger = ConsoleLogger(\"app\", level=\"ERROR\") # Only ERROR and above\n\n# This callable is NEVER executed (DEBUG is disabled)\nlogger.debug(\"Debug info\", expensive_data=lambda: compute_expensive_data())\n\n# This callable IS executed (ERROR is enabled)\nlogger.error(\"Error occurred\", context=lambda: gather_error_context())\n```\n\n#### Robust JSON Serialization\nJSON logger now handles complex Python types without crashing:\n\n```python\nfrom datetime import datetime\nfrom decimal import Decimal\nfrom uuid import uuid4\nfrom pathlib import Path\n\nlogger = JSONLogger(\"app\")\n\n# All of these work automatically\nlogger.info(\"Event\",\n timestamp=datetime.now(), # \u2192 ISO format string\n user_id=uuid4(), # \u2192 string representation\n price=Decimal(\"19.99\"), # \u2192 float\n file_path=Path(\"/tmp/file\"), # \u2192 string\n status=Status.ACTIVE # \u2192 enum value\n)\n```\n\n#### Thread-Safe Context Updates\nAll context operations are now thread-safe:\n\n```python\nlogger = ConsoleLogger(\"app\")\n\n# Safe to call from multiple threads\ndef worker(worker_id):\n logger.bind(worker_id=worker_id)\n logger.info(\"Worker started\")\n\nthreads = [Thread(target=worker, args=(i,)) for i in range(10)]\n```\n\n### v0.3.0 Features\n\n#### Flexible Log Levels\nAccepts both string and integer log levels for better developer experience:\n- Strings: `\"DEBUG\"`, `\"INFO\"`, `\"WARNING\"`, `\"ERROR\"`, `\"CRITICAL\"`\n- Case-insensitive: `\"info\"` works the same as `\"INFO\"`\n- Integers: Traditional `logging.DEBUG`, `logging.INFO`, etc.\n- Numeric values: `10`, `20`, `30`, `40`, `50`\n\n### Instance-Based\nEach logger is a self-contained instance with its own configuration:\n\n```python\ndef process_data(logger):\n \"\"\"Accept any logger instance.\"\"\"\n logger.info(\"Processing started\")\n # ... do work ...\n logger.info(\"Processing complete\")\n\n# Use with different loggers\nconsole = ConsoleLogger(\"console\")\njson_log = JSONLogger(\"json\")\n\nprocess_data(console) # Outputs to console\nprocess_data(json_log) # Outputs as JSON\n```\n\n### Context Preservation\nPermanent context fields appear in every log message:\n\n```python\nlogger = ConsoleLogger(\n name=\"worker\",\n context={\n \"worker_id\": \"w-1\",\n \"datacenter\": \"us-east-1\"\n }\n)\n\n# Every log includes worker_id and datacenter\nlogger.info(\"Task started\")\nlogger.error(\"Task failed\")\n```\n\n### Zero Dependencies\nBuilt entirely on Python's standard `logging` module - no external dependencies required for core functionality.\n\n## Migration from Traditional Logging\n\n```python\n# Traditional Python logging (global)\nimport logging\nlogging.info(\"Message\")\n\n# ff-logger (instance-based)\nfrom ff_logger import ConsoleLogger\nlogger = ConsoleLogger(\"app\")\nlogger.info(\"Message\")\n```\n\n## Advanced Usage\n\n### Flexible Log Levels\n\n```python\n# All of these work now (v0.3.0+):\nlogger1 = ConsoleLogger(\"app\", level=\"DEBUG\") # String\nlogger2 = ConsoleLogger(\"app\", level=\"info\") # Case-insensitive\nlogger3 = ConsoleLogger(\"app\", level=logging.INFO) # Traditional int\nlogger4 = ConsoleLogger(\"app\", level=20) # Numeric value\nlogger5 = ConsoleLogger(\"app\") # Default: \"DEBUG\"\n\n# Supported string levels:\n# \"DEBUG\", \"INFO\", \"WARNING\"/\"WARN\", \"ERROR\", \"CRITICAL\"\n```\n\n### Exception Logging\n\n```python\ntry:\n risky_operation()\nexcept Exception:\n logger.exception(\"Operation failed\")\n # Automatically includes full traceback\n```\n\n### Reserved Fields\n\nPython's logging module reserves 23+ field names for LogRecord internals. If you use these as context fields in log calls, they're automatically prefixed with `x_` to prevent conflicts:\n\n```python\n# Constructor 'name' parameter - this works as expected\nlogger = ConsoleLogger(\"my_app\") # \u2705 Sets logger name\n\n# Log method 'name' kwarg - automatically prefixed to avoid conflict\nlogger.info(\"Message\", name=\"custom\") # Becomes x_name=\"custom\"\n\n# Other reserved fields also prefixed\nlogger.info(\"Event\",\n module=\"auth\", # Becomes x_module=\"auth\"\n process=\"worker\", # Becomes x_process=\"worker\"\n thread=\"t-1\" # Becomes x_thread=\"t-1\"\n)\n```\n\n**Reserved fields include:** `name`, `module`, `pathname`, `funcName`, `process`, `thread`, `levelname`, `msg`, `args`, and 15+ more. See [Python logging documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes) for the complete list.\n\n**Why?** These fields are used internally by Python's LogRecord class. Overwriting them would cause crashes like \"Attempt to overwrite 'name' in LogRecord\".\n\n## Testing\n\nUse `NullLogger` in tests for zero overhead:\n\n```python\ndef test_my_function():\n # Option 1: Pass the class directly\n result = my_function(logger=NullLogger) # No logging output\n assert result == expected\n \n # Option 2: Functions with NullLogger as default\n def my_function(data, logger=NullLogger):\n logger.info(\"Processing: %s\", data)\n return process(data)\n \n # In tests, just call without logger parameter\n result = my_function(test_data) # Silent by default\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request to the [GitLab repository](https://gitlab.com/fenixflow/fenix-packages).\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\nCopyright (c) 2024 Ben Moag / Fenixflow\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Fenixflow structured logging package with scoped, instance-based loggers",
"version": "0.4.1",
"project_urls": {
"Bug Tracker": "https://gitlab.com/fenixflow/fenix-packages/-/issues",
"Changelog": "https://gitlab.com/fenixflow/fenix-packages/-/blob/main/ff-logger/CHANGELOG.md",
"Documentation": "https://gitlab.com/fenixflow/fenix-packages/-/tree/main/ff-logger",
"Homepage": "https://fenixflow.com",
"Repository": "https://gitlab.com/fenixflow/fenix-packages"
},
"split_keywords": [
"logging",
" structured-logging",
" json-logging",
" scoped-logger",
" instance-logger",
" fenixflow"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "5bf7cc28219fa6f557de7c4c0977a1c48000762f21069f72d95fa5e73c3ab5f1",
"md5": "2914ac3b89e77ea28755315f7c5f0449",
"sha256": "c0b1b1c992aced3ab6c95a2dc1cb3f0d5f00d2019f717a8b799dd136276b88c9"
},
"downloads": -1,
"filename": "ff_logger-0.4.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "2914ac3b89e77ea28755315f7c5f0449",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 20067,
"upload_time": "2025-10-06T19:41:01",
"upload_time_iso_8601": "2025-10-06T19:41:01.210142Z",
"url": "https://files.pythonhosted.org/packages/5b/f7/cc28219fa6f557de7c4c0977a1c48000762f21069f72d95fa5e73c3ab5f1/ff_logger-0.4.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "0e1a9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2",
"md5": "f7a58768b39b31223af17d78c7ee35f4",
"sha256": "43af1b4ee9d77325a645099de654ac39644fc4815e43eeba6f17bac6f9342d5c"
},
"downloads": -1,
"filename": "ff_logger-0.4.1.tar.gz",
"has_sig": false,
"md5_digest": "f7a58768b39b31223af17d78c7ee35f4",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 29220,
"upload_time": "2025-10-06T19:41:02",
"upload_time_iso_8601": "2025-10-06T19:41:02.164212Z",
"url": "https://files.pythonhosted.org/packages/0e/1a/9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2/ff_logger-0.4.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-06 19:41:02",
"github": false,
"gitlab": true,
"bitbucket": false,
"codeberg": false,
"gitlab_user": "fenixflow",
"gitlab_project": "fenix-packages",
"lcname": "ff-logger"
}