Skip to content

Commit

Permalink
hydra-server: Support logs in S3
Browse files Browse the repository at this point in the history
  • Loading branch information
edolstra committed Apr 5, 2017
1 parent 4f11cf4 commit fec895a
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 134 deletions.
44 changes: 12 additions & 32 deletions src/lib/Hydra/Controller/Build.pm
Expand Up @@ -6,6 +6,7 @@ use warnings;
use base 'Hydra::Base::Controller::NixChannel';
use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils;
use File::Basename;
use File::stat;
use File::Slurp;
use Data::Dump qw(dump);
Expand Down Expand Up @@ -125,62 +126,41 @@ sub view_nixlog : Chained('buildChain') PathPart('nixlog') {

$c->stash->{step} = $step;

showLog($c, $mode, $step->busy == 0, $step->drvpath,
map { $_->path } $step->buildstepoutputs->all);
showLog($c, $mode, $step->busy == 0, $step->drvpath);
}


sub view_log : Chained('buildChain') PathPart('log') {
my ($self, $c, $mode) = @_;
showLog($c, $mode, $c->stash->{build}->finished,
$c->stash->{build}->drvpath,
map { $_->path } $c->stash->{build}->buildoutputs->all);
$c->stash->{build}->drvpath);
}


sub showLog {
my ($c, $mode, $finished, $drvPath, @outPaths) = @_;
my ($c, $mode, $finished, $drvPath) = @_;
$mode //= "pretty";

my $logPath = findLog($c, $drvPath, @outPaths);

notFound($c, "The build log of derivation ‘$drvPath’ is not available.") unless defined $logPath;

# Don't send logs that we can't stream.
my $size = stat($logPath)->size; # FIXME: not so meaningful for compressed logs
error($c, "This build log is too big to display ($size bytes).") unless
$mode eq "raw"
|| (($mode eq "tail" || $mode eq "tail-reload") && $logPath !~ /\.bz2$/)
|| $size < 64 * 1024 * 1024;
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [basename($drvPath)]);

if ($mode eq "pretty") {
$c->stash->{log_uri} = $log_uri;
$c->stash->{template} = 'log.tt';
$c->stash->{logtext} = logContents($logPath);
}

elsif ($mode eq "raw") {
$c->stash->{logPath} = $logPath;
$c->stash->{finished} = $finished;
$c->forward('Hydra::View::NixLog');
}

elsif ($mode eq "tail-reload") {
my $url = $c->uri_for($c->request->uri->path);
$url =~ s/tail-reload/tail/g;
$c->stash->{url} = $url;
$c->stash->{reload} = !$c->stash->{build}->finished;
$c->stash->{title} = "";
$c->stash->{contents} = (scalar logContents($logPath, 50)) || " ";
$c->stash->{template} = 'plain-reload.tt';
$c->res->redirect($log_uri);
}

elsif ($mode eq "tail") {
$c->stash->{'plain'} = { data => (scalar logContents($logPath, 50)) || " " };
$c->forward('Hydra::View::Plain');
my $lines = 50;
$c->stash->{log_uri} = $log_uri . "?tail=$lines";
$c->stash->{tail} = $lines;
$c->stash->{template} = 'log.tt';
}

else {
error($c, "Unknown log display mode `$mode'.");
error($c, "Unknown log display mode '$mode'.");
}
}

Expand Down
34 changes: 26 additions & 8 deletions src/lib/Hydra/Controller/Root.pm
Expand Up @@ -10,6 +10,7 @@ use Digest::SHA1 qw(sha1_hex);
use Nix::Store;
use Nix::Config;
use Encode;
use File::Basename;
use JSON;

# Put this controller at top-level.
Expand Down Expand Up @@ -434,19 +435,36 @@ sub search :Local Args(0) {
{ order_by => ["id desc"] } ) ];
}

sub serveLogFile {
my ($c, $logPath, $tail) = @_;
$c->stash->{logPath} = $logPath;
$c->stash->{tail} = $tail;
$c->forward('Hydra::View::NixLog');
}

sub log :Local :Args(1) {
my ($self, $c, $path) = @_;
my ($self, $c, $drvPath) = @_;

$path = ($ENV{NIX_STORE_DIR} || "/nix/store")."/$path";
$drvPath = "/nix/store/$drvPath";

my @outpaths = ($path);
my $logPath = findLog($c, $path, @outpaths);
notFound($c, "The build log of $path is not available.") unless defined $logPath;
my $tail = $c->request->params->{"tail"};

$c->stash->{logPath} = $logPath;
$c->forward('Hydra::View::NixLog');
}
die if defined $tail && $tail !~ /^[0-9]+$/;

my $logFile = findLog($c, $drvPath);

if (defined $logFile) {
serveLogFile($c, $logFile, $tail);
return;
}

my $logPrefix = $c->config->{log_prefix};

