fix: service logs

This commit is contained in:
Andras Bacsai 2022-10-20 10:42:47 +02:00
parent b4f17ac3c6
commit 9f3732d35b
9 changed files with 105 additions and 116 deletions

View File

@ -42,6 +42,7 @@ async function weblate(service: any) {
`WEBLATE_SITE_DOMAIN@@@$$generate_domain`, `WEBLATE_SITE_DOMAIN@@@$$generate_domain`,
`POSTGRES_USER@@@${postgresqlUser}`, `POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DATABASE@@@${postgresqlDatabase}`, `POSTGRES_DATABASE@@@${postgresqlDatabase}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
`POSTGRES_HOST@@@$$id-postgres`, `POSTGRES_HOST@@@$$id-postgres`,
`POSTGRES_PORT@@@5432`, `POSTGRES_PORT@@@5432`,
`REDIS_HOST@@@$$id-redis`, `REDIS_HOST@@@$$id-redis`,

View File

@ -725,18 +725,22 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
} }
// Generate files for builds // Generate files for builds
if (template.services[service].build) { if (template.services[service]?.extras?.files?.length > 0) {
if (template.services[service]?.extras?.files?.length > 0) { if (!template.services[service].build) {
let Dockerfile = ` template.services[service].build = {
FROM ${template.services[service].image}` context: workdir,
for (const file of template.services[service].extras.files) { dockerfile: `Dockerfile.${service}`
const { source, destination, content } = file;
await fs.writeFile(source, content);
Dockerfile += `
COPY ./${path.basename(source)} ${destination}`
} }
await fs.writeFile(`${workdir}/Dockerfile.${service}`, Dockerfile);
} }
let Dockerfile = `
FROM ${template.services[service].image}`
for (const file of template.services[service].extras.files) {
const { source, destination, content } = file;
await fs.writeFile(source, content);
Dockerfile += `
COPY ./${path.basename(source)} ${destination}`
}
await fs.writeFile(`${workdir}/Dockerfile.${service}`, Dockerfile);
} }
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)

View File

@ -21,8 +21,8 @@ export default [
`WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password`, `WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password`,
`POSTGRES_PASSWORD=$$secret_postgres_password`, `POSTGRES_PASSWORD=$$secret_postgres_password`,
`POSTGRES_USER=$$config_postgres_user`, `POSTGRES_USER=$$config_postgres_user`,
`POSTGRES_DATABASE=$$config_postgres_db`, `POSTGRES_DATABASE=$$config_postgres_database`,
`POSTGRES_HOST=$$id-postgres`, `POSTGRES_HOST=$$id-postgresql`,
`POSTGRES_PORT=5432`, `POSTGRES_PORT=5432`,
`REDIS_HOST=$$id-redis`, `REDIS_HOST=$$id-redis`,
], ],
@ -59,7 +59,7 @@ export default [
{ {
"id": "$$config_weblate_site_domain", "id": "$$config_weblate_site_domain",
"name": "WEBLATE_SITE_DOMAIN", "name": "WEBLATE_SITE_DOMAIN",
"label": "Weblate domain", "label": "Weblate Domain",
"defaultValue": "$$generate_domain", "defaultValue": "$$generate_domain",
"description": "", "description": "",
}, },
@ -69,6 +69,9 @@ export default [
"label": "Weblate Admin Password", "label": "Weblate Admin Password",
"defaultValue": "$$generate_password", "defaultValue": "$$generate_password",
"description": "", "description": "",
"extras": {
"isVisibleOnUI": true,
}
}, },
{ {
"id": "$$config_postgres_user", "id": "$$config_postgres_user",
@ -81,16 +84,23 @@ export default [
"id": "$$secret_postgres_password", "id": "$$secret_postgres_password",
"name": "POSTGRES_PASSWORD", "name": "POSTGRES_PASSWORD",
"label": "PostgreSQL Password", "label": "PostgreSQL Password",
"defaultValue": "", "defaultValue": "$$generate_password",
"description": "", "description": "",
}, },
{ {
"id": "$$config_postgres_db", "id": "$$config_postgres_db",
"name": "POSTGRES_DB", "name": "POSTGRES_DB",
"label": "PostgreSQL Database", "label": "PostgreSQL Database",
"defaultValue": "hasura", "defaultValue": "weblate",
"description": "", "description": "",
}, },
{
"id": "$$config_postgres_database",
"name": "POSTGRES_DATABASE",
"label": "PostgreSQL Database",
"defaultValue": "$$config_postgres_db",
"description": ""
},
] ]
}, },
{ {
@ -102,10 +112,6 @@ export default [
"services": { "services": {
"$$id": { "$$id": {
"name": "SearXNG", "name": "SearXNG",
"build": {
context: "$$workdir",
dockerfile: "Dockerfile.$$id"
},
"depends_on": [ "depends_on": [
"$$id-redis" "$$id-redis"
], ],
@ -511,10 +517,6 @@ export default [
"$$id-postgresql": { "$$id-postgresql": {
"name": "PostgreSQL", "name": "PostgreSQL",
"documentation": "Official docs are [here](https://umami.is/docs/getting-started)", "documentation": "Official docs are [here](https://umami.is/docs/getting-started)",
"build": {
context: "$$workdir",
dockerfile: "Dockerfile.$$id-postgresql"
},
"depends_on": [], "depends_on": [],
"image": "postgres:12-alpine", "image": "postgres:12-alpine",
"volumes": [ "volumes": [
@ -1371,10 +1373,6 @@ export default [
"$$id-clickhouse": { "$$id-clickhouse": {
"name": "Clickhouse", "name": "Clickhouse",
"documentation": "Taken from https://plausible.io/", "documentation": "Taken from https://plausible.io/",
"build": {
context: "$$workdir",
dockerfile: "Dockerfile.$$id-clickhouse"
},
"volumes": [ "volumes": [
'$$id-clickhouse-data:/var/lib/clickhouse', '$$id-clickhouse-data:/var/lib/clickhouse',
], ],
@ -1455,7 +1453,7 @@ export default [
"defaultValue": "$$generate_password", "defaultValue": "$$generate_password",
"description": "This is the admin password. Please change it.", "description": "This is the admin password. Please change it.",
"extras": { "extras": {
"isVisibleOnUI": true "isVisibleOnUI": true,
} }
}, },
{ {

View File

@ -157,9 +157,9 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
const { name, value } = setting const { name, value } = setting
const regex = new RegExp(`\\$\\$config_${name}\\"`, 'gi') const regex = new RegExp(`\\$\\$config_${name}\\"`, 'gi')
if (service.fqdn && value === '$$generate_fqdn') { if (service.fqdn && value === '$$generate_fqdn') {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, service.fqdn+ "\"")) parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, service.fqdn + "\""))
} else if (service.fqdn && value === '$$generate_domain') { } else if (service.fqdn && value === '$$generate_domain') {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, getDomain(service.fqdn)+ "\"")) parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, getDomain(service.fqdn) + "\""))
} else { } else {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, value + "\"")) parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, value + "\""))
@ -363,7 +363,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
} }
export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) { export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
try { try {
const { id } = request.params; const { id, containerId } = request.params;
let { since = 0 } = request.query let { since = 0 } = request.query
if (since !== 0) { if (since !== 0) {
since = day(since).unix(); since = day(since).unix();
@ -374,10 +374,8 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
}); });
if (destinationDockerId) { if (destinationDockerId) {
try { try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi') const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout) const logs = stripLogsStderr.concat(stripLogsStdout)
@ -385,7 +383,10 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
return { logs: sortedLogs } return { logs: sortedLogs }
// } // }
} catch (error) { } catch (error) {
const { statusCode } = error; const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true }
}
if (statusCode === 404) { if (statusCode === 404) {
return { return {
logs: [] logs: []

View File

@ -70,7 +70,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveServiceDestination>('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply)); fastify.post<SaveServiceDestination>('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply));
fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request)); fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request));
fastify.get<GetServiceLogs>('/:id/logs', async (request) => await getServiceLogs(request)); // fastify.get<GetServiceLogs>('/:id/logs', async (request) => await getServiceLogs(request));
fastify.get<GetServiceLogs>('/:id/logs/:containerId', async (request) => await getServiceLogs(request));
fastify.post<ServiceStartStop>('/:id/start', async (request) => await startService(request)); fastify.post<ServiceStartStop>('/:id/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request)); fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));

View File

@ -15,9 +15,13 @@ export interface SaveServiceDestination extends OnlyId {
destinationId: string destinationId: string
} }
} }
export interface GetServiceLogs extends OnlyId { export interface GetServiceLogs{
Params: {
id: string,
containerId: string
},
Querystring: { Querystring: {
since: number since: number,
} }
} }
export interface SaveServiceSettings extends OnlyId { export interface SaveServiceSettings extends OnlyId {

View File

@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let application: any = {}; let application: any = {};
let logsLoading = false; let logsLoading = false;
@ -137,12 +135,13 @@
{:else} {:else}
<div class="relative w-full" /> <div class="relative w-full" />
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
<button {#if loadLogsInterval}
on:click={followBuild} <button id="streaming" class="btn btn-sm bg-transparent border-none loading"
class="btn btn-sm bg-coollabs" >Streaming logs</button
class:bg-coolgray-300={followingLogs} >
class:text-applications={followingLogs} {/if}
> <div class="flex-1" />
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2" class="w-6 h-6 mr-2"
@ -161,11 +160,6 @@
</svg> </svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'} {followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div> </div>
<div <div
bind:this={logsEl} bind:this={logsEl}

View File

@ -187,36 +187,30 @@
<div class="title lg:pb-10"> <div class="title lg:pb-10">
<div class="flex justify-center items-center space-x-2"> <div class="flex justify-center items-center space-x-2">
<div> <div>
{#if $page.url.pathname === `/services/${id}/secrets`} {#if $page.url.pathname === `/services/${id}/configuration/type`}
Secrets
{:else if $page.url.pathname === `/services/${id}/storages`}
Persistent Storages
{:else if $page.url.pathname === `/services/${id}/logs`}
Service Logs
{:else if $page.url.pathname === `/services/${id}/configuration/type`}
Select a Service Type Select a Service Type
{:else if $page.url.pathname === `/services/${id}/configuration/version`} {:else if $page.url.pathname === `/services/${id}/configuration/version`}
Select a Service Version Select a Service Version
{:else if $page.url.pathname === `/services/${id}/configuration/destination`} {:else if $page.url.pathname === `/services/${id}/configuration/destination`}
Select a Destination Select a Destination
{:else} {:else}
Configurations <div class="flex justify-center items-center space-x-2">
<div>Configurations</div>
<div
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.service.overallStatus === 'healthy'}
class:text-yellow-400={$status.service.overallStatus === 'degraded'}
class:text-red-500={$status.service.overallStatus === 'stopped'}
>
{$status.service.overallStatus === 'healthy'
? 'Healthy'
: $status.service.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
</div>
{/if} {/if}
</div> </div>
{#if !$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<div
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.service.overallStatus === 'healthy'}
class:text-yellow-400={$status.service.overallStatus === 'degraded'}
class:text-red-500={$status.service.overallStatus === 'stopped'}
>
{$status.service.overallStatus === 'healthy'
? 'Healthy'
: $status.service.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
{/if}
</div> </div>
</div> </div>
{#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)} {#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)}
@ -310,11 +304,11 @@
on:click={stopService} on:click={stopService}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 " class="w-6 h-6 text-error"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -381,7 +375,7 @@
<Menu {service} /> <Menu {service} />
</nav> </nav>
{/if} {/if}
<div class="pt-0 col-span-0 lg:col-span-3 pb-24"> <div class="pt-0 col-span-0 lg:col-span-3 pb-24 px-4 lg:px-0">
<slot /> <slot />
</div> </div>
</div> </div>

View File

@ -1,11 +1,8 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let service: any = {}; let service: any = {};
let template: any = null; let template: any = null;
@ -18,41 +15,32 @@
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
let selectedService: any = null; let selectedService: any = null;
let noContainer = false;
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/services/${id}`); const response = await get(`/services/${id}`);
template = response.template; template = response.template;
service = response.service; service = response.service;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}); });
onDestroy(() => { onDestroy(() => {
clearInterval(loadLogsInterval); clearInterval(loadLogsInterval);
clearInterval(followingInterval); clearInterval(followingInterval);
}); });
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/services/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
} catch (error) {
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() { async function loadLogs() {
if (logsLoading) return; if (logsLoading) return;
try { try {
const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`); const newLogs: any = await get(
`/services/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1]; lastLog = newLogs.logs[newLogs.logs.length - 1];
@ -100,9 +88,14 @@
} }
</script> </script>
{#if template} <div class="mx-auto w-full">
<div class="flex gap-2 lg:gap-8 pb-4"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
{#each Object.keys(template.services) as service} <div class="title font-bold pb-3">Service Logs</div>
</div>
</div>
<div class="flex gap-2 lg:gap-8 pb-4">
{#if template}
{#each Object.keys(template) as service}
<button <button
on:click={() => selectService(service, true)} on:click={() => selectService(service, true)}
class:bg-primary={selectedService === service} class:bg-primary={selectedService === service}
@ -112,21 +105,25 @@
{service}</button {service}</button
> >
{/each} {/each}
</div> {/if}
{/if} </div>
{#if selectedService} {#if selectedService}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> {#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
<button {#if loadLogsInterval}
on:click={followBuild} <button id="streaming" class="btn btn-sm bg-transparent border-none loading"
class="btn btn-sm bg-coollabs" >Streaming logs</button
class:bg-coolgray-300={followingLogs} >
class:text-applications={followingLogs} {/if}
> <div class="flex-1" />
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2" class="w-6 h-6 mr-2"
@ -145,11 +142,6 @@
</svg> </svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'} {followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div> </div>
<div <div
bind:this={logsEl} bind:this={logsEl}