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

[HOLD] - Otel implementation POC #224

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
606 changes: 523 additions & 83 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "worker-apollo-server",
"main": "./build/index.js",
"scripts": {
"preinstall": "npx npm-force-resolutions",
"drizzle-kit": "drizzle-kit",
"db:generate": "drizzle-kit generate && prettier ./drizzle/migrations --write",
"db:check": "drizzle-kit check",
Expand Down Expand Up @@ -69,20 +70,27 @@
"engines": {
"node": ">=18"
},
"resolutions": {
"@opentelemetry/api": "1.6.0"
},
"dependencies": {
"@cloudflare/workers-honeycomb-logger": "^2.3.3",
"@envelop/core": "^5.0.0",
"@envelop/immediate-introspection": "^2.0.0",
"@envelop/opentelemetry": "^6.3.1",
"@envelop/rate-limiter": "^5.0.0",
"@faker-js/faker": "^8.0.2",
"@graphql-authz/envelop-plugin": "^1.0.4",
"@graphql-yoga/plugin-csrf-prevention": "^2.0.3",
"@libsql/client": "^0.3.5",
"@microlabs/otel-cf-workers": "^1.0.0-rc.40",
"@neondatabase/serverless": "^0.4.26",
"@opentelemetry/api": "^1.9.0",
"@pothos/core": "^3.30.0",
"@pothos/plugin-authz": "^3.5.8",
"@pothos/plugin-dataloader": "^3.19.0",
"@pothos/plugin-tracing": "^0.5.8",
"@pothos/tracing-opentelemetry": "^1.1.0",
"@sanity/client": "^6.7.0",
"@tsndr/cloudflare-worker-jwt": "^2.5.3",
"@types/react": "^18.2.22",
Expand All @@ -99,6 +107,7 @@
"graphql-yoga": "^5.6.1",
"hono": "^3.9.0",
"mercadopago": "^2.0.9",
"npm-force-resolutions": "^0.0.10",
"p-map": "^6.0.0",
"pino": "^9.2.0",
"pino-pretty": "^11.2.1",
Expand Down
21 changes: 18 additions & 3 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { AttributeValue } from "@opentelemetry/api";
import SchemaBuilder from "@pothos/core";
import AuthzPlugin from "@pothos/plugin-authz";
import DataloaderPlugin from "@pothos/plugin-dataloader";
import TracingPlugin, { wrapResolver } from "@pothos/plugin-tracing";
import TracingPlugin, {
isRootField,
wrapResolver,
} from "@pothos/plugin-tracing";
import { createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
import { DateResolver, DateTimeResolver } from "graphql-scalars";

import * as rules from "~/authz";
import { defaultLogger } from "~/logging";
import { tracer } from "~/tracing";
import { Context } from "~/types";

type TracingOptions = boolean | { attributes?: Record<string, AttributeValue> };

const createSpan = createOpenTelemetryWrapper<TracingOptions>(tracer, {
includeSource: true,
includeArgs: true,
});

export const builder = new SchemaBuilder<{
Context: Context;
AuthZRule: keyof typeof rules;
Expand All @@ -24,12 +37,14 @@ export const builder = new SchemaBuilder<{
}>({
plugins: [TracingPlugin, AuthzPlugin, DataloaderPlugin],
tracing: {
default: () => true,
default: (config) => isRootField(config),
wrap: (resolver, options, config) =>
wrapResolver(resolver, (error, duration) => {
defaultLogger.debug(
defaultLogger.info(
`[TRACING] ${config.parentType}.${config.name} in ${duration}ms`,
);

return createSpan(resolver, options);
}),
},
});
Expand Down
37 changes: 35 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useMaskedErrors } from "@envelop/core";
import { useImmediateIntrospection } from "@envelop/immediate-introspection";
import { useOpenTelemetry } from "@envelop/opentelemetry";

Check warning on line 3 in src/index.ts

View workflow job for this annotation

GitHub Actions / Linting and Typechecking

'useOpenTelemetry' is defined but never used
import { authZEnvelopPlugin } from "@graphql-authz/envelop-plugin";
import { instrument, ResolveConfigFn } from "@microlabs/otel-cf-workers";
import { trace } from "@opentelemetry/api";
import { createYoga, maskError } from "graphql-yoga";

import { Env } from "worker-configuration";
Expand All @@ -10,6 +13,7 @@
import { APP_ENV } from "~/env";
import { createLogger } from "~/logging";
import { schema } from "~/schema";
import { tracingPlugin } from "~/tracing";

export const yoga = createYoga<Env>({
landingPage: APP_ENV !== "production",
Expand Down Expand Up @@ -65,15 +69,32 @@
}),
useImmediateIntrospection(),
authZEnvelopPlugin({ rules }),
// useOpenTelemetry({
// resolvers: true, // Tracks resolvers calls, and tracks resolvers thrown errors
// variables: true, // Includes the operation variables values as part of the metadata collected
// result: true, // Includes execution result object as part of the metadata collected
// }),
tracingPlugin,
].filter(Boolean),
context: createGraphqlContext,
});

export default {
const handler = {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
const span = trace.getActiveSpan();

const internalUUID = crypto.randomUUID();

span?.setAttribute(
"extrenal-trace-id",
req.headers.get("x-trace-id") ?? "unknown",
);
span?.setAttribute("trace-id", internalUUID);

// const span = trace.getActiveSpan()
const logger = createLogger("graphql", {
externalTraceId: req.headers.get("x-trace-id"),
traceId: crypto.randomUUID(),
traceId: internalUUID,
});

logTraceId(req, logger);
Expand All @@ -90,3 +111,15 @@
return response;
},
};

const config: ResolveConfigFn = (env: Env) => {
return {
exporter: {
url: "https://otel.baselime.io/v1",
headers: { "x-api-key": env.BASELIME_API_KEY },
},
service: { name: "api" },
};
};

export default instrument(handler, config);
35 changes: 35 additions & 0 deletions src/tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { trace as OpenTelemetryTrace } from "@opentelemetry/api";
import { AttributeNames, SpanNames } from "@pothos/tracing-opentelemetry";
import { print } from "graphql";
import { Plugin } from "graphql-yoga";

export const trace = OpenTelemetryTrace;
export const tracer = OpenTelemetryTrace.getTracer("graphql-api");

export const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) =>
tracer.startActiveSpan(
SpanNames.EXECUTE,
{
attributes: {
[AttributeNames.OPERATION_NAME]: options.operationName ?? undefined,

Check failure on line 16 in src/tracing.ts

View workflow job for this annotation

GitHub Actions / Linting and Typechecking

Unsafe assignment of an `any` value
[AttributeNames.SOURCE]: print(options.document),

Check failure on line 17 in src/tracing.ts

View workflow job for this annotation

GitHub Actions / Linting and Typechecking

Unsafe argument of type `any` assigned to a parameter of type `ASTNode`
},
},
async (span) => {
try {
const result = (await executeFn(options)) as unknown;

return result;
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
},
),
);
},
};
1 change: 1 addition & 0 deletions worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface Env {
HIGHLIGHT_PROJECT_ID: string;
HYPERDRIVE: Hyperdrive;
PURCHASE_CALLBACK_URL: string;
BASELIME_API_KEY: string;
RPC_SERVICE_EMAIL: Service<WorkerEntrypoint>;
}

Expand Down
4 changes: 1 addition & 3 deletions workers/transactional_email_service/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { Resend } from "resend";

import { sendTransactionalHTMLEmail } from "~/datasources/email/sendTransactionalHTMLEmail";
import { createLogger } from "~/logging";
import { ENV } from "~workers/transactional_email_service/types";

import { PurchaseOrderSuccessful } from "../../emails/templates/tickets/purchase-order-successful";

type ENV = {
RESEND_API_KEY: string | undefined;
};
export default class EmailService extends WorkerEntrypoint<ENV> {
logger = createLogger("EmailService");

Expand Down
7 changes: 1 addition & 6 deletions workers/transactional_email_service/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
export type ENV = {
NEON_URL: string;
HIGHLIGHT_PROJECT_ID: string;
MP_ACCESS_TOKEN: string;
MP_PUBLIC_KEY: string;
ST_KEY: string;
RV_KEY: string;
RESEND_API_KEY: string | undefined;
};
Loading