summaryrefslogtreecommitdiffstats
path: root/Log/Log4perl/Layout/Mozilla.pm
blob: 67a070c54633aaaa6c39e680b88209bd03e39e12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Log::Log4perl::Layout::Mozilla;
use 5.10.1;
use Moo;
use Sys::Hostname;
use JSON::MaybeXS ();
use English qw(-no_match_vars $PID);

use constant LOGGING_FORMAT_VERSION => 2.0;

extends 'Log::Log4perl::Layout';

has 'name' => (
    is      => 'ro',
    default => 'Bugzilla',
);

has 'max_json_length' => (
    is      => 'ro',
    isa     => sub { die "must be at least 1024\n" if $_[0] < 1024 },
    default => 4096,
);

sub BUILDARGS {
    my ($class, $params) = @_;

    delete $params->{value};
    foreach my $key (keys %$params) {
        if (ref $params->{$key} eq 'HASH') {
            $params->{$key} = $params->{$key}{value};
        }
    }
    return $params;
}

sub render {
    my ( $self, $msg, $category, $priority, $caller_level ) = @_;

    state $HOSTNAME = hostname();
    state $JSON     = JSON::MaybeXS->new(
        indent          => 0,    # to prevent newlines (and save space)
        ascii           => 1,    # to avoid encoding issues downstream
        allow_unknown   => 1,    # encode null on bad value (instead of exception)
        convert_blessed => 1,    # call TO_JSON on blessed ref, if it exists
        allow_blessed   => 1,    # encode null on blessed ref that can't be converted
    );

    my $mdc = Log::Log4perl::MDC->get_context;
    my %out = (
        EnvVersion => LOGGING_FORMAT_VERSION,
        Hostname   => $HOSTNAME,
        Logger     => $self->name,
        Pid        => $PID,
        Severity   => $Log::Log4perl::Level::SYSLOG{$priority},
        Timestamp  => time() * 1e9,
        Type       => $category,
        Fields     => { msg => $msg, %$mdc },
    );

    my $json_text = $JSON->encode(\%out) . "\n";
    if (length($json_text) > $self->max_json_length) {
        my $scary_msg = sprintf( "DANGER! LOG MESSAGE TOO BIG %d > %d", length($json_text), $self->max_json_length );
        $out{Fields}   = { remote_ip => $mdc->{remote_ip}, msg => $scary_msg };
        $out{Severity} = 1; # alert
        $json_text     = $JSON->encode(\%out) . "\n";
    }

    return $json_text;
}

1;

__END__

=head1 NAME

Log::Log4perl::Layout::Mozilla - Implement the mozilla-services json log format

=head1 SYNOPSIS

Example configuration:

    log4perl.appender.Example.layout = Log::Log4perl::Layout::Mozilla
    log4perl.appender.Example.layout.max_json_length = 16384
    log4perl.appender.Example.layout.name = Bugzilla

=head1 DESCRIPTION

This class implements a C<Log::Log4perl> layout format
that implements the recommend json format using in Mozilla services.
L<https://wiki.mozilla.org/Firefox/Services/Logging#MozLog_JSON_schema>.

The JSON hash is ASCII encoded, with no newlines or other whitespace, and is
suitable for output, via Log::Log4perl appenders, to files and syslog etc.

Contextual data in the L<Log::Log4perl::MDC> hash will be put into the Fields
hash.

=head1 LAYOUT CONFIGURATION

=head2 name

Data source, server that is doing the logging, e.g. "Sync-1_5".

Use the server's name, and avoid implementation details. "FxaAuthWebserver", not "NginxLogs".

=head2 max_json_length

Set the maximum JSON length in bytes. The default is 4096,
and it cannot be smaller than 1024.

    log4perl.appender.Example.layout.max_json_length = 16384

This is useful where some downstream system has a limit on the maximum size of
a message.

If the message is larger than this limit, the message will be replaced
with a scary message at a severity level of ALERT.