Last active
September 26, 2023 15:05
-
-
Save jberger/726d9dcc7aaffd7fb8ee030e4f1f8068 to your computer and use it in GitHub Desktop.
A little graphite CLI tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use Mojo::Base -strict; | |
use Getopt::Long qw(:config gnu_getopt no_auto_abbrev no_ignore_case); | |
use Mojo::Date; | |
use Mojo::URL; | |
use Mojo::UserAgent; | |
use Mojo::Util 'tablify'; | |
my $config = $ENV{GRAPHITE_CONFIG} || "$ENV{HOME}/.graphiterc"; | |
my $rc = do $config if -f -r $config; | |
my %options = ( | |
interval => '1week', | |
alias => 1, | |
align => 1, | |
%$rc, | |
); | |
my @argspec = ( | |
# behaviors | |
'C', # tab-completion hook | |
'completion|c', | |
'metrics|m=s', | |
'find' => sub { $options{metrics} = 'find' }, | |
'expand' => sub { $options{metrics} = 'expand' }, | |
'full-path', | |
'server=s', | |
'interval|i=s', | |
'from|f=s', | |
'until|u=s', | |
'alias!', | |
'node|n=i', | |
'headings!', | |
'timestamps!', | |
'time-format|T=s', | |
'human' => sub { $options{'time-format'} = 'human' }, | |
'rfc3339' => sub { $options{'time-format'} = 'rfc3339' }, | |
'unix' => sub { $options{'time-format'} = 'unix' }, | |
# summarize command and shortcuts | |
'summarize|s=s', | |
'align!', | |
'avg' => sub { $options{summarize} = 'avg' }, | |
'last' => sub { $options{summarize} = 'last' }, | |
'min' => sub { $options{summarize} = 'min' }, | |
'max' => sub { $options{summarize} = 'max' }, | |
'sum' => sub { $options{summarize} = 'sum' }, | |
); | |
exit 1 unless GetOptions(\%options, @argspec); | |
# simplified completion loader, put this in .profile or .bashrc: | |
# source <(graphite --completion) | |
if ($options{completion}) { | |
say q[complete -o nospace -C 'graphite -C --' graphite]; | |
exit 0; | |
} | |
unless (defined $options{server}) { | |
warn "A graphite server is required\n"; | |
exit 1; | |
} | |
my $ua = Mojo::UserAgent->new; | |
my $base = Mojo::URL->new($options{server}); | |
exit complete() if delete $options{C}; | |
my $target = shift; | |
exit find_metrics($target) if $options{metrics}; | |
if ($options{summarize}) { | |
my $align = $options{align} ? 'true' : 'false'; | |
$target = qq[summarize($target, "$options{interval}", "$options{summarize}", $align)] | |
} | |
$options{alias} //= 1 if defined $options{node}; | |
if ($options{alias}) { | |
if (defined $options{node}) { | |
$target = qq[aliasByNode($target, $options{node})]; | |
} else { | |
$target = qq[aliasByMetric($target)]; | |
} | |
} | |
$options{headings} //= 1 if defined $options{alias}; | |
my $url = $base->clone; | |
push @{$url->path}, 'render'; | |
$url->query({ | |
format => 'json', | |
target => $target, | |
}); | |
$options{from} = "-$options{interval}" unless $options{from}; | |
$url->query({from => $options{from}}) if defined $options{from}; | |
$url->query({until => $options{until}}) if defined $options{until}; | |
my $json = $ua->get($url)->result->json; | |
unless ($json && @$json) { | |
warn "No data received\n"; | |
exit 1; | |
} | |
my %table; | |
my @headings; | |
{ | |
my $single = @$json == 1 && @{ $json->[0]{datapoints} } == 1; | |
#TODO perhaps disable timestamps on single element summarize even for multiple series | |
$options{timestamps} //= !$single || !!$options{'time-format'}; | |
$options{headings} //= !$single; | |
} | |
$options{'time-format'} //= 'unix'; | |
my $time_format = | |
$options{'time-format'} eq 'human' ? 'to_string' : | |
$options{'time-format'} eq 'rfc3339' ? 'to_datetime' : | |
$options{'time-format'} eq 'unix' ? 'epoch' : | |
die "$options{'time-format'} is not an understood time format"; | |
push @headings, 'time' if $options{timestamps}; | |
for my $series (@$json) { | |
push @headings, defined $options{alias} ? $series->{target} : $series->{tags}{name} // $series->{target}; | |
for my $p (@{$series->{datapoints}}) { | |
my $row = $table{$p->[1]} ||= [ format_time($p->[1]) ]; | |
push @$row, $p->[0]; | |
} | |
} | |
my @times = sort keys %table; | |
my @table = ( | |
$options{headings} ? \@headings : (), | |
@table{@times}, | |
); | |
print tablify \@table; | |
sub format_time { | |
return unless $options{timestamps}; | |
return $_[0] unless $time_format; | |
my $t = Mojo::Date->new(shift); | |
return $t->$time_format(); | |
} | |
# complete -o nospace -C 'graphite -C --' graphite | |
sub complete { | |
my (undef, $curr, undef) = @ARGV; | |
# TODO handle other options smartly possibly using these | |
# my ($prog, $curr, $prev) = @ARGV; | |
# my ($line, $point) = @ENV{qw/COMP_LINE COMP_POINT/}; | |
return 0 if $curr =~ /^-/; | |
my $url = $base->clone; | |
push @{$url->path}, qw/metrics find/; | |
$url->query(query => $curr, format => 'completer'); | |
my $metrics = $ua->get($url)->result->json('/metrics'); | |
for my $m (@$metrics) { | |
my $path = $m->{path}; | |
$path .= ' ' if $m->{is_leaf}; | |
say $path; | |
} | |
return 0; | |
} | |
sub find_metrics { | |
my $target = shift; | |
my $url = $base->clone; | |
push @{$url->path}, 'metrics', $options{metrics}; | |
$url->query(query => $target); | |
$url->query({from => $options{from}}) if defined $options{from}; | |
$url->query({until => $options{until}}) if defined $options{until}; | |
my $json = $ua->get($url)->result->json; | |
say $options{'full-path'} ? $_->{id} : $_->{text} for @$json; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment