NAME
Log::Any::Adapter::JSONLines - One-line JSON logging of arbitrary structured data in JSON Lines format.
VERSION
version 0.001
SYNOPSIS
# Print to STDOUT:
use Log::Any::Adapter( 'JSONLines' );
# Print to a filehandle:
use Log::Any::Adapter( 'JSONLines', file => \*STDERR, );
# Print to a file, define logging level and JSON encoding, and
# sort JSON properties alphabetically:
use Log::Any::Adapter( 'JSONLines',
file => 'out.json',
log_level => 'fatal',
encoding => 'UTF-8',
canonical => 1,
);
# JSONLines uses hooks:
use Log::Any::Adapter ('JSONLines', hooks => {
before => [ \&add_pid, ],
});
sub add_pid {
my ($level, $category, $data) = @_;
$data->{pid} = $$;
return;
}
DESCRIPTION
You want to write the log messages as JSON according to the JSON Lines text file format.
This Log::Any adapter logs formatted messages and arbitrary structured data in a single line of JSON per entry. By default, it writes to STDOUT. You can also write to any file or open filehandle.
By default we write all log levels starting from trace, but you can set a higher level if needed.
It works with Log::Any Context data as well as any other structured data.
You can "hook into" the printing process right before the JSON is created if you need to add or remove properties or alter properties, for example, mask values.
REQUIREMENTS
Perl Version
Required perl: 5.8.1 or above. Same as Log::Any.
This module uses JSON (version 4.10 at the point of writing this document). JSON uses JSON::XS as its backend by default, and when not available, it falls back on JSON::PP, which has been in the Perl core since 5.14.
Module JSON can use other backends. Please see the module docs for more information.
Dependencies
- JSON
- Log::Any::Adapter::Util which is part of Log::Any.
USAGE
The simplest way to get JSON logs:
use Log::Any::Adapter( 'JSONLines' );
By default Log::Any::Adapter::JSONLines writes to STDOUT and finishes every full JSON object with a linefeed, '\n'. By default, it sets the log level to TRACE, the lowest level. By default, it sets the category to "all" so it will record logging messages from every producer.
Place the example above into the script or module which is producing logs or using other modules which do.
Please see below for explanation of the parameters.
Hooks
A situation can arise when you would like to modify the JSON right before it gets printed.
You may pass a hooks parameter to the constructor. It should be a hashref with keys before and proxy, which should be an arrayref of coderefs. Each coderef will be called with the level, category, and a hashref representing the log entry. The coderef may modify the log entry hashref in place. These will be executed in the order they are written.
The proxy hooks are executed in the logging proxy, class Log::Any::Proxy or its child class.
The hooks have different arguments. First three are same: level, category and data (the log entry). The hook proxy has an additional parameter: info. This contains two keys: calling_sub and proxy. proxy is a pointer to the Proxy class in which the hook is being executed and calling_sub is the name of the subroutine in which the hook is being executed. calling_sub is either trace|debug|info|fatal|... or their "f" (formatting) equivalent.
use Log::Any::Adapter ('JSONLines', hooks => {
before => [ \&add_pid, \&shorten ],
proxy => [ \&add_location ],
});
sub add_pid {
my ($level, $category, $data) = @_;
$data->{pid} = $$;
return;
}
sub shorten {
my ($level, $category, $data) = @_;
$data->{msg} = delete $data->{message};
return;
}
sub add_location {
my ($level, $category, $data, $info) = @_;
my $frames = substr( $info->{calling_sub}, -1, 1 ) eq 'f' ? 2 : 1;
$data->{file} = (caller $frames)[1];
$data->{line} = (caller $frames)[2];
$data->{file} =~ s/\/home\/mikkoi\/tmp\/[\w-]+/\[..\]/gmsx;
}
Logging
You are most likely to use structured logging like this:
use Log::Any qw( $log );
$log->debug('Create account', { nr=>'12345', user=>'Smith'});
Or, if you use logging context, like this:
use Log::Any qw( $log );
$log->context->{user} = 'Smith';
$log->debug('Create account', { nr=>'12345' });
Or like this:
use Log::Any qw( $log );
$log->context->{user} = 'Smith';
$log->context->{nr} = '12345';
$log->debug('Create account');
All three would produce the same JSON (subject to changing sorting order):
{"message":"Create account","nr":"12345","user":"Smith"}
A common logging command consists of a plain text string and a possible hash.
Caveats
If there is more than two items provided to this Log::Any::Adapter, we will do things differently:
use Log::Any qw( $log );
$log->debug('Create account', 'New Account', { nr=>'12345'}, {user=>'Smith'});
The produced JSON contains property messages instead of message:
{"messages":["Create account","New Account",{"nr":"12345"},{"user":"Smith"}]}
You also cannot use the formatting functions, e.g. debugf:
$log->debugf('Create account: %s', '12345', {user=>'Smith'});
This is due to Log::Any standard proxy Log::Any::Proxy using sprintf function which discards extra attributes. Log::Any (sprintf) cannot tell that the remaining arguments are, in fact, additional structures. If you turn on all warnings, Perl will show a warning about redundant argument in sprintf.
However, Log::Any context will work as expected.
Other special cases are:
use Log::Any qw( $log );
$log->debug([ 1, 2, 3, 4, 5 ]);
$log->debug(sub { return "Lastname, Firstname"; });
$log->debug("Person:", sub { return "Lastname, Firstname"; });
$log->context->{user} = 'Smith';
$log->context->{nr} = '12345';
$log->debug({user=>'Johnson'});
Results with:
{"messages":[1,2,3,4,5]}
{"message":"Lastname, Firstname"}
{"messages":["Person:","Lastname, Firstname"]}
{"nr":"12345","user":"Johnson"}
PARAMETERS
file
Stream or open filehandle or a file path. E.g. \*STDERR, out.json.
Default: \*STDOUT
log_level
Set log_level. Allowed values: digits 0-8, level name constants in upper and lower case.
Default: trace
canonical
Do you want the JSON properties alphabetically sorted? Set this to "1". Be warned! Sorting is a time consuming operation. If you mostly pump the logs somewhere else for analysis, sorting is unnecessary.
If you need this on terminal during development or debugging, you might find logviewer.pl in examples/ useful.
Default: 0
encoding
Set encoding of the out file.
Default: UTF-8
EXAMPLES
# Use hooks to modify the JSON:
use Log::Any::Adapter( 'JSONLines',
hooks => {
before => [ \&prepare_json, \&mask_card_number, ]
},
);
sub prepare_json {
my ($level, $category, $log_entry) = @_;
$log_entry->{epoch} = time;
$log_entry->{lvl} = $level;
$log_entry->{cat} = $category;
$log_entry->{msg} = delete $log_entry->{message};
return;
}
sub mask_card_number {
my ($level, $category, $log_entry) = @_;
my $last_nums = ($log_entry->{card} =~ m/^[0-9]{12}([0-9]{4})$/msx)[0];
$log_entry->{card} = q{XXXX XXXX XXXX } . $last_nums;
return;
}
Please see the directory examples for more examples. It also contains file logviewer.pl as an example of how to make the JSON logs readable in the terminal.
STATUS
This module is currently being developed so changes in the API are possible.
THANKS
Big thanks to Log::Any::Adapter::JSON for being an inspiration and example for this module.
SEE ALSO
Log::Any::Adapter::JSON is similar to this module but not oriented towards the special case of JSON Lines text file format.
AUTHOR
Mikko Koivunalho <mikkoi@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Mikko Koivunalho.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.