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