clan-app: Add logging middleware

This commit is contained in:
Qubasa
2025-07-02 17:59:05 +07:00
parent 26680bc784
commit 9cf6cecdae
9 changed files with 144 additions and 93 deletions

View File

@@ -109,9 +109,7 @@ class _WebviewLibrary:
self.webview_return = self.lib.webview_return self.webview_return = self.lib.webview_return
self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p] self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
self.binding_callback_t = CFUNCTYPE( self.binding_callback_t = CFUNCTYPE(None, c_char_p, c_char_p, c_void_p)
None, c_char_p, c_char_p, c_void_p
)
self.CFUNCTYPE = CFUNCTYPE self.CFUNCTYPE = CFUNCTYPE

View File

@@ -66,13 +66,14 @@ class Webview:
) -> None: ) -> None:
op_key = op_key_bytes.decode() op_key = op_key_bytes.decode()
args = json.loads(request_data.decode()) args = json.loads(request_data.decode())
log.debug(f"Calling {method_name}({args[0]})") log.debug(f"Calling {method_name}({args})")
metadata: dict[str, Any] = {} header: dict[str, Any]
try: try:
# Initialize dataclasses from the payload # Initialize dataclasses from the payload
reconciled_arguments = {} reconciled_arguments = {}
if len(args) > 0: if len(args) > 1:
header = args[1]
for k, v in args[0].items(): for k, v in args[0].items():
# Some functions expect to be called with dataclass instances # Some functions expect to be called with dataclass instances
# But the js api returns dictionaries. # But the js api returns dictionaries.
@@ -83,6 +84,8 @@ class Webview:
# TODO: rename from_dict into something like construct_checked_value # TODO: rename from_dict into something like construct_checked_value
# from_dict really takes Anything and returns an instance of the type/class # from_dict really takes Anything and returns an instance of the type/class
reconciled_arguments[k] = from_dict(arg_class, v) reconciled_arguments[k] = from_dict(arg_class, v)
elif len(args) == 1:
header = args[0]
reconciled_arguments["op_key"] = op_key reconciled_arguments["op_key"] = op_key
except Exception as e: except Exception as e:
@@ -109,12 +112,12 @@ class Webview:
ctx.should_cancel = lambda: stop_event.is_set() ctx.should_cancel = lambda: stop_event.is_set()
# If the API call has set log_group in metadata, # If the API call has set log_group in metadata,
# create the log file under that group. # create the log file under that group.
log_group = metadata.get("logging", {}).get("group", None) log_group = header.get("logging", {}).get("group", None)
if log_group is not None: if log_group is not None:
log.warning( log.warning(
f"Using log group {log_group} for {method_name} with op_key {op_key}" f"Using log group {log_group} for {method_name} with op_key {op_key}"
) )
breakpoint()
log_file = log_manager.create_log_file( log_file = log_manager.create_log_file(
wrap_method, op_key=op_key, group=log_group wrap_method, op_key=op_key, group=log_group
).get_file_path() ).get_file_path()
@@ -136,10 +139,11 @@ class Webview:
try: try:
# Original logic: call the wrapped API method. # Original logic: call the wrapped API method.
result = wrap_method(**reconciled_arguments) result = wrap_method(**reconciled_arguments)
wrapped_result = {"body": dataclass_to_dict(result), "header": {}}
# Serialize the result to JSON. # Serialize the result to JSON.
serialized = json.dumps( serialized = json.dumps(
dataclass_to_dict(result), indent=4, ensure_ascii=False dataclass_to_dict(wrapped_result), indent=4, ensure_ascii=False
) )
# This log message will now also be written to log_f # This log message will now also be written to log_f
@@ -206,7 +210,6 @@ class Webview:
_webview_lib.webview_set_title(self._handle, _encode_c_string(value)) _webview_lib.webview_set_title(self._handle, _encode_c_string(value))
self._title = value self._title = value
def destroy(self) -> None: def destroy(self) -> None:
for name in list(self._callbacks.keys()): for name in list(self._callbacks.keys()):
self.unbind(name) self.unbind(name)

View File

@@ -46,21 +46,22 @@ interface BackendOpts {
} }
interface BackendReturnType<K extends OperationNames> { interface BackendReturnType<K extends OperationNames> {
result: OperationResponse<K>; body: OperationResponse<K>;
metadata: Record<string, any>; header: Record<string, any>;
} }
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: BackendOpts,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
// if window[method] does not exist, throw an error // if window[method] does not exist, throw an error
if (!(method in window)) { if (!(method in window)) {
console.error(`Method ${method} not found on window object`); console.error(`Method ${method} not found on window object`);
// return a rejected promise // return a rejected promise
return { return {
promise: Promise.resolve({ promise: Promise.resolve({
body: {
status: "error", status: "error",
errors: [ errors: [
{ {
@@ -69,17 +70,19 @@ const _callApi = <K extends OperationNames>(
}, },
], ],
op_key: "noop", op_key: "noop",
},
header: {},
}), }),
op_key: "noop", op_key: "noop",
}; };
} }
let metadata: BackendOpts | undefined = undefined; let header: BackendOpts = {};
if (backendOpts != undefined) { if (backendOpts != undefined) {
metadata = { ...backendOpts }; header = { ...backendOpts };
let group = backendOpts?.logging?.group; let group = backendOpts?.logging?.group;
if (group != undefined && isMachine(group)) { if (group != undefined && isMachine(group)) {
metadata = { header = {
logging: { group: group.flake.identifier + "#" + group.name }, logging: { group: group.flake.identifier + "#" + group.name },
}; };
} }
@@ -90,9 +93,10 @@ const _callApi = <K extends OperationNames>(
OperationNames, OperationNames,
( (
args: OperationArgs<OperationNames>, args: OperationArgs<OperationNames>,
) => Promise<OperationResponse<OperationNames>> metadata: BackendOpts,
) => Promise<BackendReturnType<OperationNames>>
> >
)[method](args) as Promise<OperationResponse<K>>; )[method](args, header) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string; const op_key = (promise as any)._webviewMessageId as string;
@@ -102,7 +106,7 @@ const _callApi = <K extends OperationNames>(
const handleCancel = async <K extends OperationNames>( const handleCancel = async <K extends OperationNames>(
ops_key: string, ops_key: string,
orig_task: Promise<OperationResponse<K>>, orig_task: Promise<BackendReturnType<K>>,
) => { ) => {
console.log("Canceling operation: ", ops_key); console.log("Canceling operation: ", ops_key);
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
@@ -122,7 +126,7 @@ const handleCancel = async <K extends OperationNames>(
}); });
const resp = await promise; const resp = await promise;
if (resp.status === "error") { if (resp.body.status === "error") {
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
@@ -186,14 +190,14 @@ export const callApi = <K extends OperationNames>(
console.log("Not printing toast because operation was cancelled"); console.log("Not printing toast because operation was cancelled");
} }
const result = response; const body = response.body;
if (result.status === "error" && !cancelled) { if (body.status === "error" && !cancelled) {
toast.remove(toastId); toast.remove(toastId);
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
t={t} t={t}
message={"Error: " + result.errors[0].message} message={"Error: " + body.errors[0].message}
/> />
), ),
{ {
@@ -203,7 +207,7 @@ export const callApi = <K extends OperationNames>(
} else { } else {
toast.remove(toastId); toast.remove(toastId);
} }
return result; return body;
}); });
return { promise: new_promise, op_key: op_key }; return { promise: new_promise, op_key: op_key };

View File

@@ -207,7 +207,11 @@ export function RemoteForm(props: RemoteFormProps) {
flake: props.machine.flake, flake: props.machine.flake,
field: props.field || "targetHost", field: props.field || "targetHost",
}, },
{logging: { group: { name: props.machine.name, flake: props.machine.flake } },}, {
logging: {
group: { name: props.machine.name, flake: props.machine.flake },
},
},
).promise; ).promise;
if (result.status === "error") if (result.status === "error")

View File

