Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,62 @@ and `record` structure from Logurich 0.9.
}
```

`Table`, `Panel`, `Tree`, `Syntax`, `Markdown`, `Rule`, `Group` and `Columns`
have dedicated shapes; `Padding`, `Align` and `Constrain` are unwrapped. Any
other Rich object degrades to `{"type": "text", "text": ...}` and any
non-renderable value to `{"type": "object", "repr": ...}`. Strings passed to
`Table`, `Panel`, `Tree`, `Syntax`, `Markdown`, `Rule`, `Layout`, `Group` and
`Columns` have dedicated shapes; `Padding`, `Align` and `Constrain` are
unwrapped. Any other Rich object degrades to `{"type": "text", "text": ...}` and
any non-renderable value to `{"type": "object", "repr": ...}`. Strings passed to
`logger.rich()` are not structured: they stay in `text`. Nesting is capped at
four levels and tables at 100 rows, with a `"truncated": true` marker.

The same converter is public, so tools that build reports from Rich objects can
reuse it instead of re-rendering:

```python
from logurich import SCHEMA_VERSION, serialize_renderables

payload = serialize_renderables((table,), max_rows=None, styles=True)
```

`max_depth` and `max_rows` relax the caps (`max_rows=None` keeps every row), and
`styles=True` switches to fidelity mode: text values become
`{"text": ..., "spans": [...]}` objects instead of plain strings, where each
span carries `start`, `end`, `style` and an optional `link`. Fidelity mode also
adds `justify`, `no_wrap` and per-column `style` to tables, `border_style` and
alignments to panels, and `align`/`style` to rules. Log output always uses the
default mode, so enabling styles never changes what handlers emit.

`SCHEMA_VERSION` identifies the payload contract. New keys may be added within a
version; existing keys are never renamed or removed.

## Premarkup

Premarkup tags transform text *before* Rich parses styling markup. Unknown tags
are left untouched, so Rich still handles them:

```python
from logurich import process_premarkup_to_text

process_premarkup_to_text("[defang]http://evil.test/a[/defang]")
# Text: http[:]//evil[.]test/a
```

Three actions ship built in: `defang` (neutralise URLs, domains and e-mails),
`color-obs` (highlight observables) and `truncate-url` (shorten long URLs).
Tags may combine actions, which then run in priority order:
`[truncate-url defang]...[/truncate-url defang]`.

Register your own with `register_premarkup(name, handler, priority=...)`; lower
priorities run first. `unregister_premarkup()` removes one and
`premarkup_actions()` lists them in execution order.

Premarkup is a standalone utility: it is never applied automatically to log
records, so it costs nothing on the logging path. `process_premarkup()` returns
a markup string that must be handed to `Text.from_markup` for its escapes to
resolve; `process_premarkup_to_text()` does that for you and passes non-string
inputs through unchanged. Inputs longer than `MAX_PREMARKUP_INPUT` are returned
as-is, and untrusted content should go through `rich.markup.escape` first, since
the output is markup.

## Rich objects

`logger.rich(level, *renderables, title="", prefix=True, end="\n", width=None,
Expand Down
3 changes: 2 additions & 1 deletion examples/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ def create_rich_table() -> Table:
Panel("Rich panel content", border_style="green"),
create_rich_table(),
title="Structured output",
width=60,
)

with logger.contextualize(app=logger.ctx("example", style="yellow")):
logger.info("This log has app context")
logger.info("This [blue]log[/blue] has app context")

logger.info(
"This log has per-call context",
Expand Down
38 changes: 38 additions & 0 deletions examples/premarkup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from logurich import (
console,
get_logger,
init_logger,
premarkup_actions,
process_premarkup_to_text,
register_premarkup,
unregister_premarkup,
)


def redact(text: str) -> str:
return "".join("*" if char.isdigit() else char for char in text)


if __name__ == "__main__":
init_logger("INFO", enqueue=False)
logger = get_logger(__name__)

console.rule("Built-in premarkup actions")
samples = [
"[defang]Reach out to http://evil.test/a or mail@evil.test[/defang]",
"[truncate-url]Fetched https://example.test/very/long/path/report?id=7#x[/truncate-url]",
"[color-obs]Observed evil.test during triage[/color-obs]",
"[truncate-url defang]https://evil.test/a/b/c/d/e/f[/truncate-url defang]",
"[bold]Unknown tags such as [nope]this[/nope] reach Rich untouched[/bold]",
]
for sample in samples:
console.print(process_premarkup_to_text(sample))

console.rule("Custom action")
register_premarkup("redact", redact, priority=5)
console.print(process_premarkup_to_text("[redact]Ticket 12345 closed[/redact]"))
names = ", ".join(action.name for action in premarkup_actions())
console.print(f"Actions in execution order: {names}")
unregister_premarkup("redact")

logger.info("Premarkup demo complete", samples=len(samples))
10 changes: 9 additions & 1 deletion examples/serialize.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json

from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich.tree import Tree

from logurich import ctx, get_logger, global_context, init_logger
from logurich import ctx, get_logger, global_context, init_logger, serialize_renderables


def build_table() -> Table:
Expand Down Expand Up @@ -54,3 +56,9 @@ def build_table() -> Table:
raise RuntimeError("serialize example failure")
except RuntimeError:
logger.exception("Exception payload")

# The converter behind record.renderables is public and reusable directly.
linked = Table(title="Fidelity mode")
linked.add_column("Report", justify="center", style="cyan")
linked.add_row("[link=https://example.test/42]open[/link]")
print(json.dumps(serialize_renderables((linked,), styles=True), indent=2))
19 changes: 19 additions & 0 deletions src/logurich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
init_logger,
shutdown_logger,
)
from .premarkup import (
MAX_PREMARKUP_INPUT,
PremarkupAction,
premarkup_actions,
process_premarkup,
process_premarkup_to_text,
register_premarkup,
unregister_premarkup,
)
from .serialize import SCHEMA_VERSION, serialize_renderables
from .user_input import timeout, user_input, user_input_with_timeout

__all__ = [
Expand Down Expand Up @@ -66,4 +76,13 @@
"timeout",
"user_input",
"user_input_with_timeout",
"SCHEMA_VERSION",
"serialize_renderables",
"MAX_PREMARKUP_INPUT",
"PremarkupAction",
"premarkup_actions",
"process_premarkup",
"process_premarkup_to_text",
"register_premarkup",
"unregister_premarkup",
]
Loading