log table

This commit is contained in:
Cory Sanin 2025-01-13 01:58:38 -05:00
parent da8d0fcc7e
commit 68005e8492
8 changed files with 117 additions and 31 deletions

View File

@ -91,8 +91,8 @@ class BuildController extends EventEmitter {
return (data: Buffer | string) => { return (data: Buffer | string) => {
const str = data.toString(); const str = data.toString();
const readyToLog = remainder[type] + str.substring(0, str.lastIndexOf('\n')); const readyToLog = remainder[type] + str.substring(0, str.lastIndexOf('\n'));
remainder[type] = str.substring(str.lastIndexOf('\n')); remainder[type] = str.substring(str.lastIndexOf('\n') + 1);
this.db.appendLog(build.id, readyToLog); this.db.appendLog(build.id, type, readyToLog);
this.emitLog({ this.emitLog({
id: build.id, id: build.id,
type: type, type: type,
@ -153,4 +153,4 @@ class BuildController extends EventEmitter {
export default BuildController; export default BuildController;
export { BuildController }; export { BuildController };
export type { }; export type { BuildEvent, LogType };

View File

@ -1,5 +1,6 @@
import { Sequelize, DataTypes, Op, } from 'sequelize'; import { Sequelize, DataTypes, Op, } from 'sequelize';
import type { ModelStatic, Filterable } from 'sequelize'; import type { ModelStatic, Filterable } from 'sequelize';
import type { LogType } from './BuildController.ts';
type Status = 'queued' | 'running' | 'cancelled' | 'success' | 'error'; type Status = 'queued' | 'running' | 'cancelled' | 'success' | 'error';
type Dependencies = 'stable' | 'testing' | 'staging'; type Dependencies = 'stable' | 'testing' | 'staging';
@ -23,7 +24,13 @@ interface Build {
endTime?: Date; endTime?: Date;
status: Status; status: Status;
pid?: number; pid?: number;
log?: string; }
interface LogChunk {
id: number
buildId: number
type: LogType,
chunk: string
} }
const MONTH = 1000 * 60 * 60 * 24 * 24; const MONTH = 1000 * 60 * 60 * 24 * 24;
@ -37,6 +44,7 @@ const SELECT = ['id', 'repo', 'commit', 'distro', 'dependencies', 'startTime', '
class DB { class DB {
private build: ModelStatic<any>; private build: ModelStatic<any>;
private logChunk: ModelStatic<any>;
private sequelize: Sequelize; private sequelize: Sequelize;
constructor(config: DBConfig = {}) { constructor(config: DBConfig = {}) {
@ -88,14 +96,42 @@ class DB {
pid: { pid: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true
},
log: {
type: DataTypes.TEXT,
allowNull: true
} }
}); });
this.build.sync(); this.logChunk = this.sequelize.define('logChunk', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
buildId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'builds',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
type: {
type: DataTypes.ENUM('std', 'err'),
allowNull: false,
defaultValue: 'std'
},
chunk: {
type: DataTypes.TEXT,
allowNull: false
}
});
this.sync();
}
private async sync(): Promise<void> {
await this.build.sync();
await this.logChunk.sync();
} }
public async createBuild(repo: string, commit: string, patch: string, distro: string): Promise<number> { public async createBuild(repo: string, commit: string, patch: string, distro: string): Promise<number> {
@ -132,13 +168,19 @@ class DB {
}); });
} }
public async appendLog(id: number, log: string): Promise<void> { public async appendLog(buildId: number, type: LogType, chunk: string): Promise<void> {
const sanitizedLog = log.replace(/'/g, "''"); await this.logChunk.create({
await this.build.update({ buildId,
log: Sequelize.literal(`log || '${sanitizedLog}'`) type,
}, { chunk
});
}
public async getLog(buildId: number): Promise<LogChunk[]> {
return await this.logChunk.findAll({
order: [['id', 'ASC']],
where: { where: {
id buildId
} }
}); });
} }
@ -151,7 +193,7 @@ class DB {
return await this.build.findAll({ return await this.build.findAll({
attributes: SELECT, attributes: SELECT,
order: [['id', 'DESC']], order: [['id', 'DESC']],
where: FRESH, where: FRESH
}); });
} }

