Skip to content

Instantly share code, notes, and snippets.

@jberger
Last active September 26, 2023 15:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jberger/726d9dcc7aaffd7fb8ee030e4f1f8068 to your computer and use it in GitHub Desktop.
Save jberger/726d9dcc7aaffd7fb8ee030e4f1f8068 to your computer and use it in GitHub Desktop.
A little graphite CLI tool
#!/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