Skip to content

Commit c7ee9ce

Browse files
committed
support jurisdiction
1 parent 2d6aaf6 commit c7ee9ce

File tree

7 files changed

+69
-7
lines changed

7 files changed

+69
-7
lines changed

readme.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ npm run token-setup
2727

2828
This will prompt for your Cloudflare Account ID, open the browser to create an API token with the required permissions, and collect your Datadog API Key. It automatically creates a `.dev.vars` file.
2929

30+
#### Optional: Jurisdiction
31+
32+
To run GraphQL queries from a specific jurisdiction (closer to the data source), set the `JURISDICTION` variable in `wrangler.jsonc`:
33+
34+
```jsonc
35+
"vars": {
36+
"BATCH_SIZE": "5000",
37+
"JURISDICTION": "eu" // e.g., "eu", "fedramp"
38+
}
39+
```
40+
41+
This uses a Durable Object to proxy requests from the specified jurisdiction.
42+
3043
### Verify
3144

3245
```bash

src/api/cloudflare.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
export interface CloudflareApiConfig {
99
accountId: string;
1010
apiToken: string;
11+
fetcher?: typeof fetch;
1112
}
1213

1314
const ContainersListResponseSchema = z.object({
@@ -62,8 +63,11 @@ query GetCloudchamberMetrics($accountTag: string!, $datetimeStart: Time, $dateti
6263
export class CloudflareApi {
6364
private readonly baseUrl = "https://api.cloudflare.com/client/v4";
6465
private readonly graphqlUrl = "https://api.cloudflare.com/client/v4/graphql";
66+
private readonly fetcher: typeof fetch;
6567

66-
constructor(private readonly config: CloudflareApiConfig) {}
68+
constructor(private readonly config: CloudflareApiConfig) {
69+
this.fetcher = config.fetcher ?? fetch;
70+
}
6771

6872
private get headers(): HeadersInit {
6973
return {
@@ -78,7 +82,7 @@ export class CloudflareApi {
7882
async listContainers(): Promise<Container[]> {
7983
const url = `${this.baseUrl}/accounts/${this.config.accountId}/containers/applications`;
8084

81-
const response = await fetch(url, {
85+
const response = await this.fetcher(url, {
8286
headers: this.headers,
8387
});
8488

@@ -118,7 +122,7 @@ export class CloudflareApi {
118122
applicationId,
119123
};
120124

121-
const response = await fetch(this.graphqlUrl, {
125+
const response = await this.fetcher(this.graphqlUrl, {
122126
method: "POST",
123127
headers: this.headers,
124128
body: JSON.stringify({
@@ -171,6 +175,7 @@ export class CloudflareApi {
171175
export function createCloudflareApi(
172176
accountId: string,
173177
apiToken: string,
178+
fetcher?: typeof fetch,
174179
): CloudflareApi {
175-
return new CloudflareApi({ accountId, apiToken });
180+
return new CloudflareApi({ accountId, apiToken, fetcher });
176181
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { MetricsExporterWorkflow } from "./workflow";
2+
export { RequestProxy } from "./request-proxy";
23

34
const REQUIRED_ENV_VARS = [
45
"CLOUDFLARE_API_TOKEN",

src/request-proxy.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { DurableObject } from "cloudflare:workers";
2+
3+
/**
4+
* Durable Object that proxies fetch requests from a specific region.
5+
* Used to ensure GraphQL queries run in a region close to the data source.
6+
*/
7+
export class RequestProxy extends DurableObject<Env> {
8+
async fetch(request: Request): Promise<Response> {
9+
return fetch(request);
10+
}
11+
}

src/workflow.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,23 @@ const STEP_CONFIG = {
3838
export class MetricsExporterWorkflow extends WorkflowEntrypoint<Env> {
3939
async run(_event: WorkflowEvent<unknown>, step: WorkflowStep) {
4040
const batchSize = Number.parseInt(this.env.BATCH_SIZE || "5000", 10);
41+
42+
// Create a fetcher that proxies requests through a Durable Object in a specific jurisdiction
43+
// This ensures GraphQL queries run close to the data source
44+
let fetcher: typeof fetch | undefined;
45+
if (this.env.JURISDICTION) {
46+
const jurisdiction = this.env.REQUEST_PROXY.jurisdiction(
47+
this.env.JURISDICTION as DurableObjectJurisdiction,
48+
);
49+
const id = jurisdiction.idFromName("metrics-proxy");
50+
const stub = jurisdiction.get(id);
51+
fetcher = stub.fetch.bind(stub);
52+
}
53+
4154
const cloudflare = createCloudflareApi(
4255
this.env.CLOUDFLARE_ACCOUNT_ID,
4356
this.env.CLOUDFLARE_API_TOKEN,
57+
fetcher,
4458
);
4559

4660
const datadog = createDatadogApi(

worker-configuration.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
/* eslint-disable */
2-
// Generated by Wrangler by running `wrangler types` (hash: a2dd970dcb6e3b472df3dd06e13aaecb)
2+
// Generated by Wrangler by running `wrangler types` (hash: e8b40c011669a45effc49e682978268c)
33
// Runtime types generated with workerd@1.20251202.0 2025-11-17
44
declare namespace Cloudflare {
55
interface GlobalProps {
66
mainModule: typeof import("./src/index");
7+
durableNamespaces: "RequestProxy";
78
}
89
interface Env {
10+
BATCH_SIZE: "5000";
11+
JURISDICTION: "fedramp";
912
CLOUDFLARE_ACCOUNT_ID: string;
1013
CLOUDFLARE_API_TOKEN: string;
1114
DATADOG_API_KEY: string;
1215
DATADOG_SITE: string;
13-
BATCH_SIZE: string;
16+
REQUEST_PROXY: DurableObjectNamespace<import("./src/index").RequestProxy>;
1417
METRICS_WORKFLOW: Workflow<Parameters<import("./src/index").MetricsExporterWorkflow['run']>[0]['payload']>;
1518
}
1619
}

wrangler.jsonc

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,22 @@
1818
"name": "metrics-exporter-workflow"
1919
}
2020
],
21+
"durable_objects": {
22+
"bindings": [
23+
{
24+
"name": "REQUEST_PROXY",
25+
"class_name": "RequestProxy"
26+
}
27+
]
28+
},
29+
"migrations": [
30+
{
31+
"tag": "v1",
32+
"new_sqlite_classes": ["RequestProxy"]
33+
}
34+
],
2135
"vars": {
22-
"BATCH_SIZE": "5000"
36+
"BATCH_SIZE": "5000",
37+
"JURISDICTION": ""
2338
}
2439
}

0 commit comments

Comments
 (0)