View File

@ -21,10 +21,12 @@ class Web {
private _webserver: http.Server | null = null; private _webserver: http.Server | null = null;
private db: DB; private db: DB;
private buildController: BuildController; private buildController: BuildController;
private app: Express;
private port: number;
constructor(options: WebConfig = {}) { constructor(options: WebConfig = {}) {
const app: Express = express(); const app: Express = this.app = express();
const port: number = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080; this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080;
app.set('trust proxy', 1); app.set('trust proxy', 1);
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
@ -84,6 +86,8 @@ class Web {
res.sendStatus(404); res.sendStatus(404);
return; return;
} }
const log = (await this.db.getLog(build.id)).map(logChunk => logChunk.chunk.split('\n')).flat();
res.render('build', { res.render('build', {
page: { page: {
title: 'Archery', title: 'Archery',
@ -91,7 +95,7 @@ class Web {
description: `Building ${build.repo} on ${build.distro}` description: `Building ${build.repo} on ${build.distro}`
}, },
build, build,
log: build.log?.split('\n') log
}); });
}); });
@ -101,14 +105,13 @@ class Web {
res.sendStatus(404); res.sendStatus(404);
return; return;
} }
res.set('Content-Type', 'text/plain').send(build.log); const log = (await this.db.getLog(build.id)).map(logChunk => logChunk.chunk).join('\n');
res.set('Content-Type', 'text/plain').send(log);
}); });
app.get('/healthcheck', (_, res) => { app.get('/healthcheck', (_, res) => {
res.send('Healthy'); res.send('Healthy');
}); });
this._webserver = app.listen(port, () => console.log(`archery is running on port ${port}`));
} }
close = () => { close = () => {
@ -119,6 +122,9 @@ class Web {
setDB = (db: DB) => { setDB = (db: DB) => {
this.db = db; this.db = db;
if (!this._webserver) {
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
}
} }
setBuildController = (buildController: BuildController) => { setBuildController = (buildController: BuildController) => {

View File

@ -141,6 +141,22 @@ input[type="submit"] {
} }
} }
.sidebar_search {
padding: .3em 1em;
form input {
font-size: 1.1em;
width: 100%;
background: #222;
color: var(--color-text);
border: var(--primary-light) solid .12em;
&:hover {
background: #2a2a2a;
}
}
}
ul.sidebar_links { ul.sidebar_links {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -166,6 +182,10 @@ input[type="submit"] {
.content { .content {
margin-left: 16em; margin-left: 16em;
padding: 1em; padding: 1em;
&.no-padding {
padding: 0;
}
} }
.grid-2col { .grid-2col {
@ -196,18 +216,25 @@ input[type="submit"] {
} }
} }
.sidebar_search { .content.footer {
padding: .3em 1em; background: var(--primary-dark);
color: var(--color-text);
margin-top: .5em;
padding: .25em 1em;
form input { a {
font-size: 1.1em;
width: 100%;
background: #222;
color: var(--color-text); color: var(--color-text);
border: var(--primary-light) solid .12em; text-decoration: underline;
}
&:hover { ul {
background: #2a2a2a; list-style: none;
font-size: .65em;
li {
display: inline-block;
padding: 0 1em 0 0;
font-size: inherit;
} }
} }
} }

View File

@ -21,5 +21,6 @@
<div class="span-2"><button type="submit">Build</button></div> <div class="span-2"><button type="submit">Build</button></div>
</form> </form>
</div> </div>
<%- include("footer", locals) %>
</body> </body>
</html> </html>

View File

@ -24,5 +24,6 @@
<% }) %> <% }) %>
</div> </div>
</div> </div>
<%- include("footer", locals) %>
</body> </body>
</html> </html>

8
views/footer.ejs Normal file
View File

@ -0,0 +1,8 @@
<div class="content footer">
<footer>
<ul>
<li>Developed by Cory Sanin</li>
<li><a href="/?TODO">Source</a></li>
</ul>
</footer>
</div>

View File

@ -28,5 +28,6 @@
<% }) %> <% }) %>
</table> </table>
</div> </div>
<%- include("footer", locals) %>
</body> </body>
</html> </html>