Add support for tracking custom metrics

Builds can now emit metrics that Hydra will store in its database and
render as time series via flot charts. Typical applications are to
keep track of performance indicators, coverage percentages, artifact
sizes, and so on.

For example, a coverage build can emit the coverage percentage as
follows:

  echo "lineCoverage $pct %" > $out/nix-support/hydra-metrics

Graphs of all metrics for a job can be seen at

  http://.../job/<project>/<jobset>/<job>#tabs-charts

Specific metrics are also visible at

  http://.../job/<project>/<jobset>/<job>/metric/<metric>

The latter URL also allows getting the data in JSON format (e.g. via
"curl -H 'Accept: application/json'").
This commit is contained in:
Eelco Dolstra
2015-07-31 00:57:30 +02:00
parent 8092149a9f
commit 4d26546d3c
18 changed files with 437 additions and 30 deletions

View File

@ -77,6 +77,9 @@ sub overview : Chained('job') PathPart('') Args(0) {
, jobset => $c->stash->{jobset}->name
, job => $c->stash->{job}->name
})->count == 1 if $c->user_exists;
$c->stash->{metrics} = [ $job->buildmetrics->search(
{ }, { select => ["name"], distinct => 1, order_by => "timestamp desc", }) ];
}
@ -110,6 +113,20 @@ sub output_sizes : Chained('job') PathPart('output-sizes') Args(0) {
}
sub metric : Chained('job') PathPart('metric') Args(1) {
my ($self, $c, $metricName) = @_;
$c->stash->{template} = 'metric.tt';
$c->stash->{metricName} = $metricName;
my @res = $c->stash->{job}->buildmetrics->search(
{ name => $metricName },
{ order_by => "timestamp", columns => [ "build", "name", "timestamp", "value", "unit" ] });
$self->status_ok($c, entity => [ map { { id => $_->get_column("build"), timestamp => $_ ->timestamp, value => $_->value, unit => $_->unit } } @res ]);
}
# Hydra::Base::Controller::ListBuilds needs this.
sub get_builds : Chained('job') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;

View File

@ -0,0 +1,187 @@
use utf8;
package Hydra::Schema::BuildMetrics;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::BuildMetrics
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildMetrics>
=cut
__PACKAGE__->table("BuildMetrics");
=head1 ACCESSORS
=head2 build
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=head2 name
data_type: 'text'
is_nullable: 0
=head2 unit
data_type: 'text'
is_nullable: 1
=head2 value
data_type: 'double precision'
is_nullable: 0
=head2 project
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 jobset
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 job
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 timestamp
data_type: 'integer'
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"build",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 0 },
"unit",
{ data_type => "text", is_nullable => 1 },
"value",
{ data_type => "double precision", is_nullable => 0 },
"project",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"jobset",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"job",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"timestamp",
{ data_type => "integer", is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</build>
=item * L</name>
=back
=cut
__PACKAGE__->set_primary_key("build", "name");
=head1 RELATIONS
=head2 build
Type: belongs_to
Related object: L<Hydra::Schema::Builds>
=cut
__PACKAGE__->belongs_to(
"build",
"Hydra::Schema::Builds",
{ id => "build" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);
=head2 job
Type: belongs_to
Related object: L<Hydra::Schema::Jobs>
=cut
__PACKAGE__->belongs_to(
"job",
"Hydra::Schema::Jobs",
{ jobset => "jobset", name => "job", project => "project" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
);
=head2 jobset
Type: belongs_to
Related object: L<Hydra::Schema::Jobsets>
=cut
__PACKAGE__->belongs_to(
"jobset",
"Hydra::Schema::Jobsets",
{ name => "jobset", project => "project" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
);
=head2 project
Type: belongs_to
Related object: L<Hydra::Schema::Projects>
=cut
__PACKAGE__->belongs_to(
"project",
"Hydra::Schema::Projects",
{ name => "project" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
);
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qoPm5/le+sVHigW4Dmum2Q
sub json_hint {
return { columns => ['value', 'unit'] };
}
1;

View File

@ -341,6 +341,21 @@ __PACKAGE__->has_many(
undef,
);
=head2 buildmetrics
Type: has_many
Related object: L<Hydra::Schema::BuildMetrics>
=cut
__PACKAGE__->has_many(
"buildmetrics",
"Hydra::Schema::BuildMetrics",
{ "foreign.build" => "self.id" },
undef,
);
=head2 buildoutputs
Type: has_many
@ -535,8 +550,8 @@ __PACKAGE__->many_to_many(
);
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:03:55
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EwxiaQpqbdzI9RvU0uUtLQ
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Y2lDtgY8EBLOuCHAI8fWRQ
__PACKAGE__->has_many(
"dependents",
@ -630,6 +645,7 @@ my %hint = (
buildoutputs => 'name',
buildinputs_builds => 'name',
buildproducts => 'productnr',
buildmetrics => 'name',
}
);

View File

@ -81,6 +81,25 @@ __PACKAGE__->set_primary_key("project", "jobset", "name");
=head1 RELATIONS
=head2 buildmetrics
Type: has_many
Related object: L<Hydra::Schema::BuildMetrics>
=cut
__PACKAGE__->has_many(
"buildmetrics",
"Hydra::Schema::BuildMetrics",
{
"foreign.job" => "self.name",
"foreign.jobset" => "self.jobset",
"foreign.project" => "self.project",
},
undef,
);
=head2 builds
Type: has_many
@ -150,7 +169,7 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-29 19:41:42
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lnZSd0gDXgLk8WQeAFqByA
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vDAo9bzLca+QWfhOb9OLMg
1;

View File

@ -184,6 +184,24 @@ __PACKAGE__->set_primary_key("project", "name");
=head1 RELATIONS
=head2 buildmetrics
Type: has_many
Related object: L<Hydra::Schema::BuildMetrics>
=cut
__PACKAGE__->has_many(
"buildmetrics",
"Hydra::Schema::BuildMetrics",
{
"foreign.jobset" => "self.name",
"foreign.project" => "self.project",
},
undef,
);
=head2 builds
Type: has_many
@ -320,8 +338,8 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-04-23 23:13:51
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CO0aE+jrjB+UrwGRzWZLlw
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Coci9FdBAvUO9T3st2NEqA
my %hint = (
columns => [

View File

@ -106,6 +106,21 @@ __PACKAGE__->set_primary_key("name");
=head1 RELATIONS
=head2 buildmetrics
Type: has_many
Related object: L<Hydra::Schema::BuildMetrics>
=cut
__PACKAGE__->has_many(
"buildmetrics",
"Hydra::Schema::BuildMetrics",
{ "foreign.project" => "self.name" },
undef,
);
=head2 builds
Type: has_many
@ -267,8 +282,8 @@ Composing rels: L</projectmembers> -> username
__PACKAGE__->many_to_many("usernames", "projectmembers", "username");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-04-23 23:13:08
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fkd9ruEoVSBGIktmAj4u4g
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:67kWIE0IGmEJTvOIATAKaw
my %hint = (
columns => [