File Coverage

blib/lib/Whisper.pm
Criterion Covered Total %
statement 107 122 87.7
branch 25 42 59.5
condition 7 8 87.5
subroutine 8 9 88.8
pod 2 2 100.0
total 149 183 81.4


line stmt bran cond sub pod time code
1             # Original Copyright 2008 Orbitz WorldWide (python)
2             # Perl port 2013 Jean Stebens (perl)
3             #
4             # Licensed under the Apache License, Version 2.0 (the "License");
5             # you may not use this file except in compliance with the License.
6             # You may obtain a copy of the License at
7             #
8             # http://www.apache.org/licenses/LICENSE-2.0
9             #
10             # Unless required by applicable law or agreed to in writing, software
11             # distributed under the License is distributed on an "AS IS" BASIS,
12             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13             # See the License for the specific language governing permissions and
14             # limitations under the License.
15              
16             package Whisper;
17             {
18             $Whisper::VERSION = '1.035';
19             }
20              
21 2     2   2073 use 5.012;
  2         7  
  2         80  
22              
23 2     2   14 use strict;
  2         5  
  2         80  
24 2     2   27 use warnings;
  2         5  
  2         72  
25              
26 2     2   2168 use POSIX;
  2         17749  
  2         20  
27              
28             our $VERSION;
29              
30 2     2   6547 use base 'Exporter';
  2         5  
  2         3109  
