Log support

This commit is contained in:
jamesread 2021-07-19 08:13:37 +01:00
commit c072e16cc4
10 changed files with 197 additions and 36 deletions

View File

@ -22,10 +22,22 @@ message StartActionRequest {
} }
message StartActionResponse { message StartActionResponse {
string stdout = 1; LogEntry logEntry = 1;
string stderr = 2; }
bool timedOut = 3;
int32 exitCode = 4; message GetLogsRequest{};
message LogEntry {
string datetime = 1;
string actionTitle = 2;
string stdout = 3;
string stderr = 4;
bool timedOut = 5;
int32 exitCode = 6;
}
message GetLogsResponse {
repeated LogEntry logs = 1;
} }
service OliveTinApi { service OliveTinApi {
@ -41,4 +53,9 @@ service OliveTinApi {
}; };
} }
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
option (google.api.http) = {
get: "/api/GetLogs"
};
}
} }

View File

@ -11,8 +11,22 @@ import (
"time" "time"
) )
type InternalLogEntry struct {
Datetime string
Content string
Stdout string
Stderr string
TimedOut bool
ExitCode int32
ActionTitle string
}
type Executor struct {
Logs []InternalLogEntry
}
// ExecAction executes an action. // ExecAction executes an action.
func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse { func (e *Executor) ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionName": action, "actionName": action,
}).Infof("StartAction") }).Infof("StartAction")
@ -23,16 +37,31 @@ func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
log.Errorf("Error finding action %s, %s", err, action) log.Errorf("Error finding action %s, %s", err, action)
return &pb.StartActionResponse{ return &pb.StartActionResponse{
TimedOut: false, LogEntry: nil,
} }
} }
return execAction(cfg, actualAction) res := execAction(cfg, actualAction)
e.Logs = append(e.Logs, *res);
return &pb.StartActionResponse{
LogEntry: &pb.LogEntry {
ActionTitle: actualAction.Title,
TimedOut: res.TimedOut,
Stderr: res.Stderr,
Stdout: res.Stdout,
ExitCode: res.ExitCode,
},
};
} }
func execAction(cfg *config.Config, actualAction *config.ActionButton) *pb.StartActionResponse { func execAction(cfg *config.Config, actualAction *config.ActionButton) *InternalLogEntry {
res := &pb.StartActionResponse{} res := &InternalLogEntry {
res.TimedOut = false Datetime: time.Now().Format("2006-01-02 15:04:05"),
TimedOut: false,
ActionTitle: actualAction.Title,
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"title": actualAction.Title, "title": actualAction.Title,

View File

@ -15,6 +15,7 @@ import (
var ( var (
cfg *config.Config cfg *config.Config
ex = executor.Executor{}
) )
type oliveTinAPI struct { type oliveTinAPI struct {
@ -22,7 +23,7 @@ type oliveTinAPI struct {
} }
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
return executor.ExecAction(cfg, req.ActionName), nil return ex.ExecAction(cfg, req.ActionName), nil
} }
func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) { func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) {
@ -43,6 +44,25 @@ func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (
return res, nil return res, nil
} }
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) {
ret := &pb.GetLogsResponse{};
// TODO Limit to 10 entries or something to prevent browser lag.
for _, logEntry := range ex.Logs {
ret.Logs = append(ret.Logs, &pb.LogEntry{
ActionTitle: logEntry.ActionTitle,
Datetime: logEntry.Datetime,
Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut,
ExitCode: logEntry.ExitCode,
})
}
return ret, nil
}
// Start will start the GRPC API. // Start will start the GRPC API.
func Start(globalConfig *config.Config) { func Start(globalConfig *config.Config) {
cfg = globalConfig cfg = globalConfig

View File

@ -12,9 +12,27 @@
<body> <body>
<main title = "main content"> <main title = "main content">
<fieldset id = "rootGroup"> <fieldset id = "switcher">
<legend>OliveTin Actions</legend> <button id = "showActions">Actions</button>
<button id = "showLogs">Logs</button>
</fieldset> </fieldset>
<section id = "contentLogs" title = "Logs" hidden>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Log</th>
</tr>
</thead>
<tbody id = "logTableBody" />
</table>
</section>
<section id = "contentActions" title = "Actions" hidden >
<fieldset id = "rootGroup">
</fieldset>
</section>
</main> </main>
<footer title = "footer"> <footer title = "footer">
<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p> <p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
@ -29,6 +47,22 @@
<p role = "title" class = "title">Untitled Button</p> <p role = "title" class = "title">Untitled Button</p>
</template> </template>
<template id = "tplLogRow">
<tr>
<td class = "timestamp">?</td>
<td>
<span class = "content">?</span>
<details>
<summary>stdout</summary>
<pre>
?
</pre>
</details>
</td>
</tr>
</template>
<script type = "module" src = "main.js"></script> <script type = "module" src = "main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,3 +1,5 @@
import { marshalLogsJsonToHtml } from './marshaller.js';
class ActionButton extends window.HTMLButtonElement { class ActionButton extends window.HTMLButtonElement {
constructFromJson (json) { constructFromJson (json) {
this.updateIterationTimestamp = 0; this.updateIterationTimestamp = 0;
@ -40,10 +42,12 @@ class ActionButton extends window.HTMLButtonElement {
window.fetch(this.actionCallUrl).then(res => res.json() window.fetch(this.actionCallUrl).then(res => res.json()
).then((json) => { ).then((json) => {
if (json.timedOut) { marshalLogsJsonToHtml({"logs": [json.logEntry]})
if (json.logEntry.timedOut) {
this.onActionResult('actionTimeout', 'Timed out') this.onActionResult('actionTimeout', 'Timed out')
} else if (json.exitCode !== 0) { } else if (json.logEntry.exitCode !== 0) {
this.onActionResult('actionNonZeroExit', 'Exit code ' + json.exitCode) this.onActionResult('actionNonZeroExit', 'Exit code ' + json.logEntry.exitCode)
} else { } else {
this.onActionResult('actionSuccess', 'Success!') this.onActionResult('actionSuccess', 'Success!')
} }
@ -56,6 +60,10 @@ class ActionButton extends window.HTMLButtonElement {
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]' this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
this.updateHtml() this.updateHtml()
this.classList.add(cssClass) this.classList.add(cssClass)
setTimeout(() => {
this.classList.remove(cssClass)
}, 1000);
} }
onActionError (err) { onActionError (err) {
@ -64,6 +72,10 @@ class ActionButton extends window.HTMLButtonElement {
this.isWaiting = false this.isWaiting = false
this.updateHtml() this.updateHtml()
this.classList.add('actionFailed') this.classList.add('actionFailed')
setTimeout(() => {
this.classList.remove('actionFailed')
}, 1000);
} }
constructTemplate () { constructTemplate () {

View File

@ -1,14 +0,0 @@
window.logs = []
export function addToLog (evt) {
window.logs.append(evt)
showLog(evt)
}
function showLog (evt) {
let msg = document.createElement('pre')
msg.innerText = evt;
document.body.appendChild(msg)
}

View File

@ -16,13 +16,25 @@ export function marshalActionButtonsJsonToHtml (json) {
htmlButton.updateHtml() htmlButton.updateHtml()
} }
console.log("action", jsonButton.title)
htmlButton.updateIterationTimestamp = currentIterationTimestamp; htmlButton.updateIterationTimestamp = currentIterationTimestamp;
} }
for (const existingButton of document.querySelectorAll('button')) { for (const existingButton of document.querySelector('#contentActions').querySelectorAll('button')) {
if (existingButton.updateIterationTimestamp != currentIterationTimestamp) { if (existingButton.updateIterationTimestamp != currentIterationTimestamp) {
existingButton.remove(); existingButton.remove();
} }
} }
} }
export function marshalLogsJsonToHtml (json) {
for (const logEntry of json.logs) {
const tpl = document.getElementById('tplLogRow')
const row = tpl.content.cloneNode(true)
row.querySelector('.timestamp').innerText = logEntry.datetime
row.querySelector('.content').innerText = logEntry.actionTitle
row.querySelector('pre').innerText = logEntry.stdout
document.querySelector('#logTableBody').prepend(row)
}
}

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
import { marshalActionButtonsJsonToHtml } from './js/marshaller.js' import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
function showBigError (type, friendlyType, message) { function showBigError (type, friendlyType, message) {
clearInterval(window.buttonInterval) clearInterval(window.buttonInterval)
@ -14,10 +14,26 @@ function showBigError (type, friendlyType, message) {
document.getElementById('rootGroup').appendChild(domErr) document.getElementById('rootGroup').appendChild(domErr)
} }
function showSection (name) {
for (let otherName of ["Actions", "Logs"]) {
document.getElementById('show' + otherName).classList.remove('activeSection');
document.getElementById('content' + otherName).hidden = true;
}
document.getElementById('show' + name).classList.add('activeSection')
document.getElementById('content' + name).hidden = false;
}
function setupSections() {
document.getElementById('showActions').onclick = () => { showSection('Actions') };
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
showSection('Actions');
}
function fetchGetButtons() { function fetchGetButtons() {
window.fetch(window.restBaseUrl + 'GetButtons', { window.fetch(window.restBaseUrl + 'GetButtons', {
cors: 'cors' cors: 'cors'
// No fetch options
}).then(res => { }).then(res => {
return res.json() return res.json()
}).then(res => { }).then(res => {
@ -27,6 +43,18 @@ function fetchGetButtons() {
}) })
} }
function fetchGetLogs() {
window.fetch(window.restBaseUrl + 'GetLogs', {
cors: 'cors'
}).then(res => {
return res.json()
}).then(res => {
marshalLogsJsonToHtml(res)
}).catch(err => {
showBigError('fetch-buttons', 'getting buttons', err, 'blat')
})
}
function processWebuiSettingsJson (settings) { function processWebuiSettingsJson (settings) {
window.restBaseUrl = settings.Rest window.restBaseUrl = settings.Rest
@ -40,14 +68,18 @@ function processWebuiSettingsJson (settings) {
} }
} }
setupSections();
window.fetch('webUiSettings.json').then(res => { window.fetch('webUiSettings.json').then(res => {
return res.json() return res.json()
}).then(res => { }).then(res => {
processWebuiSettingsJson(res) processWebuiSettingsJson(res)
fetchGetButtons() fetchGetButtons()
fetchGetLogs()
window.buttonInterval = setInterval(fetchGetButtons, 3000); window.buttonInterval = setInterval(fetchGetButtons, 3000);
}).catch(err => { }).catch(err => {
showBigError('fetch-webui-settings', 'getting webui settings', err) showBigError('fetch-webui-settings', 'getting webui settings', err)
}) })

View File

@ -15,7 +15,6 @@ fieldset#rootGroup {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-template-rows: auto auto auto auto; grid-template-rows: auto auto auto auto;
padding: 1em;
grid-gap: 1em; grid-gap: 1em;
text-align: center; text-align: center;
border: 0; border: 0;
@ -231,3 +230,20 @@ details[open] {
margin-top: 1em; margin-top: 1em;
display: block; display: block;
} }
main {
padding: 1em;
}
summary {
cursor: pointer;
}
details {
display: inline-block;
}
details[open] {
margin-top: 1em;
display: block;
}

View File

@ -0,0 +1,3 @@
body {
color: green;
}