Skip to content

Commit

Permalink
feat: worker log and revision
Browse files Browse the repository at this point in the history
  • Loading branch information
Keyrxng committed Nov 5, 2024
1 parent fe99961 commit f9f9fa7
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 32 deletions.
5 changes: 4 additions & 1 deletion src/sdk/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,16 @@ export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TSu
env = process.env as TEnv;
}



const context: Context<TConfig, TEnv, TSupportedEvents> = {
eventName: inputs.eventName as TSupportedEvents,
payload: JSON.parse(inputs.eventPayload),
octokit: new customOctokit({ auth: inputs.authToken }),
config: config,
env: env,
logger: new Logs(pluginOptions.logLevel),
pluginDeploymentDetails: getGithubWorkflowRunUrl()
};

try {
Expand Down Expand Up @@ -120,7 +123,7 @@ async function postErrorComment(context: Context, error: LogReturn) {
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issue_number: context.payload.issue.number,
body: `${error.logMessage.diff}\n<!--\n${getGithubWorkflowRunUrl()}\n${sanitizeMetadata(error.metadata)}\n-->`,
body: `${error.logMessage.diff}\n<!--\n${context.pluginDeploymentDetails}\n${sanitizeMetadata(error.metadata)}\n-->`,
});
} else {
context.logger.info("Cannot post error comment because issue is not found in the payload");
Expand Down
130 changes: 112 additions & 18 deletions src/sdk/comment.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,84 @@
import { Context } from "./context";
import { LogReturn } from "@ubiquity-os/ubiquity-os-logger";
import { LogReturn, Metadata } from "@ubiquity-os/ubiquity-os-logger";
import { sanitizeMetadata } from "./util";
import { CloudflareEnvBindings } from "./server";

const HEADER_NAME = "Ubiquity";

/**
* Posts a comment on a GitHub issue if the issue exists in the context payload, embedding structured metadata to it.
*/
export async function postComment(context: Context, message: LogReturn) {
if ("issue" in context.payload && context.payload.repository?.owner?.login) {
const metadata = createStructuredMetadata(message.metadata?.name, message);
await context.octokit.rest.issues.createComment({
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issue_number: context.payload.issue.number,
body: [message.logMessage.diff, metadata].join("\n"),
});
export async function postComment(context: Context, message: LogReturn | Error | null, honoEnv: CloudflareEnvBindings) {
if (!message) {
return;
}

let issueNumber

if ("issue" in context.payload) {
issueNumber = context.payload.issue.number;
} else if ("pull_request" in context.payload) {
issueNumber = context.payload.pull_request.number;
} else if ("discussion" in context.payload) {
issueNumber = context.payload.discussion.number;
} else {
context.logger.info("Cannot post comment because issue is not found in the payload");
return;
}

await context.octokit.rest.issues.createComment({
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issue_number: issueNumber,
body: await createStructuredMetadataErrorComment(message, context, honoEnv),
});
}

function createStructuredMetadata(className: string | undefined, logReturn: LogReturn) {
const logMessage = logReturn.logMessage;
const metadata = logReturn.metadata;
async function createStructuredMetadataErrorComment(message: LogReturn | Error, context: Context, honoEnv: CloudflareEnvBindings) {
let metadata: Metadata = {};
let logMessage, logTier, callingFnName, headerName;

if (message instanceof Error) {
metadata = {
message: message.message,
name: message.name,
stack: message.stack,
};

callingFnName = message.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1];

} else if (message instanceof LogReturn && message.metadata) {
logMessage = message.logMessage;
logTier = message.logMessage.level; // LogLevel
metadata = message.metadata;

if (metadata.stack || metadata.error) {
metadata.stack = metadata.stack || metadata.error?.stack;
metadata.caller = metadata.caller || metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1];
}

callingFnName = metadata.caller;
} else {
metadata = { ...message };
}

if ("organization" in context.payload) {
headerName = context.payload.organization?.login;
} else if ("repository" in context.payload) {
headerName = context.payload.repository?.owner?.login;
} else if ("installation" in context.payload && "account" in context.payload.installation!) {

Check failure on line 69 in src/sdk/comment.ts

View workflow job for this annotation

GitHub Actions / Check for formatting issues

Forbidden non-null assertion
// could use their ID here instead as ID is in all installation payloads
headerName = context.payload.installation?.account?.name;
} else {
headerName = context.payload.sender?.login || HEADER_NAME;
}

const workerDetails = await getWorkerDeploymentHash(context, honoEnv);
const workerLogUrl = await getWorkerErrorLogUrl(context, honoEnv);
metadata.worker = { ...workerDetails, logUrl: workerLogUrl };

const jsonPretty = sanitizeMetadata(metadata);
const stack = logReturn.metadata?.stack;
const stackLine = (Array.isArray(stack) ? stack.join("\n") : stack)?.split("\n")[2] ?? "";
const caller = stackLine.match(/at (\S+)/)?.[1] ?? "";
const ubiquityMetadataHeader = `<!-- ${HEADER_NAME} - ${className} - ${caller} - ${metadata?.revision}`;
const ubiquityMetadataHeader = `<!-- UbiquityOS - ${headerName} - ${logTier} - ${context.pluginDeploymentDetails} - ${callingFnName} - ${workerDetails?.versionId.split("-")[0]}`;

let metadataSerialized: string;
const metadataSerializedVisible = ["```json", jsonPretty, "```"].join("\n");
Expand All @@ -43,6 +92,51 @@ function createStructuredMetadata(className: string | undefined, logReturn: LogR
metadataSerialized = metadataSerializedHidden;
}

if (message instanceof Error) {
return [context.logger.error(message.message).logMessage.diff, `\n${metadataSerialized}\n`].join("\n");
}

// Add carriage returns to avoid any formatting issue
return `\n${metadataSerialized}\n`;
return [logMessage?.diff, `\n${metadataSerialized}\n`].join("\n");
}

/**
* These vars will be injected into the worker environment via
* `worker-deploy` action. These are not defined in plugin env schema.
*/
async function getWorkerDeploymentHash(context: Context, honoEnv: CloudflareEnvBindings) {
const accountId = honoEnv.CLOUDFLARE_ACCOUNT_ID;
let scriptName = context.pluginDeploymentDetails;

if (scriptName === "localhost") {
return { versionId: "local", message: "Local development environment" };
}

const scriptUrl = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/deployments`;
const response = await fetch(scriptUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${honoEnv.CLOUDFLARE_API_TOKEN}`,
},
});

