hydra-server: Support logs in S3

This commit is contained in:
Eelco Dolstra 2017-04-05 17:55:56 +02:00
parent 4f11cf45dc
commit fec895a642
No known key found for this signature in database
GPG key ID: 8170B4726D7198DE
13 changed files with 146 additions and 136 deletions

View file

@ -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);
@ -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'.");
}
}

View file

@ -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.
@ -434,19 +435,36 @@ sub search :Local Args(0) {
{ order_by => ["id desc"] } ) ];
}
sub log :Local :Args(1) {
my ($self, $c, $path) = @_;
$path = ($ENV{NIX_STORE_DIR} || "/nix/store")."/$path";
my @outpaths = ($path);
my $logPath = findLog($c, $path, @outpaths);
notFound($c, "The build log of $path is not available.") unless defined $logPath;
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, $drvPath) = @_;
$drvPath = "/nix/store/$drvPath";
my $tail = $c->request->params->{"tail"};
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;

View file

@ -18,7 +18,7 @@ our @EXPORT = qw(
getSCMCacheDir
registerRoot getGCRootsDir gcRootFor
jobsetOverview jobsetOverview_
removeAsciiEscapes getDrvLogPath findLog logContents
getDrvLogPath findLog
getMainOutput
getEvals getMachines
pathIsInsidePrefix
@ -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;
@ -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

View file

@ -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);

View file

@ -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 =~ /^.*@(.*)$/) {

View file

@ -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 %]
@ -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>
@ -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 %]

View file

@ -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;

View file

@ -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 %]

View file

@ -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>

View file

@ -1,39 +0,0 @@
[% WRAPPER layout.tt title="Log of " _ (step ? " step $step.stepnr of " : "") _ "build ${build.id} of job $build.project.name:$build.jobset.name:$build.job.name" %]
[% PROCESS common.tt %]
[% project = build.project %]
[% jobset = build.jobset %]
[% job = build.job %]
<p>Below are the last 50 log lines. The <a href="[% c.uri_for('/build' build.id 'log') %]">full log</a> is also available.</p>
[% IF reload %]
<script>
function scrollDown() {
$("#contents").scrollTop($("#contents").get(0).scrollHeight);
}
function injectTail() {
$.ajax({
url: "[% url %]",
dataType: "text",
success: function (tail) {
$("#contents").text(tail);
scrollDown();
}
});
}
$(document).ready(function() {
scrollDown();
injectTail();
setInterval(injectTail, 5000);
});
</script>
[% END %]
<pre class="taillog" id="contents">
[% HTML.escape(contents) %]
</pre>
[% END %]

View file

@ -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;
}

View file

@ -120,24 +120,48 @@ function escapeHTML(s) {
return $('<div/>').text(s).html();
};
function requestFile(args) {
if (!"error" in args) {
args.error = function(data) {
json = {};
try {
if (data.responseText)
json = $.parseJSON(data.responseText);
} catch (err) {
}
if (json.error)
bootbox.alert(escapeHTML(json.error));
else if (data.responseText)
bootbox.alert("Server error: " + escapeHTML(data.responseText));
else
bootbox.alert("Unknown server error!");
if (args.postError) args.postError(data);
};
}
return $.ajax(args);
};
function requestJSON(args) {
args.dataType = 'json';
args.error = function(data) {
json = {};
try {
if (data.responseText)
json = $.parseJSON(data.responseText);
} catch (err) {
requestFile(args);
};
function requestPlainFile(args) {
args.dataType = 'text';
/* Remove the X-Requested-With header, which would turn trigger
CORS checks for this request.
http://stackoverflow.com/a/24719409/6747243
*/
args.xhr = function() {
var xhr = jQuery.ajaxSettings.xhr();
var setRequestHeader = xhr.setRequestHeader;
xhr.setRequestHeader = function(name, value) {
if (name == 'X-Requested-With') return;
setRequestHeader.call(this, name, value);
}
if (json.error)
bootbox.alert(escapeHTML(json.error));
else if (data.responseText)
bootbox.alert("Server error: " + escapeHTML(data.responseText));
else
bootbox.alert("Unknown server error!");
if (args.postError) args.postError(data);
return xhr;
};
return $.ajax(args);
requestFile(args);
};
function redirectJSON(args) {

View file

@ -25,7 +25,7 @@ order of descending finish time.</p>
<td><tt>[% step.drvpath.match('-(.*).drv').0 %]</tt></td>
<td><tt>[% INCLUDE renderFullJobNameOfBuild build=step.build %]</tt></td>
<td><a href="[% c.uri_for('/build' step.build.id) %]">[% step.build.id %]</a></td>
<td><a class="row-link" href="[% c.uri_for('/build' step.build.id 'nixlog' step.stepnr 'tail-reload') %]">[% step.stepnr %]</a></td>
<td><a class="row-link" href="[% c.uri_for('/build' step.build.id 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a></td>
<td>[% INCLUDE renderRelativeDate timestamp=step.stoptime %]</td>
<td style="width: 10em">[% INCLUDE renderDuration duration = step.stoptime - step.starttime %] </td>
<td><tt>[% INCLUDE renderMachineName machine=step.machine %]</tt></td>