Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic instrumentation of Cloudflare bindings (AI, KV, R2, D1) #196

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f892c8d
Prototype proxying cloudflare bindings (AI and R2)
brettimus Aug 26, 2024
bf9cd6f
Update comment on measuredBinding
brettimus Aug 26, 2024
dea79fd
Fix typescript error
brettimus Aug 26, 2024
470a99a
Add goosify sample app with every cf binding imaginable
brettimus Aug 27, 2024
f7e438b
Set up local D1 with drizzle
brettimus Aug 27, 2024
cae7193
Return geese from goosify (and format code)
brettimus Aug 27, 2024
20fe194
Try implementing a d1 binding
brettimus Aug 27, 2024
510908b
Remove cl
brettimus Aug 27, 2024
3643ff7
Format
brettimus Aug 27, 2024
cde2d44
Rename some attrs
brettimus Aug 27, 2024
5cf907f
Start adding more attributes depending on the args to the binding, etc
brettimus Aug 27, 2024
3b17a75
Proxy kv
brettimus Aug 27, 2024
0f318f6
Finish D1 instrumentation
brettimus Aug 27, 2024
27fdd02
Format
brettimus Aug 27, 2024
9d2091f
Clean up cf binding code and move to patch module
brettimus Aug 28, 2024
4ab2e86
Use constants for cf binding attributes
brettimus Aug 28, 2024
35ebebe
Fix ts-expect-error
brettimus Aug 28, 2024
1f12c26
Try recording non-binary responses
brettimus Aug 28, 2024
7731592
Add a German Gans
brettimus Aug 28, 2024
4974ff8
Add showcase request generation app
brettimus Aug 28, 2024
3a3c8c1
Add function calling cloudflare ai example
brettimus Aug 28, 2024
7035608
Rename sample-apps to examples
brettimus Aug 28, 2024
01474b1
Update ai request generation example
brettimus Aug 28, 2024
ef4ff82
Turn off cloudflare binding instrumentation by default, and add it ba…
brettimus Aug 28, 2024
e7fac80
Update dev vars example for goosify
brettimus Aug 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/client-library-otel/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export const FPX_REQUEST_ENV = "fpx.http.request.env";

export const FPX_RESPONSE_BODY = "fpx.http.response.body";

export const CF_BINDING_TYPE = "cf.binding.type";
export const CF_BINDING_NAME = "cf.binding.name";
export const CF_BINDING_METHOD = "cf.binding.method";
export const CF_BINDING_RESULT = "cf.binding.result";
export const CF_BINDING_ERROR = "cf.binding.error";

// NOT YET IMPLEMENTED
export const FPX_REQUEST_HANDLER_FILE = "fpx.http.request.handler.file";
export const FPX_REQUEST_HANDLER_SOURCE_CODE =
Expand Down
63 changes: 49 additions & 14 deletions packages/client-library-otel/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import type { ExecutionContext } from "hono";
// TODO figure out we can use something else
import { AsyncLocalStorageContextManager } from "./async-hooks";
import { measure } from "./measure";
import { patchConsole, patchFetch, patchWaitUntil } from "./patch";
import {
patchCloudflareBindings,
patchConsole,
patchFetch,
patchWaitUntil,
} from "./patch";
import { propagateFpxTraceId } from "./propagation";
import { isRouteInspectorRequest, respondWithRoutes } from "./routes";
import type { HonoLikeApp, HonoLikeEnv, HonoLikeFetch } from "./types";
Expand All @@ -20,32 +25,49 @@ import {
getRootRequestAttributes,
} from "./utils";

/**
* The type for the configuration object we use to configure the instrumentation
* Different from @FpxConfigOptions because all properties are required
*
* @internal
*/
type FpxConfig = {
monitor: {
/** Send data to FPX about each fetch call made during a handler's lifetime */
/** Send data to FPX about each `fetch` call made during a handler's lifetime */
fetch: boolean;
/** Send data to FPX about each `console.*` call made during a handler's lifetime */
logging: boolean;
/** Proxy Cloudflare bindings to add instrumentation */
cfBindings: boolean;
};
};

// TODO - Create helper type for making deeply partial types
/**
* The type for the configuration object the user might pass to `instrument`
* Different from @FpxConfig because all properties are optional
*
* @public
*/
type FpxConfigOptions = Partial<
FpxConfig & {
monitor: Partial<FpxConfig["monitor"]>;
}
>;

const defaultConfig = {
// TODO - Implement library debug logging
// libraryDebugMode: false,
monitor: {
fetch: true,
logging: true,
cfBindings: true,
},
};

export function instrument(app: HonoLikeApp, config?: FpxConfigOptions) {
// Freeze the web standard fetch function so that we can use it below to report registered routes back to fpx studio
const webStandardFetch = fetch;

return new Proxy(app, {
// Intercept the `fetch` function on the Hono app instance
get(target, prop, receiver) {
Expand All @@ -54,35 +76,44 @@ export function instrument(app: HonoLikeApp, config?: FpxConfigOptions) {
const originalFetch = value as HonoLikeFetch;
return async function fetch(
request: Request,
env: HonoLikeEnv,
// Name this "rawEnv" because we coerce it below into something that's easier to work with
rawEnv: HonoLikeEnv,
executionContext: ExecutionContext | undefined,
) {
// NOTE - We used to have a handy default for the fpx endpoint, but we need to remove that,
const env = rawEnv as
| undefined
| null
| Record<string, string | null>;

// NOTE - We do *not* want to have a default for the FPX_ENDPOINT,
// so that people won't accidentally deploy to production with our middleware and
// start sending data to the default url.
const endpoint =
typeof env === "object" && env !== null
? (env as Record<string, string | null>).FPX_ENDPOINT
: null;
typeof env === "object" && env !== null ? env.FPX_ENDPOINT : null;
const isEnabled = !!endpoint && typeof endpoint === "string";

if (!isEnabled) {
return await originalFetch(request, env, executionContext);
return await originalFetch(request, rawEnv, executionContext);
}

// If the request is from the route inspector, respond with the routes
if (isRouteInspectorRequest(request)) {
return respondWithRoutes(webStandardFetch, endpoint, app);
}

const serviceName =
(env as Record<string, string | null>).FPX_SERVICE_NAME ??
"unknown";
const serviceName = env?.FPX_SERVICE_NAME ?? "unknown";

// Patch the related functions to monitor
const {
monitor: { fetch: monitorFetch, logging: monitorLogging },
monitor: {
fetch: monitorFetch,
logging: monitorLogging,
cfBindings: monitorCfBindings,
},
} = mergeConfigs(defaultConfig, config);
if (monitorCfBindings) {
patchCloudflareBindings(env);
}
if (monitorLogging) {
patchConsole();
}
Expand Down Expand Up @@ -171,7 +202,11 @@ export function instrument(app: HonoLikeApp, config?: FpxConfigOptions) {

try {
return await context.with(activeContext, async () => {
return await measuredFetch(newRequest, env, proxyExecutionCtx);
return await measuredFetch(
newRequest,
env as HonoLikeEnv,
proxyExecutionCtx,
);
});
} finally {
// Make sure all promises are resolved before sending data to the server
Expand Down
Loading
Loading