sllm

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 12, 2022 License: MIT Imports: 7 Imported by: 2

README

sllm – Structured Logging Leightweight Markup

Build Status codecov Go Report Card GoDoc

A human readable approach to make parameters from an actual log message recognizable for machines.

Disclaimer: sllm is not a loggin library. Its a concept to make log messages human- and machine-readable at the same time. There are some examples in the Go reference docs. For a Go logging lib that uses sllm see qbsllm.

Rationale

Logging is subject to two conflicting requirements. Log entries should be understandable and easy to read. On the other hand, they should be able to be reliably processed automatically, i.e. to extract relevant data from the entry. Current technologies force a decision for one of the two requirements.

To decide for either human readability or machine processing means that significant cutbacks are made to the other requirement. sllm picks up the idea of "markup", which is typically intended to bring human readability and machine processing together. At the same time, sllm remains simple and unobtrusive—unlike XML or even JSON.

Let's take the example of a standard log message that states some business relevant event and might be generated from the following pseudo-code:

…
// code that knows something about transaction:
DC.put("tx.id", tx.getId());
…
// down the stack: an actual business related message:
log.Info("added {} x {} to shopping cart by {}", 7, "Hat", "John Doe");
…
// finally(!) when climbing up the stack:
DC.clear();
…

Besides the message the log entry contains some standard fields time, thread, level and module and some Diagnostic Context information to give a more complete picture.

How would the output of such a scenario look like? – Either one gets a visually pleasant message that is rather good to read or you get output that is easy to be consumed by computers. Currently one has to make a choice. But, can't we have both in one?

Note that sllm focuses on the log message. We do not propose how to handle other pieces of information! So lets have a look what sllm would do with some decent log formats. We use ↩/↪ only to avoid very long lines on this page. In real logs things would be on one line!

Classic log output
2018-07-02 20:52:39 [main] INFO sllm.Example - added 7 x Hat to ↩
↪ shopping cart by John Doe - tx.id=4711

This message is nice to read for humans. Especially the message part is easy to understand because humans are used to gain some understanding from natural language. However, the relevant parameters— i.e. the number of items, which type of item and the respective user—is not easy to extract from the text by machines. Even if you do, simple changes of the text template can easily break the mechanism to extrac those parameters.

With sllm message:

2018-07-02 20:52:39 [main] INFO sllm.Example - added `count:7` x ↩
↪ `item:Hat` to shopping cart by `user:John Doe` - tx.id=4711

The sllm'ed format is still quite readable but lets one reliably identify the business relevant values.

logfmt
time=2018-07-02T20:52:39 thread=main level=INFO module=sllm.Example ↩
↪ ix.id=4711 number=7 item=Hat user=John_Doe tag=fill_shopping_cart

The logftm page itself states that the human readability of logftm is far from perferct and encourages the approach to include a human readable message with every log line:

time=2018-07-02T20:52:39 thread=main level=INFO module=sllm.Example ↩
↪ ix.id=4711 msg="added 7 x Hat to shopping cart by John Doe" ↩
↪ number=7 item=Hat user=John_Doe tag=fill_shopping_cart

Once you find the message by skimming the log entry its meaning is not subject of presonal interpretation of technical key/value pairs any more. That's fine! But there is still a significant amount of “visual noise”. However the result is quite acceptable. But one still may ask if the redundancy in the log entry is necessary. With a “slim” message you don't need that redundancy.

With sllm message:

time=2018-07-02T20:52:39 thread=main level=INFO module=sllm.Example ↩
↪ ix.id=4711 msg=added `count:7` x `item:Hat` to shopping cart by ↩
↪ `user:John Doe`
JSON Lines
{"time":"2018-07-02T20:52:39","thread":"main","level":"INFO", ↩
↪ "module":"sllm.Example","ix.id"="4711","number":"7","item":"Hat", ↩
↪ "user":"John Doe","tag":"fill_shopping_cart"}

Obviously, JSON is the least readable format. However, JSON has an outstanding advantage: JSON can display structured data. Structured data is deliberately avoided with sllm. Taking this path would inevitably lead to something with the complexity of XML.

However, similar to the logfmt example, you can use sllm to insert a machine-readable message into the entry.

