package Plack::Middleware::AccessLog::Structured; { $Plack::Middleware::AccessLog::Structured::VERSION = '0.001000'; } use parent qw(Plack::Middleware); # ABSTRACT: Access log middleware which creates structured log messages use strict; use warnings; use Carp; use MRO::Compat; use Time::HiRes; use Plack::Util::Accessor qw(logger callback extra_field); use Net::Domain qw(hostname hostfqdn); use DateTime; use Time::HiRes; use JSON; sub new { my ($class, $arg_ref) = @_; my $self = $class->next::method($arg_ref); if (defined $self->callback() && ref $self->callback() ne 'CODE') { croak("Passed 'callback' parameter must be a code reference"); } if (defined $self->extra_field() && ref $self->extra_field() ne 'HASH') { croak("Passed 'extra_field' parameter must be a hash reference"); } return $self; } sub call { my ($self, $env) = @_; my $t_before = Time::HiRes::time(); my $res = $self->app->($env); my $t_after = Time::HiRes::time(); return $self->response_cb($res, sub { my ($cb_res) = @_; my $h = Plack::Util::headers($res->[1]); my $content_type = $h->get('Content-Type'); my $log_entry = { class => ref($self), # Request data remote_addr => $env->{REMOTE_ADDR}, request_method => _safe($env->{REQUEST_METHOD}), request_uri => _safe($env->{REQUEST_URI}), server_protocol => $env->{SERVER_PROTOCOL}, http_user_agent => _safe($env->{HTTP_USER_AGENT}), http_host => $env->{HTTP_HOST} || $env->{SERVER_NAME}, http_referer => $env->{HTTP_REFERER}, remote_user => $env->{REMOTE_USER}, # Server information pid => $$, hostfqdn => hostfqdn(), hostname => hostname(), # Response data response_status => $cb_res->[0], content_length => Plack::Util::content_length($res->[2]) || $h->get('Content-Length'), content_type => defined $content_type ? "$content_type" : undef, # Timing request_duration => ( $t_after - $t_before ) * 1000, date => DateTime->from_epoch(epoch => $t_before)->strftime('%Y-%m-%dT%H:%M:%S.%3NZ'), epochtime => $t_before, }; if ($self->extra_field()) { for my $env_field (keys %{$self->extra_field}) { $log_entry->{$self->extra_field()->{$env_field}} = $env->{$env_field}; } } if ($self->callback()) { $log_entry = $self->callback()->($env, $log_entry); } my $logger = $self->logger() || sub { $env->{'psgi.errors'}->print($_[0] . "\n") }; $logger->(encode_json($log_entry)); return; }); } # Taken from Plack::Middleware::AccessLog sub _safe { my ($string) = @_; $string =~ s/([^[:print:]])/"\\x" . unpack("H*", $1)/xeg if defined $string; return $string; } 1; __END__ =pod =head1 NAME Plack::Middleware::AccessLog::Structured - Access log middleware which creates structured log messages =head1 VERSION version 0.001000 =head1 SYNOPSIS use Plack::Middleware::AccessLog::Structured; Plack::Middleware::AccessLog::Structured->wrap($app, callback => sub { my ($env, $message) = @_; $message->{foo} = 'bar'; return $message; }, extra_field => { 'some.application.field' => 'log_field', 'some.psgi-env.field' => 'another_log_field', }, ); =head1 DESCRIPTION Plack::Middleware::AccessLog::Structured is a L which creates and logs structured messages. If the above example is used with a basic L application that simply returns C, the following, JSON-encoded message would be logged (on one log line): { "remote_addr": "127.0.0.1", "request_method": "GET", "request_uri": "/", "server_protocol": "HTTP/1.1", "remote_user": null, "http_referer": null, "http_user_agent": "Mozilla/5.0 [...]", "request_duration": 0.0679492950439453, "epochtime": 1348687439.49608, "date": "2012-09-26T19:23:59.496Z", "hostfqdn": "some.hostname.tld", "hostname": "some", "http_host": "localhost:5000", "pid": 4777, "log_field": null, "another_log_field": null, "foo": "bar", "response_status": 200, "content_length": 2, "content_type": "text/plain", "class": "Plack::Middleware::AccessLog::Structured" } =head1 METHODS =head2 new Constructor, creates new instance. =head3 Parameters This method expects its parameters as a hash reference. =over =item callback Optional callback that can be used to modify the log message before it is encoded and logged. Gets the L environment and the message as parameters and must return the possibly modified message. =item extra_field Optional hash reference with a mapping from L environment keys to keys in the log message. If passed, values from the L environment will be copied to the corresponding fields in the log message, using this mapping. =item logger A callback to pass the JSON-encoded log messages to. By default, log messages are printed to the C output stream. =back =head3 Result A fresh instance of the middleware. =head2 call Specialized C method. =head1 SEE ALSO =over =item * L =item * L which inspired some of the fields in the log message. =back =head1 AUTHOR Manfred Stock =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2012 by Manfred Stock. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut