Skip to content

Commit 129de4f

Browse files
committed
WIP on tracing to google cloud
Fix project ID Replace tracing with OpenTelemetry Try linking graph compute parent span Refactor routing to normal async/await
1 parent ea9bfff commit 129de4f

File tree

8 files changed

+144
-89
lines changed

8 files changed

+144
-89
lines changed

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM denoland/deno:alpine-1.30.1
1+
FROM denoland/deno:alpine-1.31.1
22
RUN apk add --no-cache graphviz
33
ADD fonts/ /usr/share/fonts/truetype/
44

@@ -7,4 +7,4 @@ ADD deps.ts .
77
RUN deno check deps.ts
88
ADD . .
99
RUN deno check server.ts
10-
ENTRYPOINT ["deno","run","--allow-env","--allow-net","--allow-run=deno,dot","--allow-read=.","server.ts"]
10+
ENTRYPOINT ["deno","run","--allow-sys=hostname","--allow-env","--allow-net","--allow-run=deno,dot","--allow-read=.","server.ts"]

deps.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ export type {
1818
ModuleGraphJson,
1919
ModuleJson,
2020
} from "https://deno.land/x/[email protected]/lib/types.d.ts";
21+
22+
export { trace, context, type Context } from "npm:@opentelemetry/api";

feat/dependencies-of/api.ts

+67-59
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import {
44
readableStreamFromIterable,
55
SubProcess,
66
type SubprocessErrorData,
7+
trace,
8+
context,
9+
Context,
710
} from "../../deps.ts";
811

912
import { templateHtml, makeErrorResponse, HtmlHeaders } from '../../lib/request-handling.ts';
1013
import { findModuleSlug, resolveModuleUrl } from "../../lib/resolve.ts";
1114
import { computeGraph, renderGraph } from "./compute.ts";
1215

13-
export async function *handleRequest(req: Request, modSlug: string, args: URLSearchParams) {
16+
const tracer = trace.getTracer('dependencies-of-api');
17+
18+
export async function handleRequest(req: Request, modSlug: string, args: URLSearchParams) {
1419
if (modSlug == '') {
1520
const url = args.get('url');
1621
if (!url) return;
@@ -26,7 +31,7 @@ export async function *handleRequest(req: Request, modSlug: string, args: URLSea
2631

2732
const slug = await findModuleSlug(url);
2833
const location = slug + (args.toString() ? `?${args}` : '');
29-
yield new Response(`302: ${location}`, {
34+
return new Response(`302: ${location}`, {
3035
status: 302,
3136
headers: { location },
3237
});
@@ -37,17 +42,13 @@ export async function *handleRequest(req: Request, modSlug: string, args: URLSea
3742

3843
switch (args.get('format')) {
3944
case 'json':
40-
yield serveBufferedOutput(req, computeGraph(modUrl, args), 'application/json');
41-
return;
45+
return await serveBufferedOutput(req, computeGraph(modUrl, args), 'application/json');
4246
case 'dot':
43-
yield serveBufferedOutput(req, computeGraph(modUrl, args), 'text/plain; charset=utf-8');
44-
return;
47+
return await serveBufferedOutput(req, computeGraph(modUrl, args), 'text/plain; charset=utf-8');
4548
case 'svg':
46-
yield serveStreamingOutput(req, renderGraph(modUrl, ["-Tsvg"], args), 'image/svg+xml');
47-
return;
49+
return await serveStreamingOutput(req, renderGraph(modUrl, ["-Tsvg"], args), 'image/svg+xml');
4850
case null:
49-
yield serveHtmlGraphPage(req, modUrl, modSlug, args);
50-
return;
51+
return await serveHtmlGraphPage(req, modUrl, modSlug, args);
5152
}
5253
}
5354

@@ -75,56 +76,40 @@ async function serveStreamingOutput(req: Request, computation: Promise<SubProces
7576

7677
const hideLoadMsg = `<style type="text/css">#graph-waiting { display: none; }</style>`;
7778

78-
async function serveHtmlGraphPage(req: Request, modUrl: string, modSlug: string, args: URLSearchParams) {
79-
args.set('font', 'Archivo Narrow');
80-
81-
// Render the basic page first, so we can error more cleanly if that fails
82-
let pageHtml = '';
79+
async function renderModuleToHtml(modUrl: string, args: URLSearchParams) {
8380
try {
84-
pageHtml = await templateHtml('feat/dependencies-of/public.html', {
85-
module_slug: entities.encode(modSlug),
86-
module_url: entities.encode(modUrl),
87-
export_prefix: entities.encode(`${req.url}${req.url.includes('?') ? '&' : '?'}format=`),
88-
});
89-
} catch (err) {
90-
return makeErrorResponse(err);
91-
}
9281

93-
const graphPromise = ((args.get('renderer') === 'interactive')
94-
95-
? computeGraph(modUrl, args, 'dot')
96-
.then(data => {
97-
return `
98-
<div id="graph"></div>
99-
<script type="text/javascript" src="https://unpkg.com/[email protected]/standalone/umd/vis-network.min.js"></script>
100-
<script type="text/javascript" src="/interactive-graph.js"></script>
101-
<template type="text/plain" id="graphviz_data">\n${data
102-
.replace(/&/g, '&amp;')
103-
.replace(/>/g, '&gt;')
104-
.replace(/</g, '&lt;')
105-
}</template>
106-
<script type="text/javascript">
107-
window.CreateModuleGraph(document.getElementById('graphviz_data').innerHTML
108-
.replace(/&gt;/g, '>')
109-
.replace(/&lt;/g, '<')
110-
.replace(/&amp;/g, '&'));
111-
</script>
112-
`.replace(/^ {10}/gm, '');
113-
})
114-
115-
: renderGraph(modUrl, ["-Tsvg"], args)
116-
.then(dotProc => dotProc.captureAllOutput())
117-
.then(raw => {
118-
const fullSvg = new TextDecoder().decode(raw);
119-
const attrs = [`id="graph"`];
120-
const svgWidth = fullSvg.match(/viewBox="(?:([0-9.-]+) ){3}/)?.[1];
121-
if (svgWidth) attrs.push(`style="max-width: ${parseInt(svgWidth)*2}px;"`);
122-
return fullSvg
123-
.slice(fullSvg.indexOf('<!--'))
124-
.replace(/<svg width="[^"]+" height="[^"]+"/, '<svg '+attrs.join(' '));
125-
})
126-
127-
).catch(err => {
82+
if (args.get('renderer') === 'interactive') {
83+
const data = await computeGraph(modUrl, args, 'dot');
84+
return `
85+
<div id="graph"></div>
86+
<script type="text/javascript" src="https://unpkg.com/[email protected]/standalone/umd/vis-network.min.js"></script>
87+
<script type="text/javascript" src="/interactive-graph.js"></script>
88+
<template type="text/plain" id="graphviz_data">\n${data
89+
.replace(/&/g, '&amp;')
90+
.replace(/>/g, '&gt;')
91+
.replace(/</g, '&lt;')
92+
}</template>
93+
<script type="text/javascript">
94+
window.CreateModuleGraph(document.getElementById('graphviz_data').innerHTML
95+
.replace(/&gt;/g, '>')
96+
.replace(/&lt;/g, '<')
97+
.replace(/&amp;/g, '&'));
98+
</script>
99+
`.replace(/^ {8}/gm, '');
100+
}
101+
102+
const dotProc = await renderGraph(modUrl, ["-Tsvg"], args);
103+
const raw = await dotProc.captureAllOutput();
104+
const fullSvg = new TextDecoder().decode(raw);
105+
const attrs = [`id="graph"`];
106+
const svgWidth = fullSvg.match(/viewBox="(?:([0-9.-]+) ){3}/)?.[1];
107+
if (svgWidth) attrs.push(`style="max-width: ${parseInt(svgWidth)*2}px;"`);
108+
return fullSvg
109+
.slice(fullSvg.indexOf('<!--'))
110+
.replace(/<svg width="[^"]+" height="[^"]+"/, '<svg '+attrs.join(' '));
111+
112+
} catch (err) {
128113
if (err.subproc) {
129114
const info = err.subproc as SubprocessErrorData;
130115
return `
@@ -140,7 +125,30 @@ async function serveHtmlGraphPage(req: Request, modUrl: string, modSlug: string,
140125
}
141126
console.error('Graph computation error:', err.stack);
142127
return `<div id="graph-error">${entities.encode(err.stack)}</div>`;
143-
});
128+
}
129+
}
130+
131+
async function serveHtmlGraphPage(req: Request, modUrl: string, modSlug: string, args: URLSearchParams) {
132+
args.set('font', 'Archivo Narrow');
133+
134+
// Render the basic page first, so we can error more cleanly if that fails
135+
let pageHtml = '';
136+
try {
137+
pageHtml = await templateHtml('feat/dependencies-of/public.html', {
138+
module_slug: entities.encode(modSlug),
139+
module_url: entities.encode(modUrl),
140+
export_prefix: entities.encode(`${req.url}${req.url.includes('?') ? '&' : '?'}format=`),
141+
});
142+
} catch (err) {
143+
return makeErrorResponse(err);
144+
}
145+
146+
const graphPromise = tracer.startActiveSpan('Compute + Render Graph', {
147+
attributes: {
148+
'render.mod_url': modUrl,
149+
'render.params': args.toString(),
150+
},
151+
}, context.active(), span => renderModuleToHtml(modUrl, args).finally(() => span.end()));
144152

145153
// Return the body in two parts, with a comment in between
146154
return new Response(readableStreamFromIterable((async function*() {

feat/shields/api.ts

+6-10
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,39 @@ import { resolveModuleUrl } from "../../lib/resolve.ts";
66
import { processDenoInfo, ModuleMap } from "../../lib/module-map.ts";
77
import { determineModuleAttrs } from "../../lib/module-registries.ts";
88

9-
export async function *handleRequest(req: Request, shieldId: string, modSlug: string) {
9+
export async function handleRequest(req: Request, shieldId: string, modSlug: string) {
1010
const modUrl = await resolveModuleUrl(modSlug);
1111
if (!modUrl) return;
1212
switch (shieldId) {
1313

1414
case 'dep-count':
15-
yield computeGraph(modUrl)
15+
return await computeGraph(modUrl)
1616
.then(makeDepCountShield)
1717
.catch(makeErrorShield);
18-
return;
1918

2019
case 'updates':
21-
yield computeGraph(modUrl)
20+
return await computeGraph(modUrl)
2221
.then(makeUpdatesShield)
2322
.catch(makeErrorShield);
24-
return;
2523

2624
case 'cache-size':
27-
yield computeGraph(modUrl)
25+
return await computeGraph(modUrl)
2826
.then(makeCacheSizeShield)
2927
.catch(makeErrorShield);
30-
return;
3128

3229
case 'latest-version':
3330
if (modSlug.startsWith('x/')) {
34-
yield makeXLatestVersionShield(modSlug.split('/')[1].split('@')[0])
31+
return await makeXLatestVersionShield(modSlug.split('/')[1].split('@')[0])
3532
.catch(makeErrorShield);
3633
}
3734
return;
3835

3936
case 'setup':
40-
yield serveTemplatedHtml(req, 'feat/shields/public.html', {
37+
return await serveTemplatedHtml(req, 'feat/shields/public.html', {
4138
module_slug: entities.encode(modSlug),
4239
module_url: entities.encode(modUrl),
4340
module_slug_component: encodeURIComponent(modSlug),
4441
});
45-
return;
4642
}
4743
}
4844

lib/module-map.ts

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { filesize, ModuleGraphJson, ModuleJson } from "../deps.ts";
22
import { CodeModule } from "./types.ts";
33
import * as registries from "./module-registries.ts";
44

5+
// TODO: enable multiple ModuleMap modes
6+
// 1. 'per-file' graph, direct from `deno info` without collapsing
7+
// 2. 'pkg-overview' graph, built from per-file graph & collapses everything into their modules
8+
// 3. 'module-focus' graph, shows all files within one module + immediate upstream/downstreams (also emits as subgraphs)
9+
// per-file graphs are used as input when constructing the other graphs
10+
511
export class ModuleMap {
612
modules = new Map<string,CodeModule>();
713
mainModule: CodeModule;

lib/request-handling.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { http, file_server } from "../deps.ts";
1+
import { http, file_server, trace, context } from "../deps.ts";
22

33
export const HtmlHeaders = new Headers({
44
'content-type': 'text/html; charset=utf-8',
@@ -7,7 +7,20 @@ export const TextHeaders = new Headers({
77
'content-type': 'text/text; charset=utf-8',
88
});
99

10-
export async function templateHtml(templatePath: string, replacements: Record<string,string> = {}) {
10+
11+
const tracer = trace.getTracer('html-templating');
12+
13+
export function templateHtml(templatePath: string, replacements: Record<string,string> = {}) {
14+
return tracer.startActiveSpan(`Render ${templatePath}`, {
15+
attributes: {
16+
'template_path': templatePath,
17+
},
18+
}, span =>
19+
templateHtmlInner(templatePath, replacements)
20+
.finally(() => span.end()));
21+
}
22+
23+
async function templateHtmlInner(templatePath: string, replacements: Record<string,string> = {}) {
1124
const [
1225
template,
1326
globals,

server.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
#!/usr/bin/env -S deno run --check=all --allow-read=. --allow-net=0.0.0.0 --allow-run=deno,dot --allow-env=PORT
1+
#!/usr/bin/env -S deno run --watch --check --allow-sys=hostname --allow-read --allow-net --allow-run=deno,dot --allow-env
22

3-
import { http } from "./deps.ts";
3+
import { Context, context, http } from "./deps.ts";
44
import { serveFont, servePublic, serveTemplatedHtml } from './lib/request-handling.ts';
5+
import { asyncGeneratorWithContext, httpTracer, provider } from "./tracer.ts";
56

67
// The different HTTP surfaces we expose
78
import * as DependenciesOf from './feat/dependencies-of/api.ts';
@@ -16,57 +17,56 @@ try {
1617
}
1718

1819
console.log('Setting up on', { port });
19-
http.serve(async request => {
20-
for await (const response of handleReq(request)) {
21-
return response;
22-
}
23-
console.log('reached 404');
24-
return new Response('404 Not Found', {
20+
http.serve(httpTracer(provider, async request => {
21+
22+
const resp = await handleReq(request);
23+
return resp ?? new Response('404 Not Found', {
2524
status: 404,
2625
});
27-
}, {
26+
27+
}), {
2828
port,
2929
});
3030

31-
async function *handleReq(req: Request) {
31+
async function handleReq(req: Request): Promise<Response | undefined> {
3232
console.log(req.method, req.url);
3333
const url = new URL(req.url, 'http://localhost');
3434
const args = new URLSearchParams(url.search);
3535

3636
{ // feature: dependencies-of
3737
const match = url.pathname.match(/^\/dependencies-of\/(.*)$/);
3838
if (match && req.method === 'GET') {
39-
yield* DependenciesOf.handleRequest(req, match[1], args);
39+
return await DependenciesOf.handleRequest(req, match[1], args);
4040
}
4141
}
4242

4343
{ // feature: shields
4444
const match = url.pathname.match(/^\/shields\/([^\/]+)\/(.+)$/);
4545
if (match && req.method === 'GET') {
46-
yield* Shields.handleRequest(req, match[1], match[2]);
46+
return await Shields.handleRequest(req, match[1], match[2]);
4747
}
4848
}
4949

5050
{ // feature: registry-key
5151
if (url.pathname === '/registry-key' && req.method === 'GET') {
52-
yield RegistryKey.handleRequest(req);
52+
return await RegistryKey.handleRequest(req);
5353
}
5454
}
5555

5656
if (url.pathname === '/') {
57-
yield serveTemplatedHtml(req, 'public/index.html');
57+
return serveTemplatedHtml(req, 'public/index.html');
5858
}
5959

6060
if ([
6161
'/global.css',
6262
'/icon-deps.png',
6363
'/interactive-graph.js',
6464
].includes(url.pathname)) {
65-
yield servePublic(req, url.pathname);
65+
return servePublic(req, url.pathname);
6666
}
6767

6868
if (url.pathname.startsWith('/fonts/') &&
6969
url.pathname.endsWith('.woff2')) {
70-
yield serveFont(req, url.pathname.slice(6));
70+
return serveFont(req, url.pathname.slice(6));
7171
}
7272
}

tracer.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
DenoTracerProvider,
3+
OTLPTraceFetchExporter,
4+
httpTracer,
5+
DenoFetchInstrumentation,
6+
SubProcessInstrumentation,
7+
Resource,
8+
asyncGeneratorWithContext,
9+
} from "https://raw.githubusercontent.com/cloudydeno/deno-observability/7a96cf859631e81df821ef8c3352b92d7f909739/tracing/mod.ts";
10+
import { GcpBatchSpanExporter } from "https://raw.githubusercontent.com/cloudydeno/deno-observability/7a96cf859631e81df821ef8c3352b92d7f909739/tracing/exporters/google-cloud.ts";
11+
import { GoogleCloudPropagator } from "https://raw.githubusercontent.com/cloudydeno/deno-observability/7a96cf859631e81df821ef8c3352b92d7f909739/tracing/propagators/google-cloud.ts";
12+
13+
export { httpTracer, asyncGeneratorWithContext };
14+
15+
export const provider = new DenoTracerProvider({
16+
resource: new Resource({
17+
'service.name': 'module-visualizer',
18+
'service.version': 'adhoc',
19+
'deployment.environment': 'local',
20+
}),
21+
propagator: new GoogleCloudPropagator(),
22+
instrumentations: [
23+
new DenoFetchInstrumentation(),
24+
new SubProcessInstrumentation(),
25+
],
26+
batchSpanProcessors: [
27+
new GcpBatchSpanExporter(),
28+
// new OTLPTraceFetchExporter(),
29+
],
30+
});

0 commit comments

Comments
 (0)