Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BreadcrumbItem, BreadcrumbPage } from "@/components/ui/breadcrumb";
export default function Page() {
return (
<BreadcrumbItem>
<BreadcrumbPage>GraphQL API (ENS v1 + v2)</BreadcrumbPage>
<BreadcrumbPage>Omnigraph API (ENS v1 + v2)</BreadcrumbPage>
</BreadcrumbItem>
);
}
2 changes: 2 additions & 0 deletions apps/ensapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@pothos/core": "^4.10.0",
"@pothos/plugin-dataloader": "^4.4.3",
"@pothos/plugin-relay": "^4.6.2",
"@pothos/plugin-tracing": "^1.1.2",
"@pothos/tracing-opentelemetry": "^1.1.3",
"@standard-schema/utils": "^0.3.0",
"dataloader": "^2.2.3",
"date-fns": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion apps/ensapi/src/config/config.singleton.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("ensdb singleton bootstrap", () => {
expect(ensDbClient.ensIndexerSchemaName).toBe(VALID_SCHEMA_NAME);
expect(ensDb).toBeDefined();
expect(ensIndexerSchema).toBeDefined();
});
}, 10_000);

it("exits when DATABASE_URL is missing", async () => {
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {
Expand Down
12 changes: 6 additions & 6 deletions apps/ensapi/src/lib/instrumentation/auto-span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
// The following internal functions implement the pattern of executing `fn` in the context of a span
// named `name` with attributes via `args` and semantic OTel handling of responses / errors.

export async function withActiveSpanAsync<Fn extends (span: Span) => Promise<any>>(
export async function withActiveSpanAsync<T>(
tracer: Tracer,
name: string,
args: Record<string, AttributeValue>,
fn: Fn,
): Promise<ReturnType<Fn>> {
fn: (span: Span) => Promise<T>,
): Promise<T> {
return tracer.startActiveSpan(name, async (span) => {
// add provded args to span attributes
for (const [key, value] of Object.entries(args)) {
Expand Down Expand Up @@ -71,12 +71,12 @@ export function withSpan<Fn extends (span: Span) => any>(
}
}

export async function withSpanAsync<Fn extends (span: Span) => Promise<any>>(
export async function withSpanAsync<T>(
tracer: Tracer,
name: string,
args: Record<string, AttributeValue>,
fn: Fn,
): Promise<ReturnType<Fn>> {
fn: (span: Span) => Promise<T>,
): Promise<T> {
const span = tracer.startSpan(name);

// add provded args to span attributes
Expand Down
55 changes: 54 additions & 1 deletion apps/ensapi/src/omnigraph-api/builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { trace } from "@opentelemetry/api";
import SchemaBuilder, { type MaybePromise } from "@pothos/core";
import DataloaderPlugin from "@pothos/plugin-dataloader";
import RelayPlugin from "@pothos/plugin-relay";
import TracingPlugin, { isRootField } from "@pothos/plugin-tracing";
import { AttributeNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
import type {
ChainId,
CoinType,
Expand All @@ -16,10 +19,40 @@ import type {
ResolverId,
ResolverRecordsId,
} from "enssdk";
import { getNamedType } from "graphql";
import superjson from "superjson";
import type { Address, Hex } from "viem";

import type { context } from "@/omnigraph-api/context";

const tracer = trace.getTracer("graphql");
const createSpan = createOpenTelemetryWrapper(tracer, {
includeSource: false,
// NOTE: the native implementation of `includeArgs` doesn't handle bigints, so we re-implement in onSpan below
// https://github.com/hayes/pothos/blob/9fadc4916929a838671714fb7cf8d6bb382bcf14/packages/tracing-opentelemetry/src/index.ts#L54
includeArgs: false,
onSpan: (span, options, parent, args, ctx, info) => {
// inject the graphql.field.args attribute using superjson to handle our BigInt scalar
span.setAttribute(AttributeNames.FIELD_ARGS, superjson.stringify(args));

// name edge spans as the parent's "Typename.fieldName" for clarity
if (info.fieldName === "edges") {
return span.updateName(`${info.path.prev?.typename}.${info.path.prev?.key}`);
}

// turn an *Edge.node span name into "Typename([:id])", ex: 'ENSv2Domain([:id])'
if (info.parentType.name.endsWith("Edge") && info.fieldName === "node") {
const typename = getNamedType(info.returnType).name;
const id = (parent as any).node?.id ?? "?";

return span.updateName(`${typename}(${id})`);
}

// otherwise name the span as "Typename.fieldName", ex: 'Query.domain'
return span.updateName(`${info.parentType.name}.${info.fieldName}`);
},
});

export const builder = new SchemaBuilder<{
Context: ReturnType<typeof context>;
Scalars: {
Expand All @@ -46,7 +79,27 @@ export const builder = new SchemaBuilder<{
totalCount: MaybePromise<number>;
};
}>({
plugins: [DataloaderPlugin, RelayPlugin],
plugins: [TracingPlugin, DataloaderPlugin, RelayPlugin],
tracing: {
default: (config) => {
// NOTE: if you need all the tracing possible in order to debug something, you can return true
// from this fn and pothos will wrap every resolver in a span for your otel trace, but it may
// be quite verbose

// always start a root span
if (isRootField(config)) return true;

// always include edges for hierarchy
// NOTE: this means that setting tracing: true on connection fields will result in (somewhat)
// superfluous spans, though technically they measure different things
if (config.name === "edges") return true;

// note that we don't trace node by default, as this results in lots of spans (one for each node)

return false;
},
wrap: (resolver, options) => createSpan(resolver, options),
},
relay: {
// disable the Query.node & Query.nodes methods
nodeQueryOptions: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { trace } from "@opentelemetry/api";
import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay";
import { and, count } from "drizzle-orm";

import { ensDb } from "@/lib/ensdb/singleton";
import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span";
import { makeLogger } from "@/lib/logger";
import type { context as createContext } from "@/omnigraph-api/context";
import type {
Expand Down Expand Up @@ -44,6 +46,7 @@ interface FindDomainsOrderArg {
*/
type DomainWithOrderValue = Domain & { __orderValue: DomainOrderValue };

const tracer = trace.getTracer("find-domains");
const logger = makeLogger("find-domains-resolver");

/**
Expand Down Expand Up @@ -97,11 +100,10 @@ export function resolveFindDomains(

return lazyConnection({
totalCount: () =>
ensDb
.with(domains)
.select({ count: count() })
.from(domains)
.then((rows) => rows[0].count),
withActiveSpanAsync(tracer, "find-domains.totalCount", {}, async () => {
const rows = await ensDb.with(domains).select({ count: count() }).from(domains);
return rows[0].count;
}),

connection: () =>
resolveCursorConnection(
Expand Down Expand Up @@ -146,12 +148,25 @@ export function resolveFindDomains(
// log the generated SQL for debugging
logger.debug({ sql: query.toSQL() });

// execute query
const results = await query;
// execute paginated query
const results = await withActiveSpanAsync(
tracer,
"find-domains.connection",
{ orderBy, orderDir, limit },
() => query.execute(),
);

// load Domain entities via dataloader
const loadedDomains = await rejectAnyErrors(
DomainInterfaceRef.getDataloader(context).loadMany(results.map((result) => result.id)),
const loadedDomains = await withActiveSpanAsync(
tracer,
"find-domains.dataloader",
{ count: results.length },
() =>
rejectAnyErrors(
DomainInterfaceRef.getDataloader(context).loadMany(
results.map((result) => result.id),
),
),
);

// map results by id for faster order value lookup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import config from "@/config";

import { trace } from "@opentelemetry/api";
import { Param, sql } from "drizzle-orm";
import { namehash } from "viem";

Expand All @@ -16,13 +17,15 @@ import {
} from "@ensnode/ensnode-sdk";

import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span";
import { lazy } from "@/lib/lazy";
import { makeLogger } from "@/lib/logger";

// lazy() defers construction until first use so that this module can be
// imported without env vars being present (e.g. during OpenAPI generation).
const getRootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.namespace));
const _maybeGetENSv2RootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.namespace));

const tracer = trace.getTracer("get-domain-by-interpreted-name");
const logger = makeLogger("get-domain-by-interpreted-name");
const v1Logger = makeLogger("get-domain-by-interpreted-name:v1");
const v2Logger = makeLogger("get-domain-by-interpreted-name:v2");
Expand Down Expand Up @@ -63,18 +66,27 @@ const v2Logger = makeLogger("get-domain-by-interpreted-name:v2");
export async function getDomainIdByInterpretedName(
name: InterpretedName,
): Promise<DomainId | null> {
// Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time
const rootRegistryId = getRootRegistryId();
const [v1DomainId, v2DomainId] = await Promise.all([
v1_getDomainIdByInterpretedName(name),
// only resolve v2Domain if ENSv2 Root Registry is defined
rootRegistryId ? v2_getDomainIdByInterpretedName(rootRegistryId, name) : null,
]);

logger.debug({ v1DomainId, v2DomainId });

// prefer v2Domain over v1Domain
return v2DomainId || v1DomainId || null;
return withActiveSpanAsync(tracer, "getDomainIdByInterpretedName", { name }, async () => {
// Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time
const rootRegistryId = _maybeGetENSv2RootRegistryId();

const [v1DomainId, v2DomainId] = await Promise.all([
withActiveSpanAsync(tracer, "v1_getDomainId", {}, () =>
v1_getDomainIdByInterpretedName(name),
),
// only resolve v2Domain if ENSv2 Root Registry is defined
rootRegistryId
? withActiveSpanAsync(tracer, "v2_getDomainId", {}, () =>
v2_getDomainIdByInterpretedName(rootRegistryId, name),
)
: null,
]);

logger.debug({ v1DomainId, v2DomainId });

// prefer v2Domain over v1Domain
return v2DomainId || v1DomainId || null;
});
}

/**
Expand Down
25 changes: 17 additions & 8 deletions apps/ensapi/src/omnigraph-api/schema/domain.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { trace } from "@opentelemetry/api";
import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay";
import { and, count, eq, getTableColumns } from "drizzle-orm";

Expand All @@ -9,6 +10,7 @@ import {
} from "@ensnode/ensnode-sdk";

import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
import { withSpanAsync } from "@/lib/instrumentation/auto-span";
import { builder } from "@/omnigraph-api/builder";
import {
orderPaginationBy,
Expand Down Expand Up @@ -41,6 +43,7 @@ import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration";
import { RegistryRef } from "@/omnigraph-api/schema/registry";
import { ResolverRef } from "@/omnigraph-api/schema/resolver";

const tracer = trace.getTracer("schema/Domain");
const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in domain;

/////////////////////////////
Expand All @@ -49,21 +52,25 @@ const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in d

export const ENSv1DomainRef = builder.loadableObjectRef("ENSv1Domain", {
load: (ids: ENSv1DomainId[]) =>
ensDb.query.v1Domain.findMany({
where: (t, { inArray }) => inArray(t.id, ids),
with: { label: true },
}),
withSpanAsync(tracer, "ENSv1Domain.load", { count: ids.length }, () =>
ensDb.query.v1Domain.findMany({
where: (t, { inArray }) => inArray(t.id, ids),
with: { label: true },
}),
),
toKey: getModelId,
cacheResolved: true,
sort: true,
});

export const ENSv2DomainRef = builder.loadableObjectRef("ENSv2Domain", {
load: (ids: ENSv2DomainId[]) =>
ensDb.query.v2Domain.findMany({
where: (t, { inArray }) => inArray(t.id, ids),
with: { label: true },
}),
withSpanAsync(tracer, "ENSv2Domain.load", { count: ids.length }, () =>
ensDb.query.v2Domain.findMany({
where: (t, { inArray }) => inArray(t.id, ids),
with: { label: true },
}),
),
toKey: getModelId,
cacheResolved: true,
sort: true,
Expand Down Expand Up @@ -126,6 +133,7 @@ DomainInterfaceRef.implement({
name: t.field({
description:
"The Canonical Name for this Domain. If the Domain is not Canonical, then `name` will be null.",
tracing: true,
type: "Name",
nullable: true,
resolve: async (domain, args, context) => {
Expand Down Expand Up @@ -161,6 +169,7 @@ DomainInterfaceRef.implement({
path: t.field({
description:
"The Canonical Path from the ENS Root to this Domain. `path` is null if the Domain is not Canonical.",
tracing: true,
type: [DomainInterfaceRef],
nullable: true,
resolve: async (domain, args, context) => {
Expand Down
14 changes: 7 additions & 7 deletions apps/ensapi/src/omnigraph-api/yoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { makeLogger } from "@/lib/logger";
import { context } from "@/omnigraph-api/context";
import { schema } from "@/omnigraph-api/schema";

const logger = makeLogger("ensnode-graphql");
const logger = makeLogger("omnigraph");

export const yoga = createYoga({
graphqlEndpoint: "*",
Expand Down Expand Up @@ -40,10 +40,10 @@ export const yoga = createYoga({
// integrate logging with pino
logging: logger,

// TODO: plugins
// plugins: [
// maxTokensPlugin({ n: maxOperationTokens }),
// maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }),
// maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }),
// ],
plugins: [
// TODO: plugins
// maxTokensPlugin({ n: maxOperationTokens }),
// maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }),
// maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }),
],
});
2 changes: 1 addition & 1 deletion packages/integration-test-env/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ async function main() {

// Phase 6: Run integration tests
log("Running integration tests...");
execaSync("pnpm", ["test:integration"], {
execaSync("pnpm", ["test:integration", "--", "--bail", "1"], {
cwd: MONOREPO_ROOT,
stdio: "inherit",
env: {
Expand Down
Loading
Loading