Skip to content

Commit 030d275

Browse files
committed
Fix #59 (comment) aside from extra technical work
1 parent 7502a50 commit 030d275

5 files changed

Lines changed: 94 additions & 9 deletions

File tree

docs/explanation.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,6 @@ For a hands on explanation of these, please read the [how-to guide on how to use
128128

129129
## Further reading
130130

131-
- For how this routing model is implemented under the hood, see the [developer explanation](https://dune-daq.github.io/daqpytools/explanation).
132-
- For how to configure ERS and advanced routing in practice, see [Configuring ERS](./how-to/configure-ers.md) and [Routing messages to specific handlers](./how-to/route-messages.md).
131+
- For how this routing model is implemented under the hood, see the [developer explanation](https://dune-daq.github.io/daqpytools/dev).
132+
- The architecture reference includes diagrams which explain exactly how the routing works. See [here](https://dune-daq.github.io/daqpytools/dev/reference/architecture/).
133+
- For how to configure ERS and advanced routing in practice, see [Configuring ERS](./how-to/configure-ers.md) and [Routing messages to specific handlers](./how-to/route-messages.md).

docs_dev/explanation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,18 @@ Defined in `routing.py`, strategies answer: "What `HandlerType` values are allow
133133
**`AllowedHandlersStrategy`** is the abstract base. Implementations:
134134

135135
1. **`DefaultAllowedHandlerStrategy`**:
136+
136137
- Uses `record.handlers` if present (explicit routing metadata)
137138
- Falls back to `fallback_handlers` set if `record.handlers` is absent or None
138139

139140
2. **`ERSAllowedHandlersStrategy`**:
141+
140142
- Reads `record.ers_handlers` dict and `record.levelno` (Python log level)
141143
- Maps the level to an ERS severity variable using `level_to_ers_var`
142144
- Returns the handler set for that severity
143145

144146
3. **`StreamAwareAllowedHandlersStrategy`**:
147+
145148
- Looks at `record.stream`
146149
- If `stream == StreamType.ERS`, uses `ERSAllowedHandlersStrategy`
147150
- Otherwise uses `DefaultAllowedHandlerStrategy`
@@ -196,6 +199,12 @@ If routing isn't what you expect, debug:
196199
1. Does the record have explicit `extra["handlers"]`?
197200
2. If not, what's the fallback set?
198201

202+
203+
For a more concrete logical flow of the entire thing, with examples, please see the [architecture](./reference/architecture.md).
204+
205+
### How HandleIDFillters, the routing strategies, and the fallback handlers work together
206+
207+
199208
### Handler and Filter Registries
200209

201210
The registries live in `handlers.py` and `filters.py`:

docs_dev/reference/architecture.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ When you call `log.info("something")`, here's the actual flow:
4242
1. **Python's logging creates a `LogRecord`** with your message, severity, and any `extra` metadata
4343

4444
2. **Logger-level filters run first** (e.g., `ThrottleFilter`):
45+
4546
- If any filter returns `False`, the record stops here
4647
- It never reaches handlers
4748
- This is where global concerns like throttling happen
4849

4950
3. **Record is offered to each attached handler**
5051

5152
4. **Each handler's `HandleIDFilter` decides** whether to emit:
53+
5254
- The filter calls the routing strategy to resolve `allowed_handlers`:
5355
- If `extra["handlers"]` is present, use it
5456
- Otherwise use the fallback set
@@ -57,8 +59,87 @@ When you call `log.info("something")`, here's the actual flow:
5759
- Non-empty = emit; empty = drop the record
5860

5961
5. **Format and emit** (if the record passed the filter):
62+
6063
- The handler formats it and emits (to file, stdout, kafka, etc.)
6164

6265
This two-stage filtering is key: logger-level filters decide "should ANY handler see this?" while handler-level filters decide "should THIS handler see this?"
6366

6467
![Filtering](../img/Filter_activity.png)
68+
69+
70+
### Practical example
71+
72+
Here the practical logic flow is explained more with demonstrations.
73+
74+
Lets say we have a logger that contains both a Rich and File handler, and we explicitly define what the fallback handlers is.
75+
76+
```python
77+
from daqpytools.logging import add_handler, HandlerType, get_daq_logger
78+
log = get_daq_logger(
79+
"myapp",
80+
log_level="INFO",
81+
rich_handler=False, # deliberately don't add handlers, we'll do it manually
82+
)
83+
84+
add_handler(log,
85+
HandlerType.Rich,
86+
use_parent_handlers=True
87+
fallback_handler={HandlerType.Rich} # default behaviour
88+
)
89+
add_handler(
90+
log,
91+
HandlerType.File,
92+
use_parent_handlers=True,
93+
fallback_handler={HandlerType.File} # default behaviour
94+
)
95+
96+
```
97+
98+
And now we investigate what happens if we send a log message with `log.info("msg", extra={"handlers": HandlerType.Rich})`. Here, the log message will be sent to both the Rich and File handlers for processing. In each handler, it will check to see what the allowed handlers are (lets call it `handlers_to_check_against`). In this case, since we set it explicitly when sending the mesage, `handlers_to_check_against = HandlerType.Rich`.
99+
100+
For the Rich handler, it checks if the Rich handler is in the `handlers_to_check_against` list. In this case, it is, so it will transmit.
101+
102+
For the File handler, it will check if the File handler is in the `handlers_to_check_against` list. In this case, the file handler is _not_ in the list so it will not transmit the record.
103+
104+
105+
Using the same setup, lets investigate what happens if we send a log a log message with `log.info("msg")`. In this case, the handlers keyword is not explicitly populated.
106+
107+
In this example, the record is sent independently to the two handlers. Since the handlers keyword is not set, the logic that populates `handlers_to_check_against` will check the `fallback_handlers` that are attached to each handler.
108+
109+
In the rich handler, we have initialised it with a fallback_handler of `HandlerType.Rich`, so `handlers_to_check_against = fallback_handler = HandlerType.Rich`. The rich handler will then check if the rich handler is in the `handlers_to_check_against`, which it is. So it will transmit the record.
110+
111+
In the file handler, we have initialised it with a fallback_handler of `HandlerType.File`, so `handlers_to_check_against = fallback_handler = HandlerType.File`. The file handler will then check if the file handler is in the `handlers_to_check_against`, which it is. So it will transmit the record.
112+
113+
This sounds a bit circular, but the reasoning for this should become clear in the case where we set different fallback handlers. Consider the following setup, where the only difference is that we set the the file handler's fallback_handler to `HandlerType.Unknown`:
114+
115+
116+
```python
117+
from daqpytools.logging import add_handler, HandlerType, get_daq_logger
118+
log = get_daq_logger(
119+
"myapp",
120+
log_level="INFO",
121+
rich_handler=False, # deliberately don't add handlers, we'll do it manually
122+
)
123+
124+
add_handler(log,
125+
HandlerType.Rich,
126+
use_parent_handlers=True
127+
fallback_handler={HandlerType.Rich} # default behaviour
128+
)
129+
add_handler(
130+
log,
131+
HandlerType.File,
132+
use_parent_handlers=True,
133+
fallback_handler={HandlerType.Unknown} # Suppressing behaviour!
134+
)
135+
```
136+
137+
Transmitting `log.info("msg", extra={"handlers": HandlerType.Rich})` will yield identical results to before. However, transmitting `log.info("msg")` yields different results.
138+
139+
As before, the log record is _independently_ fed towards the two handlers. For the rich handler, the flow is identical and so it will transmit.
140+
141+
In the case of the file handler, we have initialised it with a fallback_handler of `HandlerType.Unknown`, so `handlers_to_check_against = fallback_handler = HandlerType.Unknown`. The file handler will then check if the file handler is in the `handlers_to_check_against = HandlerType.Unknown`, which it is _not_ as `HandlerType.File` is not in `HandlerType.Unknown`. So it will **suppress** the record.
142+
143+
This has a very useful suppression effect, allowing the handlers to remain dormant by default. The only way to transmit with the file handler now is only if it was explicitly called for, by `log.info("msg", extra={"handlers": HandlerType.File})`
144+
145+

docs_dev/reference/patterns.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,3 @@ log.info("skip me", extra={"skip_logging": True})
4747
```
4848

4949
---
50-
51-
## Next steps
52-
53-
- Look at existing handlers in `handlers.py` for patterns
54-
- Look at `ThrottleFilter` for a complex filter example
55-
- Check test files in `tests/logging/` for usage examples
56-
- Add your handler/filter, submit a PR!

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ theme:
2222

2323

2424
markdown_extensions:
25+
- sane_lists
2526
- admonition
2627
- pymdownx.highlight:
2728
anchor_linenums: true

0 commit comments

Comments
 (0)