forked from lix-project/hydra
hydra-server: Support logs in S3
This commit is contained in:
parent
4f11cf45dc
commit
fec895a642
|
@ -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'.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 =~ /^.*@(.*)$/) {
|
||||
|
|
|
@ -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 %]
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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 %]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %]
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue