more stats
This commit is contained in:
parent
2cde2f57f6
commit
a3f0ddb99f
4 changed files with 151 additions and 16 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -266,6 +266,7 @@ dependencies = [
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
|
@ -281,6 +282,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1083,6 +1085,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -8,13 +8,14 @@ anyhow = "1"
|
||||||
askama = { version = "0.12", features = ["with-axum"] }
|
askama = { version = "0.12", features = ["with-axum"] }
|
||||||
askama_axum = "0.3"
|
askama_axum = "0.3"
|
||||||
axum = "0.6"
|
axum = "0.6"
|
||||||
bollard = "0.15"
|
bollard = { version = "0.15", features = ["time"] }
|
||||||
clap = { version = "4", features = ["env", "derive"] }
|
clap = { version = "4", features = ["env", "derive"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
time = { version = "0.3", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
|
@ -1,34 +1,59 @@
|
||||||
use crate::http::error::Result;
|
use crate::http::error::Result;
|
||||||
use bollard::{
|
use bollard::{
|
||||||
container::InspectContainerOptions,
|
container::InspectContainerOptions,
|
||||||
service::{ContainerInspectResponse, ContainerSummary},
|
service::{ContainerInspectResponse, ContainerSummary, MountPoint},
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ContainerInfo {
|
pub struct ContainerInfo {
|
||||||
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
pub image: String,
|
pub image: String,
|
||||||
|
pub image_id: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub volumes: Option<Vec<MountPoint>>,
|
||||||
|
pub env: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ContainerSummary> for ContainerInfo {
|
impl From<ContainerSummary> for ContainerInfo {
|
||||||
fn from(value: ContainerSummary) -> Self {
|
fn from(value: ContainerSummary) -> Self {
|
||||||
|
let created = OffsetDateTime::from_unix_timestamp(value.created.unwrap())
|
||||||
|
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||||
ContainerInfo {
|
ContainerInfo {
|
||||||
name: value.names.unwrap()[0].trim_start_matches('/').to_string(),
|
id: value.id.unwrap_or_default(),
|
||||||
state: value.state.unwrap(),
|
name: value.names.unwrap_or_default()[0]
|
||||||
image: value.image.unwrap(),
|
.trim_start_matches('/')
|
||||||
|
.to_string(),
|
||||||
|
state: value.state.unwrap_or_default(),
|
||||||
|
image: value.image.unwrap_or_default(),
|
||||||
|
image_id: value.image_id.unwrap_or_default(),
|
||||||
|
created_at: created.time().to_string(),
|
||||||
|
volumes: value.mounts,
|
||||||
|
env: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ContainerInspectResponse> for ContainerInfo {
|
impl From<ContainerInspectResponse> for ContainerInfo {
|
||||||
fn from(value: ContainerInspectResponse) -> Self {
|
fn from(value: ContainerInspectResponse) -> Self {
|
||||||
|
let config = value.config.unwrap_or_default();
|
||||||
ContainerInfo {
|
ContainerInfo {
|
||||||
name: value.name.unwrap(),
|
id: value.id.unwrap_or_default(),
|
||||||
state: value.state.and_then(|s| s.status).unwrap().to_string(),
|
name: value.name.unwrap_or_default(),
|
||||||
image: value.image.unwrap(),
|
state: value
|
||||||
|
.state
|
||||||
|
.and_then(|s| s.status)
|
||||||
|
.unwrap_or_else(|| bollard::service::ContainerStateStatusEnum::EMPTY)
|
||||||
|
.to_string(),
|
||||||
|
image: config.image.unwrap_or_default(),
|
||||||
|
image_id: value.image.unwrap_or_default(),
|
||||||
|
created_at: value.created.unwrap_or_default(),
|
||||||
|
volumes: value.mounts,
|
||||||
|
env: config.env,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,44 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<h1>Container <span class="container-name">{{container_name}}</span></h1>
|
<h1>Container <span class="container-name">{{container_name}}</span></h1>
|
||||||
<h2>Status</h2>
|
<section class="status-section">
|
||||||
<div>TODO</div>
|
<h2>Status</h2>
|
||||||
<h2>Logs</h2>
|
<dl>
|
||||||
<code id="log">
|
<dt>ID</dt>
|
||||||
</code>
|
<dd>{{ info.id }}</dd>
|
||||||
|
<dt>Status</dt>
|
||||||
|
<dd>{{ info.state }}</dd>
|
||||||
|
<dt>Image</dt>
|
||||||
|
<dd>{{ info.image }}</dd>
|
||||||
|
<dt>Image ID</dt>
|
||||||
|
<dd>{{ info.image_id }}</dd>
|
||||||
|
<dt>Created at</dt>
|
||||||
|
<dd>{{ info.created_at }}</dd>
|
||||||
|
{% match info.volumes %}
|
||||||
|
{% when Some with (volumes) %}
|
||||||
|
<dt>Volumes</dt>
|
||||||
|
{% for volume in volumes %}
|
||||||
|
<dd>
|
||||||
|
{{ volume.source.clone().unwrap_or_default() }} → {{
|
||||||
|
volume.destination.clone().unwrap_or_default() }}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
{% match info.env %}
|
||||||
|
{% when Some with (env) %}
|
||||||
|
<dt>Environment</dt>
|
||||||
|
{% for var in env %}
|
||||||
|
<dd>{{ var }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section class="log-section">
|
||||||
|
<h2>Logs</h2>
|
||||||
|
<code id="log"></code>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -23,6 +56,7 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
@ -39,7 +73,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
height: 800px;
|
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@ -59,6 +92,73 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& time {
|
||||||
|
padding-right: 1ch;
|
||||||
|
color: #B1A9FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
border: 1px solid #3D3E82;
|
||||||
|
background-color: #202248;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd~dt,
|
||||||
|
dd~dd {
|
||||||
|
border-top: 1px solid #262A65;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
float: left;
|
||||||
|
width: 25%;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-left: 25%;
|
||||||
|
border-left: 1px dotted #3D3E82;
|
||||||
|
background-color: #171625;
|
||||||
|
padding: 4px 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dd:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>h1 {
|
||||||
|
grid-area: head;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
grid-area: info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-section {
|
||||||
|
grid-area: log;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"head head head"
|
||||||
|
"info log log"
|
||||||
|
"info log log";
|
||||||
|
|
||||||
|
grid-template-rows: 80px 1fr;
|
||||||
|
grid-template-columns: 30vw 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -98,9 +198,15 @@
|
||||||
}
|
}
|
||||||
// Received lines of log
|
// Received lines of log
|
||||||
if ("lines" in data) {
|
if ("lines" in data) {
|
||||||
data.lines.split("\n").map(line => convert.toHtml(line)).filter(line => line).forEach(line => {
|
data.lines.split("\n").forEach(line => {
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Extract timestamp
|
||||||
|
const firstSpace = line.indexOf(' ')
|
||||||
|
const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)];
|
||||||
const lineEl = document.createElement("p");
|
const lineEl = document.createElement("p");
|
||||||
lineEl.innerHTML = line.trim();
|
lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
|
||||||
logEl.appendChild(lineEl);
|
logEl.appendChild(lineEl);
|
||||||
lineEl.scrollIntoView();
|
lineEl.scrollIntoView();
|
||||||
});
|
});
|
||||||
|
|
Reference in a new issue