try {
const data = await response.json() as { result: { deployments: { annotations: { "workers/message": string }, id: string, versions: { version_id: string }[] }[] } };
const deployment = data.result.deployments[0];
const versionId = deployment.versions[0].version_id;
const message = deployment.annotations["workers/message"];
return { versionId, message };
} catch (error) {
context.logger.error(`Error fetching worker deployment hash: ${String(error)}`);
}
}

async function getWorkerErrorLogUrl(context: Context, honoEnv: CloudflareEnvBindings) {
const accountId = honoEnv.CLOUDFLARE_ACCOUNT_ID;
const workerName = context.pluginDeploymentDetails;
const toTime = Date.now() + 60000;
const fromTime = Date.now() - 60000;
const timeParam = encodeURIComponent(`{"type":"absolute","to":${toTime},"from":${fromTime}}`);
return `https://dash.cloudflare.com/${accountId}/workers/services/view/${workerName}/production/observability/logs?granularity=0&time=${timeParam}`;
}
1 change: 1 addition & 0 deletions src/sdk/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export interface Context<TConfig = unknown, TEnv = unknown, TSupportedEvents ext
config: TConfig;
env: TEnv;
logger: Logs;
pluginDeploymentDetails: string;
}
33 changes: 20 additions & 13 deletions src/sdk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { TAnySchema } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { LOG_LEVEL, LogLevel, LogReturn, Logs } from "@ubiquity-os/ubiquity-os-logger";
import { Hono } from "hono";
import { Env, Hono } from "hono";

Check failure on line 5 in src/sdk/server.ts

View workflow job for this annotation

GitHub Actions / Check for formatting issues

'Env' is defined but never used. Allowed unused vars must match /^_/u
import { HTTPException } from "hono/http-exception";
import { Manifest } from "../types/manifest";
import { KERNEL_PUBLIC_KEY } from "./constants";
Expand Down Expand Up @@ -31,6 +31,11 @@ const inputSchema = T.Object({
signature: T.String(),
});

export type CloudflareEnvBindings = {
CLOUDFLARE_ACCOUNT_ID: string,
CLOUDFLARE_API_TOKEN: string
}

export function createPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName>(
handler: (context: Context<TConfig, TEnv, TSupportedEvents>) => Promise<Record<string, unknown> | undefined>,
manifest: Manifest,
Expand All @@ -44,7 +49,8 @@ export function createPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents
envSchema: options?.envSchema,
};

const app = new Hono();

const app = new Hono<{ Bindings: CloudflareEnvBindings }>();

app.get("/manifest.json", (ctx) => {
return ctx.json(manifest);
Expand Down Expand Up @@ -92,32 +98,33 @@ export function createPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents
env = ctx.env as TEnv;
}

const workerUrl = new URL(inputs.ref).origin;
let workerName;

if (workerUrl.includes("localhost")) {
workerName = "localhost";
} else {
workerName = `${workerUrl.split("//")[1].split(".")[0]}`;
}

const context: Context<TConfig, TEnv, TSupportedEvents> = {
eventName: inputs.eventName as TSupportedEvents,
payload: inputs.eventPayload,
octokit: new customOctokit({ auth: inputs.authToken }),
config: config,
env: env,
logger: new Logs(pluginOptions.logLevel),
pluginDeploymentDetails: `${workerName}`,
};

try {
const result = await handler(context);
return ctx.json({ stateId: inputs.stateId, output: result });
} catch (error) {
console.error(error);

let loggerError: LogReturn | null;
if (error instanceof Error) {
loggerError = context.logger.error(`Error: ${error}`, { error: error });
} else if (error instanceof LogReturn) {
loggerError = error;
} else {
loggerError = context.logger.error(`Error: ${error}`);
}

const loggerError = error as LogReturn | Error | null;
if (pluginOptions.postCommentOnError && loggerError) {
await postComment(context, loggerError);
await postComment(context, loggerError, honoEnvironment);
}

throw new HTTPException(500, { message: "Unexpected error" });
Expand Down

0 comments on commit f9f9fa7

Please sign in to comment.