Streaming logs
A log is written at the end and read from start to finish. In STXT each entry is a root node, and the file is processed entry by entry, without loading it whole.
A log file
Log (com.acme.log): Start of user creation
Time: 2026-09-20T10:15:02.120Z
Level: INFO
Request ID: 3e45bad6-3a82-4959-844e-9eefd4c418a3
Message >>
Creating the user:
- Name:
- Age: 19
Log (com.acme.log): Error creating user
Time: 2026-09-20T10:15:02.480Z
Level: ERROR
Request ID: 3e45bad6-3a82-4959-844e-9eefd4c418a3
Message: Cannot invoke "String.length()" because "name" is null
Stacktrace >>
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at com.example.service.UserService.validateName(UserService.java:42)
at com.example.service.UserService.createUser(UserService.java:27)
at com.example.controller.UserController.register(UserController.java:58)
at com.example.App.main(App.java:15)
Log (com.acme.log): Request rejected
Time: 2026-09-20T10:15:02.495Z
Level: WARN
Request ID: 3e45bad6-3a82-4959-844e-9eefd4c418a3
Message: 400 Bad RequestIn this example we have:
- Entries: each
Logis a root node, independent of the others. The namespace is not inherited between root nodes, which is why each entry declares its own. - Data:
Time,LevelandRequest ID. They are nodes with a value, which a program can filter. - Text:
MessageandStacktrace. In a block node, quotes,:and line breaks are written as they are, with no escape characters. - Writing: adding an entry is writing at the end of the file. There is no closing to maintain, and no separators between entries.
The template
Template (@stxt.template): com.acme.log
Structure >>
Log:
Time: (1) TIMESTAMP
Level: (1) ENUM [DEBUG, INFO, WARN, ERROR]
Request ID: (?) UUID
Message: (1) TEXT
Stacktrace: (?) TEXTAn entry that does not validate
# ERROR: this document does not validate
Log (com.acme.log): Error creating user
# A TIMESTAMP is ISO 8601: `2026-09-20T10:15:02Z`
Time: 20/09/2026 10:15
# Lowercase `error` is not `ERROR`
Level: error
# Not a UUID
Request ID: 3e45bad6
# `Message` is missing, and it is requiredReading in streaming
The parser delivers each root node when it is complete and already validated, and then releases it.
The memory in use is that of one entry, not that of the file.
This program shows the entries of level ERROR:
from stxt import Parser, StreamObserver, UnifiedSchemaProvider, SchemaValidator
class Errors(StreamObserver):
def on_root_node(self, log):
if log.get_child("Level").get_text() == "ERROR":
print(log.get_child("Time").get_text(), log.get_text())
def on_error(self, error):
print("line", error.line, error.code)
provider = UnifiedSchemaProvider()
with open(".stxt/log.stxt", encoding="utf-8") as f:
provider.add_file(f.read())
parser = Parser(max_input_size=-1)
parser.register_validator(SchemaValidator(provider))
parser.register_stream_observer(Errors())
with open("app.stxt", encoding="utf-8", newline="\n") as f:
parser.parse_stream(f)
2026-09-20T10:15:02.480Z Error creating user
- An invalid entry does not stop the read: its errors arrive through
on_error, and the other entries keep being delivered. - The size of the input is limited by default to 10,000,000 characters
(STXT-SPEC §11.2). A log may exceed it, which is why the example
disables the limit with
max_input_size=-1. - TypeScript and Java have the same API:
parseStreamandStreamObserver. stxt validatealso reads in streaming:stxt validate app.stxt --max-input-size -1validates a file of any size (The command line).
A truncated file
If the process dies halfway through a write, the file ends in an incomplete entry:
Log (com.acme.log): Request rejected
Time: 2026-09-20T10:15:02.495Z
Lev
The errors stay in that entry: an INVALID_LINE on the truncated line, and a
TOO_FEW_CHILDREN for each required node that is missing. The previous entries are read all the same.
Writing entries
An entry is built as a tree, and NodeWriter writes it.
In this example the message comes from outside, and tries to forge an entry:
from stxt import InlineNode, NodeWriter
log = InlineNode("Log", "com.acme.log", "Login failed")
log.add_inline_node("Time", "2026-09-20T10:16:40Z")
log.add_inline_node("Level", "WARN")
log.add_text_node("Message", "User: ana\nLog (com.acme.log): Login succeeded\n\tLevel: INFO")
with open("app.stxt", "a", encoding="utf-8", newline="\n") as f:
f.write(NodeWriter.to_stxt(log))
And this is what ends up in the file:
Log (com.acme.log): Login failed
Time: 2026-09-20T10:16:40Z
Level: WARN
Message >>
User: ana
Log (com.acme.log): Login succeeded
Level: INFO- The text of a block node is written indented under its node, and inside a block nothing
is interpreted. The forged entry is text of
Message. - An inline value with a line break is rejected with
LINE_BREAK_NOT_ALLOWED.
Limits
- An entry takes several lines.
grep ERRORfinds the line, not the entry: filtering by entries takes a program like the one above. - A file takes more space than with a one-line-per-entry format.