improve insturmentation / serror

This commit is contained in:
mbecker20
2024-04-12 03:07:09 -07:00
parent 7bbd22b4b2
commit 46ee857c22
10 changed files with 239 additions and 104 deletions

8
Cargo.lock generated
View File

@@ -2896,9 +2896,9 @@ dependencies = [
[[package]] [[package]]
name = "resolver_api" name = "resolver_api"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9245971c55ef05018019a1da642fce51439df2b4336369144a7bb866e91f79cf" checksum = "43981e6bc9a85f1072ffd38bcaca5823e3cd7f24bb675fa5e79736a318f1f998"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3251,9 +3251,9 @@ dependencies = [
[[package]] [[package]]
name = "serror" name = "serror"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d3df9a2b74d806ecbe70d5156f436edca357e458b8d970ce88cab324a599190" checksum = "ac699ec20dda9d3630e499777c7822d9811beafbedf3b6305586b7338eb5a50e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.4", "axum 0.7.4",

View File

@@ -17,7 +17,7 @@ logger = { path = "lib/logger" }
# MOGH # MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] } run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.1.9", features = ["axum"] } serror = { version = "0.1.10", features = ["axum"] }
slack = { version = "0.1.0", package = "slack_client_rs" } slack = { version = "0.1.0", package = "slack_client_rs" }
derive_default_builder = "0.1.8" derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0" derive_empty_traits = "0.1.0"
@@ -27,7 +27,7 @@ async_timing_util = "0.1.14"
partial_derive2 = "0.2.2" partial_derive2 = "0.2.2"
derive_variants = "0.1.3" derive_variants = "0.1.3"
mongo_indexed = "0.2.2" mongo_indexed = "0.2.2"
resolver_api = "0.1.8" resolver_api = "0.1.9"
parse_csl = "0.1.0" parse_csl = "0.1.0"
mungos = "0.5.4" mungos = "0.5.4"
svi = "0.1.4" svi = "0.1.4"

View File

@@ -1,5 +1,6 @@
use std::{sync::OnceLock, time::Instant}; use std::{sync::OnceLock, time::Instant};
use anyhow::anyhow;
use async_trait::async_trait; use async_trait::async_trait;
use axum::{http::HeaderMap, routing::post, Json, Router}; use axum::{http::HeaderMap, routing::post, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{headers::ContentType, TypedHeader};
@@ -58,11 +59,15 @@ async fn handler(
let timer = Instant::now(); let timer = Instant::now();
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
debug!("/auth request {req_id} | METHOD: {}", request.req_type()); debug!("/auth request {req_id} | METHOD: {}", request.req_type());
let res = State.resolve_request(request, headers).await; let res = State.resolve_request(request, headers).await.map_err(
if let Err(resolver_api::Error::Serialization(e)) = &res { |e| match e {
debug!("/auth request {req_id} | serialization error: {e:?}"); resolver_api::Error::Serialization(e) => {
} anyhow!("{e:?}").context("response serialization error")
if let Err(resolver_api::Error::Inner(e)) = &res { }
resolver_api::Error::Inner(e) => e,
},
);
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {e:#}"); debug!("/auth request {req_id} | error: {e:#}");
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();

View File

@@ -1,6 +1,6 @@
use std::time::Instant; use std::time::Instant;
use anyhow::Context; use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Json, Router}; use axum::{middleware, routing::post, Extension, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{headers::ContentType, TypedHeader};
use monitor_client::{api::execute::*, entities::user::User}; use monitor_client::{api::execute::*, entities::user::User};
@@ -54,38 +54,52 @@ pub fn router() -> Router {
.layer(middleware::from_fn(auth_request)) .layer(middleware::from_fn(auth_request))
} }
#[instrument(name = "ExecuteHandler", skip(user))]
async fn handler( async fn handler(
Extension(user): Extension<User>, Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>, Json(request): Json<ExecuteRequest>,
) -> AppResult<(TypedHeader<ContentType>, String)> { ) -> AppResult<(TypedHeader<ContentType>, String)> {
let timer = Instant::now();
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
info!(
"/execute request {req_id} | user: {} ({})", let res = tokio::spawn(task(req_id, request, user))
user.username, user.id .await
); .context("failure in spawned execute task");
let res = tokio::spawn(async move {
State.resolve_request(request, user).await
})
.await
.context("failure in spawned execute task");
if let Err(e) = &res { if let Err(e) = &res {
warn!("/execute request {req_id} spawn error: {e:#}",); warn!("/execute request {req_id} spawn error: {e:#}",);
} }
let res = res?; AppResult::Ok((TypedHeader(ContentType::json()), res??))
}
if let Err(resolver_api::Error::Serialization(e)) = &res { #[instrument(name = "ExecuteTask")]
warn!("/execute request {req_id} serialization error: {e:?}"); async fn task(
} req_id: Uuid,
if let Err(resolver_api::Error::Inner(e)) = &res { request: ExecuteRequest,
user: User,
) -> anyhow::Result<String> {
info!(
"/execute request {req_id} | user: {} ({})",
user.username, user.id
);
let timer = Instant::now();
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("/execute request {req_id} error: {e:#}"); warn!("/execute request {req_id} error: {e:#}");
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();
info!("/execute request {req_id} | resolve time: {elapsed:?}"); info!("/execute request {req_id} | resolve time: {elapsed:?}");
AppResult::Ok((TypedHeader(ContentType::json()), res?)) res
} }

View File

@@ -1,5 +1,6 @@
use std::time::Instant; use std::time::Instant;
use anyhow::anyhow;
use async_trait::async_trait; use async_trait::async_trait;
use axum::{middleware, routing::post, Extension, Json, Router}; use axum::{middleware, routing::post, Extension, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{headers::ContentType, TypedHeader};
@@ -150,11 +151,17 @@ async fn handler(
"/read request {req_id} | user: {} ({})", "/read request {req_id} | user: {} ({})",
user.username, user.id user.username, user.id
); );
let res = State.resolve_request(request, user).await; let res =
if let Err(resolver_api::Error::Serialization(e)) = &res { State
warn!("/read request {req_id} serialization error: {e:?}"); .resolve_request(request, user)
} .await
if let Err(resolver_api::Error::Inner(e)) = &res { .map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("/read request {req_id} error: {e:#}"); warn!("/read request {req_id} error: {e:#}");
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();

View File

@@ -1,6 +1,6 @@
use std::time::Instant; use std::time::Instant;
use anyhow::Context; use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Json, Router}; use axum::{middleware, routing::post, Extension, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{headers::ContentType, TypedHeader};
use monitor_client::{api::write::*, entities::user::User}; use monitor_client::{api::write::*, entities::user::User};
@@ -115,33 +115,49 @@ async fn handler(
Extension(user): Extension<User>, Extension(user): Extension<User>,
Json(request): Json<WriteRequest>, Json(request): Json<WriteRequest>,
) -> AppResult<(TypedHeader<ContentType>, String)> { ) -> AppResult<(TypedHeader<ContentType>, String)> {
let timer = Instant::now();
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
info!(
"/write request {req_id} | user: {} ({})", let res = tokio::spawn(task(req_id, request, user))
user.username, user.id .await
); .context("failure in spawned task");
let res = tokio::spawn(async move {
State.resolve_request(request, user).await
})
.await
.context("failure in spawned task");
if let Err(e) = &res { if let Err(e) = &res {
warn!("/write request {req_id} spawn error: {e:#}"); warn!("/write request {req_id} spawn error: {e:#}");
} }
let res = res?; AppResult::Ok((TypedHeader(ContentType::json()), res??))
}
if let Err(resolver_api::Error::Serialization(e)) = &res { #[instrument(name = "WriteTask")]
warn!("/write request {req_id} serialization error: {e:?}"); async fn task(
} req_id: Uuid,
if let Err(resolver_api::Error::Inner(e)) = &res { request: WriteRequest,
user: User,
) -> anyhow::Result<String> {
info!(
"/write request {req_id} | user: {} ({})",
user.username, user.id
);
let timer = Instant::now();
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("/write request {req_id} error: {e:#}"); warn!("/write request {req_id} error: {e:#}");
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();
info!("/write request {req_id} | resolve time: {elapsed:?}"); info!("/write request {req_id} | resolve time: {elapsed:?}");
AppResult::Ok((TypedHeader(ContentType::json()), res?)) res
} }

View File

@@ -1,5 +1,6 @@
use std::time::Instant; use std::time::Instant;
use anyhow::{anyhow, Context};
use axum::Json; use axum::Json;
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{headers::ContentType, TypedHeader};
use resolver_api::Resolver; use resolver_api::Resolver;
@@ -8,33 +9,47 @@ use uuid::Uuid;
use crate::State; use crate::State;
#[instrument(name = "PeripheryHandler")]
pub async fn handler( pub async fn handler(
Json(request): Json<crate::api::PeripheryRequest>, Json(request): Json<crate::api::PeripheryRequest>,
) -> AppResult<(TypedHeader<ContentType>, String)> { ) -> AppResult<(TypedHeader<ContentType>, String)> {
let timer = Instant::now();
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
info!("request {req_id} | {request:?}");
let res =
tokio::spawn(
async move { State.resolve_request(request, ()).await },
)
.await;
let elapsed = timer.elapsed(); let res = tokio::spawn(task(req_id, request))
info!("request {req_id} | resolve time: {elapsed:?}"); .await
.context("task handler spawn error");
if let Err(e) = &res { if let Err(e) = &res {
warn!("request {req_id} spawn error: {e:#}"); warn!("request {req_id} spawn error: {e:#}");
} }
let res = res?; AppResult::Ok((TypedHeader(ContentType::json()), res??))
if let Err(resolver_api::Error::Serialization(e)) = &res { }
warn!("request {req_id} serialization error: {e:?}");
} #[instrument(name = "PeripheryTask")]
if let Err(resolver_api::Error::Inner(e)) = &res { async fn task(
req_id: Uuid,
request: crate::api::PeripheryRequest,
) -> anyhow::Result<String> {
info!("request {req_id} | {request:?}");
let timer = Instant::now();
let res =
State
.resolve_request(request, ())
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("request {req_id} error: {e:#}"); warn!("request {req_id} error: {e:#}");
} }
AppResult::Ok((TypedHeader(ContentType::json()), res?)) let elapsed = timer.elapsed();
info!("request {req_id} | resolve time: {elapsed:?}");
res
} }

View File

@@ -1027,6 +1027,9 @@ export type AlertData =
server_name: string; server_name: string;
from: DockerContainerState; from: DockerContainerState;
to: DockerContainerState; to: DockerContainerState;
}}
| { type: "AwsBuilderTerminationFailed", data: {
instance_id: string;
}} }}
| { type: "None", data: { | { type: "None", data: {
}}; }};
@@ -1166,7 +1169,7 @@ export interface GetLog {
export interface SearchLog { export interface SearchLog {
/** Id or name */ /** Id or name */
deployment: string; deployment: string;
search: string; terms: string[];
} }
export interface GetDeployedVersion { export interface GetDeployedVersion {

View File

@@ -1,4 +1,4 @@
import { Section } from "@components/layouts"; import { Section } from "@components/layouts";
import { useRead } from "@lib/hooks"; import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/tabs";
@@ -8,8 +8,10 @@ import {
AlertOctagon, AlertOctagon,
RefreshCw, RefreshCw,
ChevronDown, ChevronDown,
Search,
X,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { createRef, useState } from "react";
import { useDeployment } from "."; import { useDeployment } from ".";
import { import {
Select, Select,
@@ -21,6 +23,7 @@ import {
} from "@ui/select"; } from "@ui/select";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import { Input } from "@ui/input";
export const DeploymentLogs = ({ id }: { id: string }) => { export const DeploymentLogs = ({ id }: { id: string }) => {
const state = useDeployment(id)?.info.state; const state = useDeployment(id)?.info.state;
@@ -36,11 +39,22 @@ export const DeploymentLogs = ({ id }: { id: string }) => {
const DeploymentLogsInner = ({ id }: { id: string }) => { const DeploymentLogsInner = ({ id }: { id: string }) => {
const [tail, set] = useState("100"); const [tail, set] = useState("100");
const { data: logs, refetch } = useRead( const searchRef = createRef<HTMLInputElement>();
"GetLog", const [search, setSearch] = useState("");
{ deployment: id, tail: Number(tail) },
{ refetchInterval: 30000 } const updateSearch = () =>
); searchRef.current && setSearch(searchRef.current.value);
const clearSearch = () => {
if (searchRef.current) {
searchRef.current.value = "";
}
setSearch("");
};
const { Log, refetch, stderr } = search
? SearchLogs(id, search)
: NoSearchLogs(id, tail);
return ( return (
<Tabs defaultValue="stdout"> <Tabs defaultValue="stdout">
<Section <Section
@@ -48,60 +62,121 @@ const DeploymentLogsInner = ({ id }: { id: string }) => {
icon={<TerminalSquare className="w-4 h-4" />} icon={<TerminalSquare className="w-4 h-4" />}
actions={ actions={
<div className="flex gap-2"> <div className="flex gap-2">
<TabsList className="w-fit place-self-end"> <div className="relative">
<Input
ref={searchRef}
placeholder="Search Logs"
onBlur={updateSearch}
onKeyDown={(e) => {
if (e.key === "Enter") updateSearch();
}}
/>
<Button
variant="ghost"
size="icon"
onClick={clearSearch}
className="absolute right-0 top-1/2 -translate-y-1/2"
>
<X className="w-4 h-4" />
</Button>
</div>
<Button variant="outline" size="icon" onClick={updateSearch}>
<Search className="w-4 h-4" />
</Button>
<TabsList>
<TabsTrigger value="stdout" onClick={to_bottom("stdout")}> <TabsTrigger value="stdout" onClick={to_bottom("stdout")}>
stdout stdout
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="stderr" onClick={to_bottom("stderr")}> <TabsTrigger value="stderr" onClick={to_bottom("stderr")}>
stderr stderr
{logs?.stderr && ( {stderr && (
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" /> <AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
)} )}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<Button variant="secondary" onClick={() => refetch()}> <Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
</Button> </Button>
<TailLengthSelector selected={tail} onSelect={set} /> <TailLengthSelector
selected={tail}
onSelect={set}
disabled={search.length > 0}
/>
</div> </div>
} }
> >
{["stdout", "stderr"].map((t) => { {Log}
const log = logs?.[t as keyof typeof logs] as string | undefined;
return (
<TabsContent key={t} className="h-full relative" value={t}>
<div className="h-[70vh] overflow-y-auto">
<pre
id={t}
dangerouslySetInnerHTML={{
__html: log ? logToHtml(log) : `no ${t} logs`,
}}
className="-scroll-mt-24"
/>
</div>
<Button
className="absolute bottom-4 right-4"
onClick={to_bottom(t)}
>
<ChevronDown className="h-4 w-4" />
</Button>
</TabsContent>
);
})}
</Section> </Section>
</Tabs> </Tabs>
); );
}; };
const NoSearchLogs = (id: string, tail: string) => {
const { data: log, refetch } = useRead(
"GetLog",
{ deployment: id, tail: Number(tail) },
{ refetchInterval: 30000 }
);
return {
Log: <Log log={log} />,
refetch,
stderr: !!log?.stderr,
};
};
const SearchLogs = (id: string, search: string) => {
const { data: log, refetch } = useRead(
"SearchLog",
{ deployment: id, terms: search.split(" ").filter((term) => term) },
{ refetchInterval: 30000 }
);
return {
Log: <Log log={log} />,
refetch,
stderr: !!log?.stderr,
};
};
const Log = ({ log }: { log: Types.Log | undefined }) => {
return (
<>
{["stdout", "stderr"].map((stream) => {
const _log = log?.[stream as keyof typeof log] as string | undefined;
return (
<TabsContent key={stream} className="h-full relative" value={stream}>
<div className="h-[70vh] overflow-y-auto">
<pre
id={stream}
dangerouslySetInnerHTML={{
__html: _log ? logToHtml(_log) : `no ${stream} logs`,
}}
className="-scroll-mt-24"
/>
</div>
<Button
className="absolute bottom-4 right-4"
onClick={to_bottom(stream)}
>
<ChevronDown className="h-4 w-4" />
</Button>
</TabsContent>
);
})}
</>
);
};
const TailLengthSelector = ({ const TailLengthSelector = ({
selected, selected,
onSelect, onSelect,
disabled,
}: { }: {
selected: string; selected: string;
onSelect: (value: string) => void; onSelect: (value: string) => void;
disabled?: boolean;
}) => ( }) => (
<Select value={selected} onValueChange={onSelect}> <Select value={selected} onValueChange={onSelect} disabled={disabled}>
<SelectTrigger> <SelectTrigger className="w-[120px]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -77,7 +77,7 @@ export const Login = () => {
</div> </div>
<div className="flex justify-center items-center container mt-32"> <div className="flex justify-center items-center container mt-32">
<Card className="w-full max-w-[500px] place-self-center"> <Card className="w-full max-w-[500px] place-self-center">
<CardHeader className="flex justify-between"> <CardHeader className="flex-row justify-between">
<div> <div>
<CardTitle className="text-xl">Monitor</CardTitle> <CardTitle className="text-xl">Monitor</CardTitle>
<CardDescription>Log In</CardDescription> <CardDescription>Log In</CardDescription>