File Coverage

blib/lib/Log/Log4perl/Appender/Elasticsearch.pm
Criterion Covered Total %
statement 74 104 71.1
branch 8 20 40.0
condition 1 5 20.0
subroutine 14 19 73.6
pod 1 2 50.0
total 98 150 65.3


line stmt bran cond sub pod time code
1             package Log::Log4perl::Appender::Elasticsearch;
2              
3 3     3   57460 use strict;
  3         5  
  3         101  
4 3     3   12 use warnings;
  3         3  
  3         123  
5             our @ISA = qw(Log::Log4perl::Appender);
6              
7 3     3   13 use Carp;
  3         7  
  3         191  
8 3     3   1548 use HTTP::Headers();
  3         18956  
  3         74  
9 3     3   1510 use HTTP::Request();
  3         34131  
  3         80  
10 3     3   633 use JSON;
  3         9572  
  3         19  
11 3     3   2412 use LWP::UserAgent();
  3         61395  
  3         92  
12 3     3   1075 use Log::Log4perl;
  3         41737  
  3         24  
13 3     3   1782 use MIME::Base64;
  3         1878  
  3         223  
14 3     3   18 use URI;
  3         4  
  3         2871  
15              
16             =head1 NAME
17              
18             Log::Log4perl::Appender::Elasticsearch - implements appending to Elasticsearch
19              
20             =head1 DESCRIPTION
21              
22             This is a simple appender for writing log entries to Elasticsearch via L.
23              
24             L does the same task by using L.
25              
26             =head1 VERSION
27              
28             Version 0.06
29              
30             =cut
31              
32             $Log::Log4perl::Appender::Elasticsearch::VERSION = '0.06';
33              
34             =head1 SYNOPSIS
35              
36             use Log::Log4perl;
37              
38             Log::Log4perl->init(\<<'HERE');
39             log4perl.logger=DEBUG, ES
40              
41             log4perl.appender.ES = Log::Log4perl::Appender::Elasticsearch
42             log4perl.appender.ES.layout = Log::Log4perl::Layout::NoopLayout
43              
44             log4perl.appender.ES.body.level = %p
45             log4perl.appender.ES.body.module = %M
46             log4perl.appender.ES.body.line = %L
47              
48             log4perl.appender.ES.nodes = localhost:9200
49             log4perl.appender.ES.index = log4perl
50             log4perl.appender.ES.type = entry
51              
52             log4perl.appender.ES.use_https = 0
53             log4perl.appender.ES.user_agent.timeout = 5
54              
55             log4perl.appender.ES.headers.User-Agent = foo
56             HERE
57              
58             Log::Log4perl::get_logger()->info("OK");
59              
60             # look up:
61             # curl -XPOST 'http://localhost:9200/log4perl/_search' -d \
62             # '{"query": {"query_string": {"query": "level:INFO AND message:OK"}}}'
63             # ...
64             # "_source": {
65             # "module": "main::__ANON__",
66             # "line": "60",
67             # "level": "INFO",
68             # "message": "OK"
69             # }
70              
71              
72             =head1 OPTIONS
73              
74             =over 4
75              
76             =item
77              
78             nodes
79              
80             a comma separeted list of nodes. The message will be sent to the next node only if previous request failed
81              
82             =item
83              
84             index
85              
86             The name of the elasticsearch index the message will be stored in.
87              
88             =item
89              
90             type
91              
92             The name of the type in given index the message belongs to.
93              
94             =item
95              
96             use_https
97              
98             0|1 global https setting for all nodes
99              
100             the individual https setting possible too: C
101              
102             =item
103              
104             user_agent
105              
106             LWP::UserAgent parameters.
107              
108             C
109              
110             =item
111              
112             headers
113              
114             HTTP::Headers parameters
115              
116             C
117              
118             =back
119              
120             =cut
121              
122             sub new {
123 2     2 1 791 my ($proto, %p) = @_;
124 2   33     12 my $class = ref $proto || $proto;
125 2         6 my $self = bless {}, $class;
126              
127 2         12 $self->_init(%p);
128 2         13 return $self;
129             } ## end sub new
130              
131             sub log {
132 0     0 0 0 my ($self, %p) = @_;
133 0         0 $self->_send_request($self->_to_json($self->_prepare_body(%p)));
134             }
135              
136             sub _init {
137 2     2   6 my ($self, %p) = @_;
138              
139 2 50       6 defined($p{nodes})
140             || Carp::croak('Log4perl: nodes not set in ', __PACKAGE__);
141              
142 2         4 my $use_https = delete($p{use_https});
143 2         22 foreach (split ',', delete($p{nodes})) {
144 2         11 (my $node = $_) =~ s/^\s+|\s+$//g;
145 2 50       12 unless ($node =~ m{^http(s)?://}) {
146 2 50       6 $node = ($use_https ? 'https://' : 'http://') . $node;
147             }
148              
149 2         12 my $uri = URI->new($node);
150 2         13881 push @{ $self->{_nodes} }, $uri;
  2         27  
151             } ## end foreach (split ',', delete(...))
152              
153 2         5 foreach my $k (qw/index type/) {
154 4         9 my $v = delete($p{$k});
155 4 50       8 $v || Carp::croak("Log4perl: $k not set in ", __PACKAGE__);
156 4         9 $self->{"_$k"} = $v;
157             }
158              
159 2         5 my $b = delete($p{body});
160 2 50       4 scalar(keys %{$b})
  2         7  
161             || Carp::croak('Log4perl: body not set in ', __PACKAGE__);
162              
163 2         2 foreach my $k (keys %{$b}) {
  2         6  
164 6 50       403 $k eq 'message' && Carp::croak(
165             "Log4perl: choose an other key name instead $k. The key $k is used to store the logging message ",
166             __PACKAGE__
167             );
168 6         39 $self->{_body}{$k} = Log::Log4perl::Layout::PatternLayout->new(
169             { ConversionPattern => { value => $b->{$k} } });
170             } ## end foreach my $k (keys %{$b})
171              
172 2         112 my $h = delete($p{headers});
173 2         19 $self->{_headers} = HTTP::Headers->new(%{$h});
  2         19  
174              
175 2         146 my $up = delete($p{user_agent});
176 2         4 $self->{_user_agent} = LWP::UserAgent->new(%{$up});
  2         18  
177              
178 2         5988 foreach my $k (keys %p) {
179 1         6 $self->{$k} = $p{$k};
180             }
181             } ## end sub _init
182              
183             sub _send_request {
184 0     0   0 my ($self, $data, $suffix) = @_;
185 0         0 my @nodes = @{ $self->{_nodes} };
  0         0  
186 0         0 my (@errors, $ok);
187 0   0     0 do {
188 0         0 my $node = shift @nodes;
189 0         0 my $uri = $self->_uri($node, $suffix);
190 0         0 my $req = $self->_request($uri, $data);
191              
192 0         0 my $resp = $self->{_user_agent}->request($req);
193 0         0 $ok = $resp->is_success;
194 0 0       0 if (!$ok) {
195 0         0 push @errors, join ': ', $uri, $resp->status_line;
196             }
197             } while (!$ok && scalar(@nodes));
198              
199 0 0       0 $ok || Carp::croak('coud not send log the message to any node: ',
200             join '; ', @errors);
201             } ## end sub _send_request
202              
203             sub _uri {
204 1     1   686 my ($self, $node, $suffix) = @_;
205 1         17 my $uri = $node->clone;
206 1 50       114 $uri->path(join '', $uri->path,
207             join('/', $self->{_index}, $self->{_type}, $suffix ? $suffix : ''));
208              
209 1         88 return $uri;
210             } ## end sub _uri
211              
212             sub _headers {
213 1     1   2 my ($self, $uri) = @_;
214 1         113 my $h = $self->{_headers}->clone;
215 1         7 $h->header('Content-Type' => 'application/json');
216              
217 1         50 my $ui = $uri->userinfo;
218 1 50       57 if ($ui) {
219 0         0 my $auth = MIME::Base64::encode_base64($ui);
220 0         0 chomp $auth;
221 0         0 $h->header(Authorization => "Basic $auth");
222             }
223              
224 1         4 return $h;
225             } ## end sub _headers
226              
227             sub _request {
228 0     0     my ($self, $uri, $data) = @_;
229              
230 0           return HTTP::Request->new(
231             POST => $uri,
232             $self->_headers($uri),
233             $data
234             );
235             } ## end sub _request
236              
237             sub _prepare_body {
238 0     0     my ($self, %p) = @_;
239              
240 0           my $b = {};
241 0           foreach my $k (keys %{ $self->{_body} }) {
  0            
242 0           my $v
243             = $self->{_body}{$k}
244             ->render($p{message}, $p{log4p_category}, $p{log4p_level},
245             5 + $Log::Log4perl::caller_depth,
246             );
247              
248 0           $b->{$k} = $v;
249             } ## end foreach my $k (keys %{ $self...})
250              
251 0           $b->{message} = $p{message};
252              
253 0           return $b;
254             } ## end sub _prepare_body
255              
256             sub _to_json {
257 0     0     my ($self, $o) = @_;
258 0           return JSON::encode_json($o);
259             }
260              
261             1; # End of Log::Log4perl::Appender::Elasticsearch
262             __END__