if (defined $logPrefix) {
$c->res->redirect($logPrefix . "log/" . basename($drvPath));
} else {
notFound($c, "The build log of $drvPath is not available.");
}
}

1;
28 changes: 3 additions & 25 deletions src/lib/Hydra/Helper/Nix.pm
Expand Up @@ -18,7 +18,7 @@ our @EXPORT = qw(
getSCMCacheDir
registerRoot getGCRootsDir gcRootFor
jobsetOverview jobsetOverview_
removeAsciiEscapes getDrvLogPath findLog logContents
getDrvLogPath findLog
getMainOutput
getEvals getMachines
pathIsInsidePrefix
Expand Down Expand Up @@ -154,9 +154,8 @@ sub getDrvLogPath {
my ($drvPath) = @_;
my $base = basename $drvPath;
my $bucketed = substr($base, 0, 2) . "/" . substr($base, 2);
my $fn = ($ENV{NIX_LOG_DIR} || "/nix/var/log/nix") . "/drvs/";
my $fn2 = Hydra::Model::DB::getHydraPath . "/build-logs/";
for ($fn2 . $bucketed, $fn2 . $bucketed . ".bz2", $fn . $bucketed . ".bz2", $fn . $bucketed, $fn . $base . ".bz2", $fn . $base) {
my $fn = Hydra::Model::DB::getHydraPath . "/build-logs/";
for ($fn . $bucketed, $fn . $bucketed . ".bz2") {
return $_ if -f $_;
}
return undef;
Expand Down Expand Up @@ -192,27 +191,6 @@ sub findLog {
}


sub logContents {
my ($logPath, $tail) = @_;
my $cmd;
if ($logPath =~ /.bz2$/) {
$cmd = "bzip2 -d < $logPath";
$cmd = $cmd . " | tail -n $tail" if defined $tail;
}
else {
$cmd = defined $tail ? "tail -$tail $logPath" : "cat $logPath";
}
return decode("utf-8", `$cmd`);
}


sub removeAsciiEscapes {
my ($logtext) = @_;
$logtext =~ s/\e\[[0-9]*[A-Za-z]//g;
return $logtext;
}


sub getMainOutput {
my ($build) = @_;
return
Expand Down
11 changes: 9 additions & 2 deletions src/lib/Hydra/View/NixLog.pm
Expand Up @@ -13,10 +13,17 @@ sub process {

my $fh = new IO::Handle;

my $tail = int($c->stash->{tail} // "0");

if ($logPath =~ /\.bz2$/) {
open $fh, "bzip2 -dc < '$logPath' |" or die;
my $doTail = $tail ? " tail -n '$tail' |" : "";
open $fh, "bzip2 -dc < '$logPath' | $doTail" or die;
} else {
open $fh, "<$logPath" or die;
if ($tail) {
open $fh, "tail -n '$tail' '$logPath' |" or die;
} else {
open $fh, "<$logPath" or die;
}
}
binmode($fh);

Expand Down
3 changes: 2 additions & 1 deletion src/lib/Hydra/View/TT.pm
Expand Up @@ -13,17 +13,18 @@ __PACKAGE__->config(

sub buildLogExists {
my ($self, $c, $build) = @_;
return 1 if defined $c->config->{log_prefix};
my @outPaths = map { $_->path } $build->buildoutputs->all;
return defined findLog($c, $build->drvpath, @outPaths);
}

sub buildStepLogExists {
my ($self, $c, $step) = @_;
return 1 if defined $c->config->{log_prefix};
my @outPaths = map { $_->path } $step->buildstepoutputs->all;
return defined findLog($c, $step->drvpath, @outPaths);
}


sub stripSSHUser {
my ($self, $c, $name) = @_;
if ($name =~ /^.*@(.*)$/) {
Expand Down
19 changes: 13 additions & 6 deletions src/root/build.tt
Expand Up @@ -7,7 +7,13 @@
[%
isAggregate = constituents.size > 0;
busy = 0;
FOR step IN steps; IF step.busy; busy = 1; END; END;
building = 0;
FOR step IN steps;
IF step.busy;
busy = 1;
IF step.drvpath == build.drvpath; building = 1; END;
END;
END;
%]

[% BLOCK renderOutputs %]
Expand Down Expand Up @@ -207,7 +213,8 @@ FOR step IN steps; IF step.busy; busy = 1; END; END;
<td>[% IF cachedBuild; INCLUDE renderFullBuildLink build=cachedBuild; ELSE %]<em>unknown</em>[% END %]</td>
</tr>
[% END %]
[% IF (!isAggregate || !build.ischannel) && build.finished; actualBuild = build.iscachedbuild ? cachedBuild : build %]
[% actualBuild = build.iscachedbuild ? cachedBuild : build %]
[% IF (!isAggregate || !build.ischannel) && build.finished; %]
[% IF actualBuild %]
<tr>
<th>Duration:</th>
Expand All @@ -219,13 +226,13 @@ FOR step IN steps; IF step.busy; busy = 1; END; END;
<td>[% INCLUDE renderDateTime timestamp = build.stoptime; %]</td>
</tr>
[% END %]
[% IF (!isAggregate || !build.ischannel) && buildLogExists(build) %]
[% IF (!build.finished && building) || (build.finished && (!isAggregate || !build.ischannel) && buildLogExists(build)) %]
<tr>
<th>Logfile:</th>
<td>
<a class="btn btn-mini" href="[% c.uri_for('/build' build.id 'log') %]">pretty</a>
<a class="btn btn-mini" href="[% c.uri_for('/build' build.id 'log' 'raw') %]">raw</a>
<a class="btn btn-mini" href="[% c.uri_for('/build' build.id 'log' 'tail-reload') %]">tail</a>
<a class="btn btn-mini" href="[% c.uri_for('/build' actualBuild.id 'log') %]">pretty</a>
<a class="btn btn-mini" href="[% c.uri_for('/build' actualBuild.id 'log' 'raw') %]">raw</a>
<a class="btn btn-mini" href="[% c.uri_for('/build' actualBuild.id 'log' 'tail') %]">tail</a>
</td>
</tr>
[% END %]
Expand Down
2 changes: 1 addition & 1 deletion src/root/common.tt
Expand Up @@ -465,7 +465,7 @@ BLOCK renderEvals %]


BLOCK renderLogLinks %]
(<a [% IF inRow %]class="row-link"[% END %] href="[% url %]">log</a>, <a href="[% "$url/raw" %]">raw</a>, <a href="[% "$url/tail-reload" %]">tail</a>)
(<a [% IF inRow %]class="row-link"[% END %] href="[% url %]">log</a>, <a href="[% "$url/raw" %]">raw</a>, <a href="[% "$url/tail" %]">tail</a>)
[% END;


Expand Down
40 changes: 37 additions & 3 deletions src/root/log.tt
Expand Up @@ -2,14 +2,48 @@
[% PROCESS common.tt %]

<p>
This is the build log of derivation <tt>[% IF step; step.drvpath; ELSE; build.drvpath; END %]</tt>.
Below
[% IF tail %]
are the last lines of
[% ELSE %]
is
[% END %]
the build log of derivation <tt>[% IF step; step.drvpath; ELSE; build.drvpath; END %]</tt>.
[% IF step && step.machine %]
It was built on <tt>[% step.machine %]</tt>.
[% END %]
[% IF tail %]
The <a href="[% step ? c.uri_for('/build' build.id 'nixlog' step.stepnr)
: c.uri_for('/build' build.id 'log') %]">full log</a> is also available.
[% END %]
</p>

<pre class="taillog" id="contents">
[% HTML.escape(logtext) %]
<pre class="log" id="contents">
<em>Loading...</em>
</pre>

<script type="text/javascript">
$(document).ready(function() {
requestPlainFile({
url: "[% HTML.escape(log_uri) %]",
dataType: "text",
type: 'GET',
success: function (log_data) {

[% IF tail %]
/* The server may give us a full log (e.g. if the log is in
S3). So extract the last lines. */
log_data = log_data.split("\n").slice(-[%tail%]).join("\n");
[% END %]

$("#contents").text(log_data);
},
error: function () {
bootbox.alert("The log file is not available.");
$("#contents").text("(Unavailable)");
}
});
});
</script>

[% END %]
2 changes: 1 addition & 1 deletion src/root/machine-status.tt
Expand Up @@ -42,7 +42,7 @@
<td><tt>[% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %]</tt></td>
<td><tt>[% step.system %]</tt></td>
<td><a href="[% c.uri_for('/build' step.build) %]">[% step.build %]</a></td>
<td><a class="row-link" href="[% c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail-reload') %]">[% step.stepnr %]</a></td>
<td><a class="row-link" href="[% c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a></td>
<td><tt>[% step.drvpath.match('-(.*)').0 %]</tt></td>
<td style="width: 10em">[% INCLUDE renderDuration duration = curTime - step.starttime %] </td>
</tr>
Expand Down
39 changes: 0 additions & 39 deletions src/root/plain-reload.tt

This file was deleted.

2 changes: 1 addition & 1 deletion src/root/static/css/hydra.css
Expand Up @@ -119,7 +119,7 @@ span.keep-whitespace {
max-width: none; /* don't apply responsive design to status images */
}

pre.log, pre.taillog {
pre.log {
line-height: 1.2em;
}

Expand Down

2 comments on commit fec895a

@copumpkin
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@vcunat
Copy link
Member

@vcunat vcunat commented on fec895a Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not perfect yet: #462

Please sign in to comment.