@@ -53,7 +53,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
field: "targetHost", field: "targetHost",
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, {logging: { group: { name, flake: { identifier: active_clan } } }} },
{ logging: { group: { name, flake: { identifier: active_clan } } } },
).promise; ).promise;
if (target_host.status == "error") { if (target_host.status == "error") {
@@ -106,13 +107,17 @@ export const MachineListItem = (props: MachineListItemProps) => {
} }
setUpdating(true); setUpdating(true);
const target_host = await callApi("get_host", { const target_host = await callApi(
"get_host",
{
field: "targetHost", field: "targetHost",
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, { },
{
logging: { group: { name, flake: { identifier: active_clan } } }, logging: { group: { name, flake: { identifier: active_clan } } },
}).promise; },
).promise;
if (target_host.status == "error") { if (target_host.status == "error") {
console.error("No target host found for the machine"); console.error("No target host found for the machine");
@@ -129,11 +134,15 @@ export const MachineListItem = (props: MachineListItemProps) => {
return; return;
} }
const build_host = await callApi("get_host", { const build_host = await callApi(
"get_host",
{
field: "buildHost", field: "buildHost",
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; },
{ logging: { group: { name, flake: { identifier: active_clan } } } },
).promise;
if (build_host.status == "error") { if (build_host.status == "error") {
console.error("No target host found for the machine"); console.error("No target host found for the machine");
@@ -145,7 +154,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
return; return;
} }
await callApi("deploy_machine", { await callApi(
"deploy_machine",
{
machine: { machine: {
name: name, name: name,
flake: { flake: {
@@ -154,7 +165,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
}, },
target_host: target_host.data!.data, target_host: target_host.data!.data,
build_host: build_host.data?.data || null, build_host: build_host.data?.data || null,
}, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; },
{ logging: { group: { name, flake: { identifier: active_clan } } } },
).promise;
setUpdating(false); setUpdating(false);
}; };

View File

@@ -55,7 +55,7 @@ export function MachineForm(props: MachineFormProps) {
...values.machine, ...values.machine,
tags: Array.from(values.machine.tags || detailed.machine.tags || []), tags: Array.from(values.machine.tags || detailed.machine.tags || []),
}, },
} ).promise; }).promise;
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: [ queryKey: [
@@ -77,10 +77,18 @@ export function MachineForm(props: MachineFormProps) {
if (!machine_name || !base_dir) { if (!machine_name || !base_dir) {
return []; return [];
} }
const result = await callApi("get_generators_closure", { const result = await callApi(
"get_generators_closure",
{
base_dir: base_dir, base_dir: base_dir,
machine_name: machine_name, machine_name: machine_name,
}, {logging: {group: { name: machine_name, flake: {identifier: base_dir} }}}).promise; },
{
logging: {
group: { name: machine_name, flake: { identifier: base_dir } },
},
},
).promise;
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },
@@ -112,13 +120,18 @@ export function MachineForm(props: MachineFormProps) {
return; return;
} }
const target = await callApi("get_host", { const target = await callApi(
"get_host",
{
field: "targetHost", field: "targetHost",
name: machine, name: machine,
flake: { flake: {
identifier: curr_uri, identifier: curr_uri,
}, },
}, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }} },
{
logging: { group: { name: machine, flake: { identifier: curr_uri } } },
},
).promise; ).promise;
if (target.status === "error") { if (target.status === "error") {
@@ -133,7 +146,9 @@ export function MachineForm(props: MachineFormProps) {
const target_host = target.data.data; const target_host = target.data.data;
setIsUpdating(true); setIsUpdating(true);
const r = await callApi("deploy_machine", { const r = await callApi(
"deploy_machine",
{
machine: { machine: {
name: machine, name: machine,
flake: { flake: {
@@ -144,7 +159,11 @@ export function MachineForm(props: MachineFormProps) {
...target_host, ...target_host,
}, },
build_host: null, build_host: null,
}, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }}).promise.finally(() => { },
{
logging: { group: { name: machine, flake: { identifier: curr_uri } } },
},
).promise.finally(() => {
setIsUpdating(false); setIsUpdating(false);
}); });
}; };

View File

@@ -149,11 +149,19 @@ export const VarsStep = (props: VarsStepProps) => {
const generatorsQuery = createQuery(() => ({ const generatorsQuery = createQuery(() => ({
queryKey: [props.dir, props.machine_id, "generators", props.fullClosure], queryKey: [props.dir, props.machine_id, "generators", props.fullClosure],
queryFn: async () => { queryFn: async () => {
const result = await callApi("get_generators_closure", { const result = await callApi(
"get_generators_closure",
{
base_dir: props.dir, base_dir: props.dir,
machine_name: props.machine_id, machine_name: props.machine_id,
full_closure: props.fullClosure, full_closure: props.fullClosure,
}, {logging: {group: { name: props.machine_id, flake: {identifier: props.dir} }}}).promise; },
{
logging: {
group: { name: props.machine_id, flake: { identifier: props.dir } },
},
},
).promise;
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },

View File

@@ -27,9 +27,9 @@ function isMachine(obj: unknown): obj is Machine {
return ( return (
!!obj && !!obj &&
typeof obj === "object" && typeof obj === "object" &&
typeof (obj as Machine).name === "string" && typeof (obj as any).name === "string" &&
typeof (obj as Machine).flake === "object" && typeof (obj as any).flake === "object" &&
typeof (obj as Machine).flake.identifier === "string" typeof (obj as any).flake.identifier === "string"
); );
} }
@@ -46,9 +46,8 @@ interface BackendOpts {
} }
interface BackendReturnType<K extends OperationNames> { interface BackendReturnType<K extends OperationNames> {
result: OperationResponse<K>; body: OperationResponse<K>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any header: Record<string, any>;
metadata: Record<string, any>;
} }
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
@@ -62,7 +61,7 @@ const _callApi = <K extends OperationNames>(
// return a rejected promise // return a rejected promise
return { return {
promise: Promise.resolve({ promise: Promise.resolve({
result: { body: {
status: "error", status: "error",
errors: [ errors: [
{ {
@@ -72,18 +71,18 @@ const _callApi = <K extends OperationNames>(
], ],
op_key: "noop", op_key: "noop",
}, },
metadata: {}, header: {},
}), }),
op_key: "noop", op_key: "noop",
}; };
} }
let metadata: BackendOpts | undefined = undefined; let header: BackendOpts = {};
if (backendOpts != undefined) { if (backendOpts != undefined) {
metadata = { ...backendOpts }; header = { ...backendOpts };
const group = backendOpts?.logging?.group; let group = backendOpts?.logging?.group;
if (group != undefined && isMachine(group)) { if (group != undefined && isMachine(group)) {
metadata = { header = {
logging: { group: group.flake.identifier + "#" + group.name }, logging: { group: group.flake.identifier + "#" + group.name },
}; };
} }
@@ -94,10 +93,10 @@ const _callApi = <K extends OperationNames>(
OperationNames, OperationNames,
( (
args: OperationArgs<OperationNames>, args: OperationArgs<OperationNames>,
metadata?: BackendOpts, metadata: BackendOpts,
) => Promise<BackendReturnType<OperationNames>> ) => Promise<BackendReturnType<OperationNames>>
> >
)[method](args, metadata) as Promise<BackendReturnType<K>>; )[method](args, header) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string; const op_key = (promise as any)._webviewMessageId as string;
@@ -127,7 +126,7 @@ const handleCancel = async <K extends OperationNames>(
}); });
const resp = await promise; const resp = await promise;
if (resp.result.status === "error") { if (resp.body.status === "error") {
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
@@ -151,7 +150,7 @@ export const callApi = <K extends OperationNames>(
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: BackendOpts,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args); console.log("Calling API", method, args, backendOpts);
const { promise, op_key } = _callApi(method, args, backendOpts); const { promise, op_key } = _callApi(method, args, backendOpts);
promise.catch((error) => { promise.catch((error) => {
@@ -191,14 +190,14 @@ export const callApi = <K extends OperationNames>(
console.log("Not printing toast because operation was cancelled"); console.log("Not printing toast because operation was cancelled");
} }
const result = response.result; const body = response.body;
if (result.status === "error" && !cancelled) { if (body.status === "error" && !cancelled) {
toast.remove(toastId); toast.remove(toastId);
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
t={t} t={t}
message={"Error: " + result.errors[0].message} message={"Error: " + body.errors[0].message}
/> />
), ),
{ {
@@ -208,7 +207,7 @@ export const callApi = <K extends OperationNames>(
} else { } else {
toast.remove(toastId); toast.remove(toastId);
} }
return result; return body;
}); });
return { promise: new_promise, op_key: op_key }; return { promise: new_promise, op_key: op_key };

View File

@@ -310,10 +310,13 @@ class LogManager:
base_dir: Path base_dir: Path
def create_log_file( def create_log_file(
self, func: Callable, op_key: str, group: str = "default" self, func: Callable, op_key: str, group: str | None = None
) -> LogFile: ) -> LogFile:
now_utc = datetime.datetime.now(tz=datetime.UTC) now_utc = datetime.datetime.now(tz=datetime.UTC)
if group is None:
group = "default"
log_file = LogFile( log_file = LogFile(
op_key=op_key, op_key=op_key,
date_day=now_utc.strftime("%Y-%m-%d"), date_day=now_utc.strftime("%Y-%m-%d"),