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   46129 use strict;
  3         6  
  3         107  
4 3     3   14 use warnings;
  3         3  
  3         124  
5             our @ISA = qw(Log::Log4perl::Appender);
6              
7 3     3   13 use Carp;
  3         7  
  3         179  
8 3     3   1275 use HTTP::Headers();
  3         16203  
  3         77  
9 3     3   1408 use HTTP::Request();
  3         31533  
  3         71  
10 3     3   826 use JSON;
  3         9624  
  3         15  
11 3     3   1795 use LWP::UserAgent();
  3         40269  
  3         59  
12 3     3   702 use Log::Log4perl;
  3         33519  
  3         17  
13 3     3   1380 use MIME::Base64;
  3         1480  
  3         159  
14 3     3   14 use URI;
  3         3  
  3         2104  
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.07
29              
30             =cut
31              
32             $Log::Log4perl::Appender::Elasticsearch::VERSION = '0.07';
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:
101              
102             C
103              
104             =item
105              
106             user_agent
107              
108             LWP::UserAgent parameters
109              
110             C
111              
112             =item
113              
114             headers
115              
116             HTTP::Headers parameters
117              
118             C
119              
120             =back
121              
122             =cut
123              
124             sub new {
125 2     2 1 899 my ($proto, %p) = @_;
126 2   33     10 my $class = ref $proto || $proto;
127 2         4 my $self = bless {}, $class;
128              
129 2         8 $self->_init(%p);
130 2         7 return $self;
131             } ## end sub new
132              
133             sub log {
134 0     0 0 0 my ($self, %p) = @_;
135 0         0 $self->_send_request($self->_to_json($self->_prepare_body(%p)));
136             }
137              
138             sub _init {
139 2     2   5 my ($self, %p) = @_;
140              
141 2 50       5 defined($p{nodes})
142             || Carp::croak('Log4perl: nodes not set in ', __PACKAGE__);
143              
144 2         14 my $use_https = delete($p{use_https});
145 2         4 foreach (split ',', delete($p{nodes})) {
146 2         10 (my $node = $_) =~ s/^\s+|\s+$//g;
147 2 50       8 unless ($node =~ m{^http(s)?://}) {
148 2 50       5 $node = ($use_https ? 'https://' : 'http://') . $node;
149             }
150              
151 2         9 my $uri = URI->new($node);
152 2         10111 push @{ $self->{_nodes} }, $uri;
  2         19  
153             } ## end foreach (split ',', delete(...))
154              
155 2         5 foreach my $k (qw/index type/) {
156 4         6 my $v = delete($p{$k});
157 4 50       7 $v || Carp::croak("Log4perl: $k not set in ", __PACKAGE__);
158 4         8 $self->{"_$k"} = $v;
159             }
160              
161 2         2 my $b = delete($p{body});
162 2 50       2 scalar(keys %{$b})
  2         6  
163             || Carp::croak('Log4perl: body not set in ', __PACKAGE__);
164              
165 2         2 foreach my $k (keys %{$b}) {
  2         4  
166 6 50       266 $k eq 'message' && Carp::croak(
167             "Log4perl: choose an other key name instead $k. The key $k is used to store the logging message ",
168             __PACKAGE__
169             );
170 6         27 $self->{_body}{$k} = Log::Log4perl::Layout::PatternLayout->new(
171             { ConversionPattern => { value => $b->{$k} } });
172             } ## end foreach my $k (keys %{$b})
173              
174 2         93 my $h = delete($p{headers});
175 2         17 $self->{_headers} = HTTP::Headers->new(%{$h});
  2         14  
176              
177 2         105 my $up = delete($p{user_agent});
178 2         3 $self->{_user_agent} = LWP::UserAgent->new(%{$up});
  2         16  
179              
180 2         4106 foreach my $k (keys %p) {
181 1         4 $self->{$k} = $p{$k};
182             }
183             } ## end sub _init
184              
185             sub _send_request {
186 0     0   0 my ($self, $data, $suffix) = @_;
187 0         0 my @nodes = @{ $self->{_nodes} };
  0         0  
188 0         0 my (@errors, $ok);
189 0   0     0 do {
190 0         0 my $node = shift @nodes;
191 0         0 my $uri = $self->_uri($node, $suffix);
192 0         0 my $req = $self->_request($uri, $data);
193              
194 0         0 my $resp = $self->{_user_agent}->request($req);
195 0         0 $ok = $resp->is_success;
196 0 0       0 if (!$ok) {
197 0         0 push @errors, join ': ', $uri, $resp->status_line;
198             }
199             } while (!$ok && scalar(@nodes));
200              
201 0 0       0 $ok || Carp::croak('coud not send log the message to any node: ',
202             join '; ', @errors);
203             } ## end sub _send_request
204              
205             sub _uri {
206 1     1   690 my ($self, $node, $suffix) = @_;
207 1         9 my $uri = $node->clone;
208 1 50       83 $uri->path(join '', $uri->path,
209             join('/', $self->{_index}, $self->{_type}, $suffix ? $suffix : ''));
210              
211 1         55 return $uri;
212             } ## end sub _uri
213              
214             sub _headers {
215 1     1   1 my ($self, $uri) = @_;
216 1         68 my $h = $self->{_headers}->clone;
217 1         5 $h->header('Content-Type' => 'application/json');
218              
219 1         29 my $ui = $uri->userinfo;
220 1 50       41 if ($ui) {
221 0         0 my $auth = MIME::Base64::encode_base64($ui);
222 0         0 chomp $auth;
223 0         0 $h->header(Authorization => "Basic $auth");
224             }
225              
226 1         3 return $h;
227             } ## end sub _headers
228              
229             sub _request {
230 0     0     my ($self, $uri, $data) = @_;
231              
232 0           return HTTP::Request->new(
233             POST => $uri,
234             $self->_headers($uri),
235             $data
236             );
237             } ## end sub _request
238              
239             sub _prepare_body {
240 0     0     my ($self, %p) = @_;
241              
242 0           my $b = {};
243 0           foreach my $k (keys %{ $self->{_body} }) {
  0            
244 0           my $v
245             = $self->{_body}{$k}
246             ->render($p{message}, $p{log4p_category}, $p{log4p_level},
247             5 + $Log::Log4perl::caller_depth,
248             );
249              
250 0           $b->{$k} = $v;
251             } ## end foreach my $k (keys %{ $self...})
252              
253 0           $b->{message} = $p{message};
254              
255 0           return $b;
256             } ## end sub _prepare_body
257              
258             sub _to_json {
259 0     0     my ($self, $o) = @_;
260 0           return JSON::encode_json($o);
261             }
262              
263             1; # End of Log::Log4perl::Appender::Elasticsearch
264             __END__