From fec895a6421a99e76772dc284a01cafe1c39d310 Mon Sep 17 00:00:00 2001
From: Eelco Dolstra
Date: Wed, 5 Apr 2017 17:55:56 +0200
Subject: [PATCH] hydra-server: Support logs in S3
---
src/lib/Hydra/Controller/Build.pm | 44 +++++++-------------------
src/lib/Hydra/Controller/Root.pm | 38 ++++++++++++++++------
src/lib/Hydra/Helper/Nix.pm | 28 ++---------------
src/lib/Hydra/View/NixLog.pm | 11 +++++--
src/lib/Hydra/View/TT.pm | 3 +-
src/root/build.tt | 19 +++++++----
src/root/common.tt | 2 +-
src/root/log.tt | 40 ++++++++++++++++++++++--
src/root/machine-status.tt | 2 +-
src/root/plain-reload.tt | 39 -----------------------
src/root/static/css/hydra.css | 2 +-
src/root/static/js/common.js | 52 ++++++++++++++++++++++---------
src/root/steps.tt | 2 +-
13 files changed, 146 insertions(+), 136 deletions(-)
delete mode 100644 src/root/plain-reload.tt
diff --git a/src/lib/Hydra/Controller/Build.pm b/src/lib/Hydra/Controller/Build.pm
index 76462e3a..d688ea0d 100644
--- a/src/lib/Hydra/Controller/Build.pm
+++ b/src/lib/Hydra/Controller/Build.pm
@@ -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'.");
}
}
diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm
index 8a90eb9f..234f73f4 100644
--- a/src/lib/Hydra/Controller/Root.pm
+++ b/src/lib/Hydra/Controller/Root.pm
@@ -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;
diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm
index 0edca601..1879ceba 100644
--- a/src/lib/Hydra/Helper/Nix.pm
+++ b/src/lib/Hydra/Helper/Nix.pm
@@ -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
diff --git a/src/lib/Hydra/View/NixLog.pm b/src/lib/Hydra/View/NixLog.pm
index 99f144c7..8103726d 100644
--- a/src/lib/Hydra/View/NixLog.pm
+++ b/src/lib/Hydra/View/NixLog.pm
@@ -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);
diff --git a/src/lib/Hydra/View/TT.pm b/src/lib/Hydra/View/TT.pm
index be3cf493..567fa1d7 100644
--- a/src/lib/Hydra/View/TT.pm
+++ b/src/lib/Hydra/View/TT.pm
@@ -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 =~ /^.*@(.*)$/) {
diff --git a/src/root/build.tt b/src/root/build.tt
index f9cffc0f..5284907c 100644
--- a/src/root/build.tt
+++ b/src/root/build.tt
@@ -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;
[% IF cachedBuild; INCLUDE renderFullBuildLink build=cachedBuild; ELSE %]unknown[% END %] |
[% 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 %]
Duration: |
@@ -219,13 +226,13 @@ FOR step IN steps; IF step.busy; busy = 1; END; END;
[% INCLUDE renderDateTime timestamp = build.stoptime; %] |
[% END %]
- [% IF (!isAggregate || !build.ischannel) && buildLogExists(build) %]
+ [% IF (!build.finished && building) || (build.finished && (!isAggregate || !build.ischannel) && buildLogExists(build)) %]
Logfile: |
- pretty
- raw
- tail
+ pretty
+ raw
+ tail
|
[% END %]
diff --git a/src/root/common.tt b/src/root/common.tt
index 076ca437..47b26e86 100644
--- a/src/root/common.tt
+++ b/src/root/common.tt
@@ -465,7 +465,7 @@ BLOCK renderEvals %]
BLOCK renderLogLinks %]
-(log, raw, tail)
+(log, raw, tail)
[% END;
diff --git a/src/root/log.tt b/src/root/log.tt
index 36eba0f0..65a092b9 100644
--- a/src/root/log.tt
+++ b/src/root/log.tt
@@ -2,14 +2,48 @@
[% PROCESS common.tt %]
- This is the build log of derivation [% IF step; step.drvpath; ELSE; build.drvpath; END %].
+ Below
+ [% IF tail %]
+ are the last lines of
+ [% ELSE %]
+ is
+ [% END %]
+ the build log of derivation [% IF step; step.drvpath; ELSE; build.drvpath; END %].
[% IF step && step.machine %]
It was built on [% step.machine %].
[% END %]
+ [% IF tail %]
+ The full log is also available.
+ [% END %]
-
-[% HTML.escape(logtext) %]
+
+Loading...
+
+
[% END %]
diff --git a/src/root/machine-status.tt b/src/root/machine-status.tt
index e0476744..7c6d1f2b 100644
--- a/src/root/machine-status.tt
+++ b/src/root/machine-status.tt
@@ -42,7 +42,7 @@
[% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %] |
[% step.system %] |
[% step.build %] |
- [% step.stepnr %] |
+ [% step.stepnr %] |
[% step.drvpath.match('-(.*)').0 %] |
[% INCLUDE renderDuration duration = curTime - step.starttime %] |
diff --git a/src/root/plain-reload.tt b/src/root/plain-reload.tt
deleted file mode 100644
index 19afc8fc..00000000
--- a/src/root/plain-reload.tt
+++ /dev/null
@@ -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 %]
-
-Below are the last 50 log lines. The full log is also available.
-
-[% IF reload %]
-
-[% END %]
-
-
-[% HTML.escape(contents) %]
-
-
-[% END %]
diff --git a/src/root/static/css/hydra.css b/src/root/static/css/hydra.css
index f4cf1d8b..a871485e 100644
--- a/src/root/static/css/hydra.css
+++ b/src/root/static/css/hydra.css
@@ -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;
}
diff --git a/src/root/static/js/common.js b/src/root/static/js/common.js
index e204ecb4..8296cadb 100644
--- a/src/root/static/js/common.js
+++ b/src/root/static/js/common.js
@@ -120,24 +120,48 @@ function escapeHTML(s) {
return $('').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) {
diff --git a/src/root/steps.tt b/src/root/steps.tt
index 26260ce7..669ec552 100644
--- a/src/root/steps.tt
+++ b/src/root/steps.tt
@@ -25,7 +25,7 @@ order of descending finish time.
[% step.drvpath.match('-(.*).drv').0 %] |
[% INCLUDE renderFullJobNameOfBuild build=step.build %] |
[% step.build.id %] |
- [% step.stepnr %] |
+ [% step.stepnr %] |
[% INCLUDE renderRelativeDate timestamp=step.stoptime %] |
[% INCLUDE renderDuration duration = step.stoptime - step.starttime %] |
[% INCLUDE renderMachineName machine=step.machine %] |