File Coverage

blib/lib/Log/Log4perl/Appender/Chunk/Store/S3.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Log::Log4perl::Appender::Chunk::Store::S3;
2             $Log::Log4perl::Appender::Chunk::Store::S3::VERSION = '0.010';
3 2     2   7167 use Moose;
  2         5  
  2         16  
4             extends qw/Log::Log4perl::Appender::Chunk::Store/;
5              
6              
7 2     2   12612 use Carp;
  2         4  
  2         138  
8              
9 2     2   1946 use Net::Amazon::S3;
  0            
  0            
10             use Net::Amazon::S3::Client;
11              
12             use DateTime;
13              
14             use Log::Log4perl;
15             my $LOGGER = Log::Log4perl->get_logger();
16              
17             sub BEGIN{
18             eval "require Net::Amazon::S3::Client;";
19             if( $@ ){
20             die "\n\nFor ".__PACKAGE__.": Cannot load Net::Amazon::S3::Client\n\n -> Please install that if you want to use S3 Log Chunk storage.\n\n";
21             }
22             }
23              
24             has 's3_client' => ( is => 'ro', isa => 'Net::Amazon::S3::Client', lazy_build => 1 );
25             has 'bucket' => ( is => 'ro' , isa => 'Net::Amazon::S3::Client::Bucket', lazy_build => 1);
26              
27              
28             has 'host' => ( is => 'ro', isa => 'Maybe[Str]');
29             has 'location_constraint' => ( is => 'ro', isa => 'Maybe[Str]');
30             has 'bucket_name' => ( is => 'ro' , isa => 'Str' , required => 1);
31             has 'aws_access_key_id' => ( is => 'ro' , isa => 'Str', required => 1 );
32             has 'aws_secret_access_key' => ( is => 'ro' , isa => 'Str' , required => 1);
33             has 'retry' => ( is => 'ro' , isa => 'Bool' , required => 1 , default => 1);
34              
35             has 'log_auth_links' => ( is => 'ro' , isa => 'Bool' , required => 1, default => 0);
36              
37              
38             # Single object properties.
39              
40             # Short access list name
41             has 'acl_short' => ( is => 'ro' , isa => 'Maybe[Str]', default => undef );
42              
43             # Expires in this amount of days.
44             has 'expires_in_days' => ( is => 'ro' , isa => 'Maybe[Int]' , default => undef );
45              
46             has 'vivify_bucket' => ( is => 'ro' , isa => 'Bool' , required => 1 , default => 0 );
47              
48             sub _build_s3_client{
49             my ($self) = @_;
50             return Net::Amazon::S3::Client->new( s3 =>
51             Net::Amazon::S3->new(
52             aws_access_key_id => $self->aws_access_key_id(),
53             aws_secret_access_key => $self->aws_secret_access_key(),
54             retry => $self->retry(),
55             ( $self->host() ? ( host => $self->host() ) : () )
56             ));
57             }
58              
59             =head1 NAME
60              
61             Log::Log4perl::Appender::Chunk::Store::S3 - Store chunks in an S3 bucket
62              
63             =head1 SYNOPSIS
64              
65             Example:
66              
67             # Built-in store class S3
68             log4perl.appender.Chunk.store_class=S3
69             # S3 Mandatory options
70             log4perl.appender.Chunk.store_args.bucket_name=MyLogChunks
71             log4perl.appender.Chunk.store_args.aws_access_key_id=YourAWSAccessKey
72             log4perl.appender.Chunk.store_args.aws_secret_access_key=YourAWS
73              
74              
75             See L<Log::Log4perl::Appender::Chunk>'s synopsis for a more complete example.
76              
77             =head1 OPTIONS
78              
79             =over
80              
81             =item bucket_name
82              
83             Mandatory. Name of the Amazon S3 bucket to store the log chunks.
84              
85             =item location_constraint
86              
87             Optional. If your current Net::Amazon::S3 supports it, you can use that to set the location constraint
88             of the vivified bucket. See L<Net::Amazon::S3#add_bucket> for more info on supported constraints.
89              
90             Note that if you specify a location constraint, you will have to follow Amazon's recommendation about
91             naming your bucket. See L<http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html> for restriction
92             about bucket names. In Particular, if you are outside the US, you will not be able to have upper case characters
93             in your bucket name.
94              
95             =item host
96              
97             Optional. If the bucket you are using is not in the USA, and depending on the version of L<Net::Amazon::S3> you
98             have, you might want to set that to a different host, according to
99             L<http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region>
100              
101             See <Net::Amazon::S3#new>
102              
103             =item aws_access_key_id
104              
105             Mandatory. Your S3 access key ID. See L<Net::Amazon::S3>
106              
107             =item asw_secret_acccess_key
108              
109             Mandatory. Your S3 Secret access key. See L<Net::Amazon::S3>
110              
111             =item retry
112              
113             Optional. See L<Net::Amazon::S3>
114              
115             Defaults to true.
116              
117             =item acl_short
118              
119             Optional. Shortcut to commonly used ACL rules. Valid values are:
120             private public-read public-read-write authenticated-read.
121              
122             See L<https://metacpan.org/source/PFIG/Net-Amazon-S3-0.59/lib/Net/Amazon/S3/Client/Object.pm>
123              
124             Defaults to undef, meaning your Amazon Bucket's default will be applied. That's probably
125             the most desirable behaviour.
126              
127             =item expires_in_days
128              
129             Optional. Amount of days in the future stored chunks should expire. No value means never.
130              
131             Defaults to undef.
132              
133             =item vivify_bucket
134              
135             Optional. If true, this writer will attempt to vivify a non existing bucket name if possible.
136              
137             Defaults to false.
138              
139             =item log_auth_links
140              
141             Optional. If true, this writer will log (at DEBUG level) the authenticated links to the stored chunks
142             in other log appenders.
143              
144             Use with care as this could lead to confidential information leakage.
145              
146             Defaults to false.
147              
148             =back
149              
150             =head1 METHODS
151              
152             =head2 clone
153              
154             Returns a fresh copy of myself based on the same settings. Mainly used internaly.
155              
156             Usage:
157              
158             my $clone = $this->clone();
159              
160             =cut
161              
162             sub clone{
163             my ($self) = @_;
164             return __PACKAGE__->new({bucket_name => $self->bucket_name(),
165             aws_access_key_id => $self->aws_access_key_id(),
166             aws_secret_access_key => $self->aws_secret_access_key(),
167             retry => $self->retry(),
168             acl_short => $self->acl_short(),
169             expires_in_days => $self->expires_in_days(),
170             vivify_bucket => $self->vivify_bucket(),
171             log_auth_links => $self->log_auth_links(),
172             host => $self->host(),
173             location_constraint => $self->location_constraint()
174             });
175             }
176              
177             sub _build_bucket{
178             my ($self) = @_;
179              
180             my $s3_client = $self->s3_client();
181             my $bucket_name = $self->bucket_name();
182              
183             # Try to hit an existing bucket from the list
184             my @buckets = $s3_client->buckets();
185             foreach my $bucket ( @buckets ){
186             if( $bucket->name() eq $bucket_name ){
187             # Hit!
188             return $bucket;
189             }
190             }
191              
192             unless( $self->vivify_bucket() ){
193             confess("Could not find bucket ".$bucket_name." in this account [access_key_id='".$self->aws_access_key_id()."'] and no vivify_bucket option");
194             }
195             return $self->s3_client()->create_bucket( name => $bucket_name,
196             ( $self->location_constraint() ? ( location_constraint => $self->location_constraint() ) : () )
197             );
198             }
199              
200             sub _expiry_ymd{
201             my ($self) = @_;
202             unless( $self->expires_in_days() ){
203             return undef;
204             }
205             return DateTime->now()->add( days => $self->expires_in_days() )->ymd();
206             }
207              
208             =head2 store
209              
210             See superclass L<Log::Log4perl::Appender::Chunk::Store>
211              
212             =cut
213              
214             sub store{
215             my ($self, $chunk_id, $big_message) = @_;
216              
217              
218             defined(my $child = fork()) or confess("Cannot fork: $!");
219             if( $child ){
220             ## We are the main parent. We wait for the child.
221             waitpid($child, 0);
222             return 1;
223             }
224              
225             # We are the child
226             # Double fork to avoid zombies.
227             defined( my $grand_child = fork() ) or confess("Cannot double fork: $!");
228             if( $grand_child ){
229             # We are the child but we dont wait for
230             # our grand child. It will be picked up by init
231             exit(0);
232             }
233              
234             # Grand child. We can do stuff.
235             $self = $self->clone();
236              
237             my $expires_ymd = $self->_expiry_ymd();
238             my $s3object = $self->bucket()->object( key => $chunk_id,
239             content_type => 'text/plain; charset=utf-8',
240             $self->acl_short() ? ( acl_short => $self->acl_short() ) : (),
241             $expires_ymd ? ( expires => $expires_ymd ) : (),
242             );
243             eval{
244             $s3object->put(Encode::encode_utf8($big_message));
245             };
246             if( my $err = $@ ){
247             $LOGGER->error("Log chunk storing error: $err");
248             warn "Log chunk storing error: $err";
249             exit(1);
250             }
251             if( $self->log_auth_links() ){
252             $LOGGER->info("Stored log chunk in ".$s3object->query_string_authentication_uri());
253             }
254             exit(0);
255             }
256              
257             __PACKAGE__->meta->make_immutable();