Skip to content

Commit f2dbe70

Browse files
authored
feat: logs streaming (#10)
1 parent 93781d1 commit f2dbe70

6 files changed

Lines changed: 147 additions & 22 deletions

File tree

auth.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { Spinner } from "@std/cli/unstable-spinner";
1313
import { error } from "./util.ts";
1414
import token_storage from "./token_storage.ts";
15+
import { EventSourcePolyfill } from "event-source-polyfill";
1516

1617
export function createTrpcClient(deployUrl: string) {
1718
const storedAuth = token_storage.get();
@@ -48,6 +49,18 @@ export function createTrpcClient(deployUrl: string) {
4849
}),
4950
true: httpSubscriptionLink({
5051
url: deployUrl + "/api",
52+
EventSource: EventSourcePolyfill,
53+
eventSourceOptions: () => {
54+
if (storedAuth) {
55+
return {
56+
headers: {
57+
cookie: `token=${storedAuth}; deno_auth_ghid=force`,
58+
},
59+
};
60+
} else {
61+
return {};
62+
}
63+
},
5164
transformer,
5265
}),
5366
}),

deno.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
"@std/tar": "jsr:@std/tar@^0.1.6",
1616
"@trpc/client": "npm:@trpc/client@^11.0.2",
1717
"@trpc/server": "npm:@trpc/server@^11.0.2",
18+
"event-source-polyfill": "npm:event-source-polyfill@^1.0.31",
1819
"jsonc-parser": "npm:jsonc-parser@^3.3.1",
1920
"open": "npm:open@^10.1.0",
2021
"superjson": "npm:superjson@^2.2.2",
21-
"@deno/framework-detect": "jsr:@deno/framework-detect@^0"
22+
"@deno/framework-detect": "jsr:@deno/framework-detect@^0",
23+
"temporal-polyfill": "npm:temporal-polyfill@^0.3.0"
2224
}
2325
}

deno.lock

Lines changed: 34 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Command } from "@cliffy/command";
22
import { publish } from "./publish.ts";
3-
import { red } from "@std/fmt/colors";
3+
import { red, yellow } from "@std/fmt/colors";
44
import { create } from "./create.ts";
5-
import { withApp } from "./util.ts";
5+
import { error, renderTemporalTimestamp, withApp } from "./util.ts";
66
import { setupAws, setupGcp } from "./setup-cloud.ts";
77
import { getAppFromConfig, readConfig, writeConfig } from "./config.ts";
88
import {
@@ -12,6 +12,7 @@ import {
1212
envUpdateContextsCommand,
1313
envUpdateValueCommand,
1414
} from "./env.ts";
15+
import { createTrpcClient } from "./auth.ts";
1516

1617
const createCommand = new Command<{ endpoint: string }>()
1718
.description("Create a new application")
@@ -97,6 +98,75 @@ const envCommand = new Command<{ endpoint: string }>()
9798
.command("update-contexts", envUpdateContextsCommand)
9899
.command("delete", envDeleteCommand);
99100

101+
const logsCommand = new Command<{ endpoint: string }>()
102+
.option("--org <name:string>", "The name of the organization")
103+
.option("--app <name:string>", "The name of the application")
104+
.option("--start <date:string>", "The starting timestamp of the logs")
105+
.option("--end <date:string>", "The ending timestamp of the logs", {
106+
depends: ["start"],
107+
})
108+
.action(async (options, rootPath = Deno.cwd()) => {
109+
const configContent = await readConfig(rootPath);
110+
let { org, app } = getAppFromConfig(configContent);
111+
org ??= options.org;
112+
app ??= options.app;
113+
const gottenApp = await withApp(options.endpoint, false, org, app);
114+
115+
interface LogEntry {
116+
Timestamp: string;
117+
TraceId: string;
118+
SpanId: string;
119+
SeverityText: string;
120+
SeverityNumber: number;
121+
Body: string;
122+
ScopeName: string;
123+
ScopeVersion: string;
124+
LogAttributes: Record<string, string>;
125+
Revision: string;
126+
}
127+
128+
const trpcClient = createTrpcClient(options.endpoint);
129+
130+
const sub = (trpcClient.apps as any).logs.subscribe({
131+
org: gottenApp.org,
132+
app: gottenApp.app,
133+
start: (options.start ? new Date(options.start) : new Date())
134+
.toISOString(),
135+
end: options.end ? new Date(options.end).toISOString() : undefined,
136+
filter: {},
137+
}, {
138+
onData: (data: "streaming" | null | LogEntry[]) => {
139+
if (data === "streaming") {
140+
console.log("Streaming logs...");
141+
} else if (Array.isArray(data)) {
142+
for (const log of data) {
143+
let text = `[${renderTemporalTimestamp(log.Timestamp)}${
144+
log.TraceId ? ` (${log.TraceId})` : ""
145+
}] ${log.Body}`;
146+
if (text.endsWith("\n")) {
147+
text = text.slice(0, -1);
148+
}
149+
150+
if (log.SeverityNumber >= 17) {
151+
console.log(red(text));
152+
} else if (log.SeverityNumber >= 13) {
153+
console.log(yellow(text));
154+
} else {
155+
console.log(text);
156+
}
157+
}
158+
}
159+
},
160+
onError: (err: unknown) => {
161+
sub.unsubscribe();
162+
error(Deno.inspect(err));
163+
},
164+
onStopped: () => {
165+
sub.unsubscribe();
166+
},
167+
});
168+
});
169+
100170
await new Command()
101171
.name("deno deploy")
102172
.description(`Interact with Deno Deploy
@@ -149,6 +219,7 @@ deploy your local directory to the specified application.`)
149219
)
150220
.command("create", createCommand)
151221
.command("env", envCommand)
222+
.command("logs", logsCommand)
152223
.command("setup-aws", setupAWSCommand)
153224
.command("setup-gcp", setupGCPCommand)
154225
.command("tunnel-login", tunnelLoginCommand)

token_storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export default {
66
Deno[Deno.internal].core.ops.op_deploy_token_set(token);
77
},
88
remove() {
9-
Deno[Deno.internal].core.ops.op_deploy_token_remove();
9+
Deno[Deno.internal].core.ops.op_deploy_token_delete();
1010
},
1111
};

util.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { red } from "@std/fmt/colors";
22
import { promptSelect } from "@std/cli/unstable-prompt-select";
3+
import { Temporal } from "temporal-polyfill";
34

45
import { createTrpcClient, getAuth } from "./auth.ts";
56
import token_storage from "./token_storage.ts";
@@ -101,3 +102,25 @@ export async function withApp(
101102
app: app as string | null,
102103
};
103104
}
105+
106+
export function renderTemporalTimestamp(timestamp: string, hideDate = false) {
107+
function pad(n: number, width: number): string {
108+
return n.toString().padStart(width, "0");
109+
}
110+
111+
const date = Temporal
112+
.Instant
113+
.from(timestamp)
114+
.toZonedDateTimeISO("UTC");
115+
const months = pad(date.month, 2);
116+
const days = pad(date.day, 2);
117+
const hours = pad(date.hour, 2);
118+
const minutes = pad(date.minute, 2);
119+
const seconds = pad(date.second, 2);
120+
const ms = (date.millisecond / 1000).toFixed(2).substring(2);
121+
122+
const time = `${hours}:${minutes}:${seconds}.${ms}`;
123+
if (hideDate) return time;
124+
125+
return `${date.year}-${months}-${days} ${time}`;
126+
}

0 commit comments

Comments
 (0)