Skip to content

Commit 7682500

Browse files
authored
Merge pull request #500 from fynnsu/general_logging
Add general logging implementation
2 parents e8eb284 + e283c9a commit 7682500

File tree

11 files changed

+1232
-151
lines changed

11 files changed

+1232
-151
lines changed

docs/logging.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Logging
2+
3+
## Brief overview of logging in python stdlib
4+
5+
The python stdlib logging module consists of 5 components:
6+
7+
1. Logger
8+
2. Handler
9+
3. Filter
10+
4. Formatter
11+
5. LogRecord
12+
13+
### Logger
14+
15+
The logger is the root of the logging system. A logger object can be created by calling the `logging.getLogger("logger_name")` function (calling this function with the same name will return the same logger object).
16+
17+
Loggers have a heirarchical structure such that a logger with name `"a"` is the parent of a logger with name `"a.b"`. It is common practice to give loggers a name that is the module name of the file that the logger is defined in. (e.g. `"instructlab.training.logger"`). All loggers are children of the root logger (specified by the empty string `""`).
18+
19+
Once a logger is created, it can be used to log messages by calling its `debug`/`info`/`warning`/`error`/`critical` methods, which create log records with the appropriate log level. Logs that pass the logger's log level and filters are then passed to the logger's handlers. If the logger's `propagate` attribute is `True`, the log record is also passed to the logger's parent (and so on up the hierarchy).
20+
21+
### Handler
22+
23+
A handler is an object that can process log messages. Each logger can have multiple handlers. Handlers can have their own log level and filters. Handlers accept `LogRecord` objects and can do things like write to a file, send an email, or push to an external API.
24+
25+
### Filter
26+
27+
A filter is an object that can be used to filter log messages. They can be set on loggers or handlers. Another use of filters is to modify the log message or its context before it is sent to a handler.
28+
29+
### Formatter
30+
31+
A formatter is an object that can be used to format log messages. It can be set on a handler. There can be only one formatter per handler, which is why filters are sometimes used to preprocess the log message before it is formatted.
32+
33+
### LogRecord
34+
35+
A LogRecord is the object that is created when a log message is made. It contains all the information about the log message, including the message, file, line number, log level, logger name, thread name, process name, and timestamp. The log message is the value that is passed to the logger's `debug`/`info`/`warning`/`error`/`critical` methods.
36+
37+
```python
38+
import logging
39+
40+
logger = logging.getLogger("logger_name")
41+
42+
logger.debug("This is a debug message")
43+
# We can also pass arbitrary objects to the logger and they will be set as the `LogRecord.msg` attribute.
44+
logger.info({"value": 1})
45+
logger.info({"lr": 0.001, "epochs": 10}, extra={"hparams": True})
46+
```
47+
48+
Most handlers will stringify the `LogRecord.msg` value in order to print it, however, we can create handlers that use the value directly. Values set in the `extra` dictionary will become attributes of the `LogRecord` object, so it is important that the keys do not conflict with existing attributes.
49+
50+
## Logging in InstructLab
51+
52+
In InstructLab, we've added support for logging with the stdlib logging module.
53+
54+
### Logging to metric backends
55+
56+
We've created special handlers `AsyncStructuredHandler`, `TensorBoardHandler`, and `WandbHandler` that accept `LogRecord` objects with dictionaries in the `msg` attribute. They can then log metrics to their respective systems.
57+
58+
They also will check for a `step` attribute in the `extra` argument of the logger methods, and if found, pass it to the system. Alternatively, a `hparams=True` value can be passed through the `extra` dictionary to indicate that the log message should be treated as hyperparameters.
59+
60+
Throughout the codebase, metric values are logged to the `"instructlab.training.metrics"` logger. By configuring this logger with `AsyncStructuredHandler`, `TensorBoardHandler`, and `WandbHandler` handlers, logs can be sent to any of the backend systems (or multiple systems at once).
61+
62+
### Filtering log messages
63+
64+
We also provide the `IsMappingFilter` and `IsRank0Filter` which can be used to filter logs for the above handlers.
65+
66+
Since the above handlers expect dictionary messages, we recommend using the `IsMappingFilter` to ensure that only log messages with dictionaries are processed. In addition, the `IsRank0Filter` can be used to ensure that only the main process logs to the backend systems, which may be necessary for some of the handlers to prevent duplicate logs.
67+
68+
Additionally, we provide the `FormatDictFilter` which is not a true filter, but rather formats a dictionary message into a string for prettier logging. This filter can be used to format dictionary messages before they are passed to stdlib or third party library handlers that expect string messages.
69+
70+
### Setup calling InstructLab as a library
71+
72+
If you are using `instructlab.training` as a library, you may wish to enable logging in your application, and propagate logs from the `instructlab.training` package to your logging system.
73+
74+
This can be done by calling `instructlab.training.logger.propagate_package_logs(True)` in your application, and configuring the stdlib root logger to output to your logging system.
75+
76+
### Setup for training with InstructLab directly
77+
78+
If you are training with `instructlab.training` directly, you likely don't need to do anything beyond passing in the `run_name`, `logger_type`, and `output_dir` arguments when calling the script. Internally, the training script will call `setup_root_logger()` and `setup_metric_logger()` with a reasonable default configuration.
79+
80+
```bash
81+
python src/instructlab/training/main_ds.py \
82+
... \
83+
--run_name "my_run" \
84+
--logger_type "async,tensorboard,wandb" \
85+
--output_dir "out/"
86+
```

src/instructlab/training/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"DistributedBackend",
1313
)
1414

15+
# First Party
16+
import instructlab.training.logger # Disable package logging by default
17+
1518
# Local
1619
from .config import (
1720
DataProcessArgs,

src/instructlab/training/async_logger.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22

33
# Standard
44
from datetime import datetime
5+
from pathlib import Path
56
import asyncio
67
import json
78
import threading
89

910
# Third Party
1011
import aiofiles
12+
import aiofiles.os
1113

1214

1315
class AsyncStructuredLogger:
1416
def __init__(self, file_name="training_log.jsonl"):
1517
self.file_name = file_name
1618
self.logs = []
1719
self.loop = asyncio.new_event_loop()
20+
self._first_log = True
1821
t = threading.Thread(
1922
target=self._run_event_loop, args=(self.loop,), daemon=True
2023
)
@@ -27,14 +30,11 @@ def _run_event_loop(self, loop):
2730

2831
async def _initialize_log_file(self):
2932
self.logs = []
30-
try:
33+
if aiofiles.path.exists(self.file_name):
3134
async with aiofiles.open(self.file_name, "r") as f:
3235
async for line in f:
3336
if line.strip(): # Avoid empty lines
3437
self.logs.append(json.loads(line.strip()))
35-
except FileNotFoundError:
36-
# File does not exist but the first log will create it.
37-
pass
3838

3939
async def log(self, data):
4040
"""logs a dictionary as a new line in a jsonl file with a timestamp"""
@@ -43,10 +43,13 @@ async def log(self, data):
4343
data["timestamp"] = datetime.now().isoformat()
4444
self.logs.append(data)
4545
await self._write_logs_to_file(data)
46-
print(f"\033[92m{json.dumps(data, indent=4)}\033[0m")
4746

4847
async def _write_logs_to_file(self, data):
4948
"""appends to the log instead of writing the whole log each time"""
49+
if self._first_log:
50+
await aiofiles.os.makedirs(Path(self.file_name).parent, exist_ok=True)
51+
self._first_log = False
52+
5053
async with aiofiles.open(self.file_name, "a") as f:
5154
await f.write(json.dumps(data, indent=None) + "\n")
5255

0 commit comments

Comments
 (0)