With sllm message:

{"time":"2018-07-02T20:52:39","thread":"main","level":"INFO", ↩
↪ "module":"sllm.Example","ix.id"="4711","msg":"added `count:7` x ↩
↪ `item:Hat` to shopping cart by `user:John Doe`"}

About the Markup Rules

The markup is simple and sticks to the following requirements:

  1. No support for multi-line messages

    Spreading a single log entry over multiple lines is considered a bad practice. However there may be use cases, e.g. logging a stack trace, that justify the multi-line approach. But in any case the message of a log entry shall not exceed a single line!

  2. Message arguments are unstructured character sequences

    sllm works on the text level. There is no type system implied by sllm. As such the arguments of a sllm message are simply sub-strings of the message string.

  3. Arguments are identified by a parameter name

    Within a message each argument is identified by its parameter name. A parameter name also is a sub-strings of the message string.

  4. Reliable and robust recognition of parameters and arguments

    The argument and the parameter can be uniquely recognised within a message. Changes of a message that do not affect neither the parameters nor the arguments do not break the recognition.

  5. Be transparent, simple and unobtrusive

    A message shall be human readable so that the meaning of the message is easy to get. The system must be transparent in the sense that even the human reader can easily recognize the parameters with their arguments.

    Note that the readability of a message also depends to a certain extent on its author.

With this requirements, why was the backtick '`' chosen for markup? – The backtick is a rarely used character from the ASCII characters set, i.e. it is also compatible with UTF-8. The fact that it is rarely used implies that we don't have to escape it often. This affects backticks in the message template and the arguments. In parameter names backticks are simply not allowed.

And last but not least: Simpler markup rules make simpler software implementations (as long as it is not too simple). Besides many advantages this gives room for efficient implementations. Part of this repository is a Go implementation that does not strive so much for efficiency but for hackability.

Documentation

Overview

Package sllm is the reference implementation for the sllm log message format.

sllm is short for Structured Logging Lightweight Markup. Its goal is to provide a human readable format for the message part of log-entries that allows parameters in the log message to be reliably recognized by programs. A task generally addressed by markup languages. For sllm we want something much less obstrusive than e.g. XML or JSON. The traditional log message:

2019/01/11 19:32:44 added 7 ⨉ Hat to shopping cart by John Doe

would become something like (depending on the choice of parameter names)

2019/01/11 19:32:44 added `count:7` ⨉ `item:Hat` to shopping cart by `user:John Doe`

Still human readable but also easy to be read by machines. Also machine reading would not break even when the message template changes the order of parameters. Careful choice of parameter names can make messages even more expressive.

This package is no logging library—it provides functions to create and parse sllm messages.

Example (ValEsc_Write)
ew := valEsc{wr: os.Stdout}
ew.Write([]byte("foo"))
fmt.Fprintln(os.Stdout)
ew.Write([]byte("`bar`"))
fmt.Fprintln(os.Stdout)
ew.Write([]byte("b`az"))
fmt.Fprintln(os.Stdout)
Output:

foo
``bar``
b``az

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Error added in v0.12.0

func Error(tmpl string, a ...interface{}) error
Example
fmt.Println(Error("this is just `an` message with missing `param`", "error"))
fmt.Println(Error("this will `fail", "dummy"))
Output:

this is just `an:error` message with missing `param:<nil>`
[sllm error:syntax error in `tmpl:this will ``fail`:`pos:11`:`desc:unterminated argument`]

func Expand

func Expand(wr io.Writer, tmpl string, writeArg ParamWriter) (n int, err error)

Expand writes a message to the io.Writer wr by expanding all arguments of the given template tmpl. The actual process of expanding an argument is left to the given ParamWriter writeArg.

Example
var writeTestArg = func(wr io.Writer, idx int, name string) (int, error) {
	return fmt.Fprintf(wr, "#%d/'%s'", idx, name)
}
Expand(os.Stdout, "want `arg1` here and `arg2` here", writeTestArg)
fmt.Println()
Expand(os.Stdout, "template with backtick '``' and an `arg` here", writeTestArg)
fmt.Println()
Expand(os.Stdout, "touching args: `one``two``three`", Args([]byte("–"), 4711, true))
fmt.Println()
Expand(os.Stdout, "explicit `index:0` and `same:0`", Args(nil, 4711))
fmt.Println()
Output:

want `arg1:#0/'arg1'` here and `arg2:#1/'arg2'` here
template with backtick '``' and an `arg:#0/'arg'` here
touching args: `one:4711``two:true``three:–`
explicit `index:4711` and `same:4711`
Example (ExplicitIndex)
var writeTestArg = func(wr io.Writer, idx int, name string) (int, error) {
	return fmt.Fprint(wr, idx)
}
Expand(os.Stdout, "`a`, `b:11`, `c`, `d:0`, `e`", writeTestArg)
Output:

`a:0`, `b:11`, `c:1`, `d:0`, `e:2`

func Expands

func Expands(tmpl string, writeArg ParamWriter) (string, error)

Expands uses Expand to return the expanded temaplate as a string.

func ExtractParams

func ExtractParams(appendTo []string, tmpl string) ([]string, error)

ExtractParams extracs the parameter names from template tmpl and appends them to appendTo.

func Parse

func Parse(msg string, tmpl *bytes.Buffer, onArg func(name, value string) error) error

Parse parses a sllm message create by Expand and calls onArg for every `name:value` parameter it finds in the message. When a non-nil buffer is passed as tmpl Parse will also reconstruct the original template into the buffer. Note that the template is appended to tmpl's content.

func ParseMap

func ParseMap(msg string, tmpl *bytes.Buffer) (map[string][]string, error)

ParseMap uses Parse to create a map with all parameters assigned to an argument in the passed message msg. ParseMap can also reconstruct the template when passing a Buffer to tmpl.

Example
var tmpl bytes.Buffer
m, _ := ParseMap(
	"added `count:7` ⨉ `item:Hat` to shopping cart by `user:John Doe`",
	&tmpl,
)
fmt.Println(tmpl.String())
for k, v := range m {
	fmt.Printf("%s:[%s]\n", k, v)
}
Output:

added `count` ⨉ `item` to shopping cart by `user`
count:[[7]]
item:[[Hat]]
user:[[John Doe]]

Types

type ArgMap

type ArgMap = map[string]interface{}

type IllegalArgIndex

type IllegalArgIndex int

func (IllegalArgIndex) Error

func (err IllegalArgIndex) Error() string

type ParamWriter

type ParamWriter = func(wr io.Writer, idx int, name string) (int, error)

ParamWriter is used by the Expand function to process an argument when it appears in the expand process of a template. Expand will pass the index idx and the name of the argument to expand, i.e. write into the writer wr. A ParamWriter returns the number of bytes writen and—just in case—an error.

NOTE The writer wr of type ValueEsc will escape whatever ParamWriter

writes to wr so that the template escape symbol '`' remains
recognizable.

func Args

func Args(u []byte, av ...interface{}) ParamWriter
Example
Expand(os.Stdout, "just an `what`", Args(nil, "example"))
Output:

just an `what:example`
Example (Undef)
Expand(os.Stdout, "just an `what`: `miss`", Args([]byte("<undef>"), "example"))
Output:

just an `what:example`: `miss:<undef>`

func Map

func Map(u []byte, m ArgMap) ParamWriter
Example
Expand(os.Stdout, "just an `what`", Map(nil, ArgMap{
	"what":  "example",
	"dummy": false,
}))
Output:

just an `what:example`
Example (Undef)
Expand(os.Stdout, "just an `what`: `miss`", Map([]byte("<undef>"), ArgMap{
	"what":  "example",
	"dummy": false,
}))
Output:

just an `what:example`: `miss:<undef>`

type SyntaxError

type SyntaxError struct {
	// Tmpl is the errornous template string
	Tmpl string
	// Pas is the byte position within the template string
	Pos int
	// Err is the description of the error
	Err string
}

SyntaxError describes errors of the sllm template syntax in a message template.

func (SyntaxError) Error

func (err SyntaxError) Error() string

type UndefinedArg

type UndefinedArg string

func (UndefinedArg) Error

func (err UndefinedArg) Error() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL