Log support
This commit is contained in:
commit
c072e16cc4
|
|
@ -22,10 +22,22 @@ message StartActionRequest {
|
|||
}
|
||||
|
||||
message StartActionResponse {
|
||||
string stdout = 1;
|
||||
string stderr = 2;
|
||||
bool timedOut = 3;
|
||||
int32 exitCode = 4;
|
||||
LogEntry logEntry = 1;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -41,4 +53,9 @@ service OliveTinApi {
|
|||
};
|
||||
}
|
||||
|
||||
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/GetLogs"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,22 @@ import (
|
|||
"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.
|
||||
func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
|
||||
func (e *Executor) ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
|
||||
log.WithFields(log.Fields{
|
||||
"actionName": action,
|
||||
}).Infof("StartAction")
|
||||
|
|
@ -23,16 +37,31 @@ func ExecAction(cfg *config.Config, action string) *pb.StartActionResponse {
|
|||
log.Errorf("Error finding action %s, %s", err, action)
|
||||
|
||||
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 {
|
||||
res := &pb.StartActionResponse{}
|
||||
res.TimedOut = false
|
||||
func execAction(cfg *config.Config, actualAction *config.ActionButton) *InternalLogEntry {
|
||||
res := &InternalLogEntry {
|
||||
Datetime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
TimedOut: false,
|
||||
ActionTitle: actualAction.Title,
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"title": actualAction.Title,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
var (
|
||||
cfg *config.Config
|
||||
ex = executor.Executor{}
|
||||
)
|
||||
|
||||
type oliveTinAPI struct {
|
||||
|
|
@ -22,7 +23,7 @@ type oliveTinAPI struct {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -43,6 +44,25 @@ func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (
|
|||
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.
|
||||
func Start(globalConfig *config.Config) {
|
||||
cfg = globalConfig
|
||||
|
|
|
|||
|
|
@ -12,9 +12,27 @@
|
|||
|
||||
<body>
|
||||
<main title = "main content">
|
||||
<fieldset id = "rootGroup">
|
||||
<legend>OliveTin Actions</legend>
|
||||
<fieldset id = "switcher">
|
||||
<button id = "showActions">Actions</button>
|
||||
<button id = "showLogs">Logs</button>
|
||||
</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>
|
||||
<footer title = "footer">
|
||||
<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>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { marshalLogsJsonToHtml } from './marshaller.js';
|
||||
|
||||
class ActionButton extends window.HTMLButtonElement {
|
||||
constructFromJson (json) {
|
||||
this.updateIterationTimestamp = 0;
|
||||
|
|
@ -40,10 +42,12 @@ class ActionButton extends window.HTMLButtonElement {
|
|||
|
||||
window.fetch(this.actionCallUrl).then(res => res.json()
|
||||
).then((json) => {
|
||||
if (json.timedOut) {
|
||||
marshalLogsJsonToHtml({"logs": [json.logEntry]})
|
||||
|
||||
if (json.logEntry.timedOut) {
|
||||
this.onActionResult('actionTimeout', 'Timed out')
|
||||
} else if (json.exitCode !== 0) {
|
||||
this.onActionResult('actionNonZeroExit', 'Exit code ' + json.exitCode)
|
||||
} else if (json.logEntry.exitCode !== 0) {
|
||||
this.onActionResult('actionNonZeroExit', 'Exit code ' + json.logEntry.exitCode)
|
||||
} else {
|
||||
this.onActionResult('actionSuccess', 'Success!')
|
||||
}
|
||||
|
|
@ -56,6 +60,10 @@ class ActionButton extends window.HTMLButtonElement {
|
|||
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
||||
this.updateHtml()
|
||||
this.classList.add(cssClass)
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove(cssClass)
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onActionError (err) {
|
||||
|
|
@ -64,6 +72,10 @@ class ActionButton extends window.HTMLButtonElement {
|
|||
this.isWaiting = false
|
||||
this.updateHtml()
|
||||
this.classList.add('actionFailed')
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove('actionFailed')
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
constructTemplate () {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -16,13 +16,25 @@ export function marshalActionButtonsJsonToHtml (json) {
|
|||
htmlButton.updateHtml()
|
||||
}
|
||||
|
||||
console.log("action", jsonButton.title)
|
||||
htmlButton.updateIterationTimestamp = currentIterationTimestamp;
|
||||
}
|
||||
|
||||
for (const existingButton of document.querySelectorAll('button')) {
|
||||
for (const existingButton of document.querySelector('#contentActions').querySelectorAll('button')) {
|
||||
if (existingButton.updateIterationTimestamp != currentIterationTimestamp) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
import { marshalActionButtonsJsonToHtml } from './js/marshaller.js'
|
||||
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
||||
|
||||
function showBigError (type, friendlyType, message) {
|
||||
clearInterval(window.buttonInterval)
|
||||
|
|
@ -14,10 +14,26 @@ function showBigError (type, friendlyType, message) {
|
|||
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() {
|
||||
window.fetch(window.restBaseUrl + 'GetButtons', {
|
||||
window.fetch(window.restBaseUrl + 'GetButtons', {
|
||||
cors: 'cors'
|
||||
// No fetch options
|
||||
}).then(res => {
|
||||
return res.json()
|
||||
}).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) {
|
||||
window.restBaseUrl = settings.Rest
|
||||
|
||||
|
|
@ -40,14 +68,18 @@ function processWebuiSettingsJson (settings) {
|
|||
}
|
||||
}
|
||||
|
||||
setupSections();
|
||||
|
||||
window.fetch('webUiSettings.json').then(res => {
|
||||
return res.json()
|
||||
}).then(res => {
|
||||
processWebuiSettingsJson(res)
|
||||
|
||||
fetchGetButtons()
|
||||
fetchGetLogs()
|
||||
|
||||
window.buttonInterval = setInterval(fetchGetButtons, 3000);
|
||||
}).catch(err => {
|
||||
showBigError('fetch-webui-settings', 'getting webui settings', err)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ fieldset#rootGroup {
|
|||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-template-rows: auto auto auto auto;
|
||||
padding: 1em;
|
||||
grid-gap: 1em;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
|
|
@ -231,3 +230,20 @@ details[open] {
|
|||
margin-top: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
margin-top: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
color: green;
|
||||
}
|
||||
Loading…
Reference in New Issue