31             our @EXPORT = qw( wsp_info wsp_fetch );
32              
33             # ABSTRACT: Handle Whisper fixed-size database files
34            
35             # This module is an implementation of the Whisper database API
36             # Here is the basic layout of a whisper data file .wsp:
37             #
38             # File = Header,Data
39             # Header = Metadata,ArchiveInfo+
40             # Metadata = aggregationType,maxRetention,xFilesFactor,archiveCount
41             # ArchiveInfo = Offset,SecondsPerPoint,Points
42             # Data = Archive+
43             # Archive = Point+
44             # Point = timestamp,value
45              
46             my $metadata_Format = "N2f>N";
47             my $metadata_Size = length pack($metadata_Format, 0);
48              
49             my $archiveInfo_Format = "N3";
50             my $archiveInfo_Size = length pack($archiveInfo_Format, 0);
51              
52             my $point_Format = "Nd>";
53             my $point_Size = length pack($point_Format, 0);
54              
55             my $long_Format = "N";
56             my $long_Size = length pack($long_Format, 0);
57              
58             my $float_Format = "f";
59             my $float_Size = length pack($float_Format, 0);
60              
61             my $value_Format = "d";
62             my $value_Size = length pack($value_Format, 0);
63              
64             my $aggtype = {
65             1 => 'average',
66             2 => 'sum',
67             3 => 'last',
68             4 => 'max',
69             5 => 'min'
70             };
71              
72             sub __read_header {
73 6     6   10 my $file = shift;
74              
75 6 50       114 read($file, my $rec, $metadata_Size) or die("unable to read");
76 6         31 my ($aggregationType, $maxRetention, $xff, $archiveCount) = unpack($metadata_Format, $rec);
77            
78 6         13 my $archives = [];
79 6         19 foreach(0..$archiveCount-1) {
80 6         15 push(@$archives, __read_archiveinfo($file));
81             }
82              
83             return {
84 6         94 aggregationType => $aggregationType,
85             maxRetention => $maxRetention,
86             xFilesFactor => $xff,
87             archiveCount => $archiveCount,
88             archives => $archives,
89             fileSize => (stat($file))[7],
90             };
91             }
92              
93             sub __read_archiveinfo {
94 6     6   9 my $file = shift;
95              
96 6 50       20 read($file, my $rec, $archiveInfo_Size) or die("unable to read");
97 6         15 my ($offset, $secondsPerPoint, $points) = unpack($archiveInfo_Format, $rec);
98              
99             return {
100 6         47 offset => $offset,
101             secondsPerPoint => $secondsPerPoint,
102             points => $points,
103             retention => $secondsPerPoint * $points,
104             size => $points * $point_Size,
105             };
106             }
107              
108             sub wsp_info {
109 0     0 1 0 my %param = @_;
110              
111 0         0 my $dbfile = $param{file};
112              
113 0 0       0 die("You need to specify a wsp file\n") unless $dbfile;
114              
115 0 0       0 open(my $file, "<", $dbfile) or die("Unable to read whisper file: $dbfile\n");
116 0         0 binmode($file);
117              
118 0         0 my $header = __read_header($file);
119 0         0 $header->{fileSize} = (stat($file))[7];
120              
121 0         0 close($file);
122 0         0 return $header;
123             }
124              
125             sub wsp_fetch {
126 6     6 1 988 my %param = @_;
127              
128 6         14 my $dbfile = $param{file};
129 6         8 my $from = $param{from};
130 6         13 my $until = $param{until};
131 6         9 my $format = $param{format};
132 6         9 my $date_format = $param{date_format};
133              
134 6 50       17 die("You need to specify a wsp file\n") unless $dbfile;
135            
136 6 50       276 open(my $file, "<", $dbfile) or die("Unable to read whisper file: $dbfile\n");
137 6         17 binmode($file);
138              
139 6         15 my $header = __read_header($file);
140              
141 6         16 my $now = time;
142 6         12 my $oldest = $now - $header->{maxRetention};
143              
144             # defaults
145 6   66     21 $until ||= $now;
146 6   100     16 $from ||= 0;
147              
148 6 50       31 die("Invalid time interval") unless $from < $until;
149              
150             # from borders
151 6 100       15 $from = $oldest if $from < $oldest;
152              
153             # until borders
154 6 50       15 $until = $now if $until > $now;
155              
156 6         10 my $diff = $now - $from;
157              
158 6         6 my $archive;
159             # Get first archive which spans our wanted timeslot
160 6         7 foreach my $a (@{ $header->{archives} }) {
  6         14  
161 6 50       15 if( $a->{retention} >= $diff ) {
162 6         8 $archive = $a;
163 6         12 last;
164             }
165             }
166 6 50       15 die("No archive satisfies our needs") unless $archive;
167              
168 6         17 my $from_interval = ($from - ( $from % $archive->{secondsPerPoint})) + $archive->{secondsPerPoint};
169 6         10 my $until_interval = ($until - ( $until % $archive->{secondsPerPoint})) + $archive->{secondsPerPoint};
170              
171 6         9 my $offset = $archive->{offset};
172              
173 6         52 seek($file, $offset, 0);
174 6 50       55 read($file, my $packed_point, $point_Size) or die("unable to read");
175 6         20 my ($base_interval, $base_value) = unpack($point_Format, $packed_point);
176              
177 6 50       14 if( $base_interval == 0 ) {
178 0         0 my $step = $archive->{secondsPerPoint};
179 0         0 my $points = ($until_interval - $from_interval) / $step;
180 0         0 my @timeinfo = ($from_interval, $until_interval, $step);
181 0         0 my @values = (undef) x $points;
182             return {
183 0         0 start => $from_interval,
184             end => $until_interval,
185             step => $step,
186             values => \@values,
187             cnt => scalar @values,
188             }
189             }
190              
191             # Determine fromOffset
192 6         10 my $from_time_distance = $from_interval - $base_interval;
193 6         13 my $from_point_distance = $from_time_distance / $archive->{secondsPerPoint};
194 6         11 my $from_byte_distance = $from_point_distance * $point_Size;
195 6         13 my $from_offset = $archive->{offset} + ( $from_byte_distance % $archive->{size});
196              
197             # Determine untilOffset
198 6         9 my $until_time_distance = $until_interval - $base_interval;
199 6         89 my $until_point_distance = $until_time_distance / $archive->{secondsPerPoint};
200 6         9 my $until_byte_distance = $until_point_distance * $point_Size;
201 6         10 my $until_offset = $archive->{offset} + ( $until_byte_distance % $archive->{size});
202              
203             # Read all the points in the interval
204 6         47 seek($file, $from_offset, 0);
205 6         8 my $series;
206 6 100       15 if( $from_offset < $until_offset ) {
207 5         42 read($file, $series, ($until_offset - $from_offset));
208             } else {
209             #We do wrap around the archive, so we need two reads
210 1         2 my $archive_end = $archive->{offset} + $archive->{size};
211 1         39 read($file, my $first, $archive_end - $from_offset);
212 1         6 seek($file, $archive->{offset}, 0 );
213 1         11 read($file, my $second, $until_offset - $archive->{offset});
214 1         33 $series = $first . $second;
215            
216             }
217              
218             # Unpack the series
219 6         15 my $points = length($series) / $point_Size;
220 6         23 my $series_format = $point_Format x $points;
221 6         1006 my @series_unpacked = unpack($series_format, $series);
222              
223 6         173 my $values = [ (undef) x $points ];
224 6         8 my $current_interval = $from_interval;
225 6         12 my $step = $archive->{secondsPerPoint};
226              
227 6         9 my $index = 0;
228 6         23 while( @series_unpacked ) {
229 1736         2415 my ($point_time, $point_value ) = splice(@series_unpacked, 0, 2);
230 1736 50       3210 if( $point_time == $current_interval ) {
231 0         0 $values->[$index] = $point_value;
232             }
233 1736         1692 $current_interval += $step;
234 1736         3464 $index++;
235             }
236              
237             # Generate datetime,data tuples
238 6         103 my $keys = [ (undef) x @$values ];
239 6 100       18 if( $format ) {
240              
241 2         4 my $current = $from_interval;
242 2         16 while( my ($i, $val) = each @$values ) {
243              
244 118         143 my $timestamp = $current;
245             # Format the datetime field if wanted
246 118 50       199 if( $date_format ) {
247 118         6949 $timestamp = POSIX::strftime($date_format, localtime($current));
248             }
249              
250 118 100       341 if( $format eq 'tuples' ) {
251 59         152 $values->[$i] = [ $timestamp, $val ];
252             }
253 118 100       228 if( $format eq "split" ) {
254 59         163 $keys->[$i] = $timestamp;
255             }
256              
257 118         348 $current += $step;
258             }
259              
260             # Format start/end too
261 2 50       6 if( $date_format ) {
262 2         110 $from_interval = POSIX::strftime($date_format, localtime($from_interval));
263 2         113 $until_interval = POSIX::strftime($date_format, localtime($until_interval));
264             }
265              
266             }
267              
268 6         108 close($file);
269              
270 6         38 my $resp = {
271             start => $from_interval,
272             end => $until_interval,
273             step => $step,
274             values => $values,
275             cnt => scalar @$values,
276             };
277              
278 6 100 100     27 if( $format && $format eq "split" ) {
279 1         4 $resp->{keys} = $keys;
280             }
281              
282 6         112 return $resp;
283             }
284            
285             1;
286              
287             __END__