Skip to content
Maple Docs
Open app
Browse the docs
On this page

Alchemy

Send a Cloudflare Worker's traces, logs and metrics to Maple from your alchemy.run.ts, then declare alerts and dashboards next to the infrastructure they watch.

@maple-dev/alchemy connects Alchemy stacks to Maple. Add one layer to a Cloudflare Worker and every request is traced, with its logs and metrics, without touching the handler. When you want more, the same package declares Maple alert rules, dashboards and keys in your alchemy.run.ts, so they deploy with the service they observe.

Package@maple-dev/alchemy
Peer dependenciesalchemy >=2.0.0-beta.80 <3, effect ^4.0.0
Sourcepackages/alchemy-maple

Quick start

Get a Worker’s telemetry into Maple in four steps.

1. Install

npm install @maple-dev/alchemy
pnpm add @maple-dev/alchemy
bun add @maple-dev/alchemy

alchemy and effect are peer dependencies, which your stack already has.

2. Register the provider

Merge Maple.providers() into your stack’s providers. It lets the stack read your organization’s ingest key, so you never copy it by hand.

// alchemy.run.ts
import * as Alchemy from "alchemy"
import * as Cloudflare from "alchemy/Cloudflare"
import * as Maple from "@maple-dev/alchemy"
import { Effect, Layer } from "effect"
import Api from "./src/worker.ts"

export default Alchemy.Stack(
	"my-app",
	{ providers: Layer.mergeAll(Cloudflare.providers(), Maple.providers()) },
	Effect.gen(function* () {
		yield* Api
	}),
)

3. Add telemetry to the Worker

Declare the ingest keys once, then provide Telemetry on the Worker’s init Effect:

// src/maple.ts
import * as Maple from "@maple-dev/alchemy"

export const Ingest = Maple.IngestKeys("ingest")
// src/worker.ts
import * as Cloudflare from "alchemy/Cloudflare"
import { Telemetry } from "@maple-dev/alchemy/telemetry"
import { Effect } from "effect"
import { HttpServerResponse } from "effect/http"
import { Ingest } from "./maple.ts"

export default Cloudflare.Worker(
	"api",
	{ main: import.meta.url },
	Effect.gen(function* () {
		const fetch = Effect.succeed(HttpServerResponse.text("Hello from Maple"))
		return { fetch }
	}).pipe(Effect.provide(Telemetry({ serviceName: "api", ingestKey: Ingest }))),
)

That is the whole integration. Each request becomes a server span, Effect logs and spans inside the handler are attached to it, and the batch is sent after the response so it adds no latency.

4. Deploy

Create an API key in Settings → API Keys. An org admin’s key with full access is simplest; see Authentication for the minimum scopes. Then deploy:

MAPLE_API_KEY=maple_ak_… alchemy deploy

Send a request to the Worker and open Traces in Maple. The api service appears within a few seconds.

Already have the ingest key in the Worker’s env? If MAPLE_INGEST_KEY is set on the Worker another way, call Telemetry({ serviceName: "api" }) without ingestKey. You then don’t need Maple.providers() or an API key at all.

Telemetry options

Telemetry binds these onto the Worker at deploy time:

OptionBound asNotes
ingestKeyMAPLE_INGEST_KEY (secret)A Maple.IngestKeys resource (its private key is used) or a Redacted key
endpointMAPLE_ENDPOINTDefaults to https://ingest.maple.dev. Use https://ingest.eu.maple.dev for EU orgs
environmentMAPLE_ENVIRONMENTBecomes deployment.environment.name, e.g. your Alchemy stage

Every other option (serviceName, serviceNamespace, dropSpanNames, anticipatedErrorIdentifiers, …) is the Effect SDK’s and is forwarded unchanged. See Effect SDK on Cloudflare Workers for the full list.

Import Telemetry from the /telemetry subpath, not the package root, so the Maple API client stays out of the Worker bundle.

Add alerts and dashboards

The same stack can declare what you watch the Worker with. Add a notification channel, an alert rule that delivers to it, and a dashboard:

// alchemy.run.ts
import * as Alchemy from "alchemy"
import * as Cloudflare from "alchemy/Cloudflare"
import * as Maple from "@maple-dev/alchemy"
import { Effect, Layer, Redacted } from "effect"
import Api from "./src/worker.ts"

export default Alchemy.Stack(
	"my-app",
	{ providers: Layer.mergeAll(Cloudflare.providers(), Maple.providers()) },
	Effect.gen(function* () {
		yield* Api

		const oncall = yield* Maple.AlertDestination("oncall", {
			type: "pagerduty",
			name: "On-call PagerDuty",
			integration_key: Redacted.make(process.env.PAGERDUTY_ROUTING_KEY!),
		})

		// Alchemy creates the destination first, because the rule uses its output.
		yield* Maple.AlertRule("api-errors", {
			name: "API error rate",
			severity: "critical",
			signal_type: "error_rate",
			comparator: "gt",
			threshold: 0.05, // error rates are 0-1 ratios
			window_minutes: 5,
			service_names: ["api"],
			destination_ids: [oncall.destinationId],
		})

		yield* Maple.Dashboard("api-health", {
			name: "API health",
			tags: ["api"],
			time_range: { type: "relative", value: "12h" },
		})
	}),
)

Change a threshold and redeploy, and the rule is updated in place. Remove it from the stack and the next deploy deletes it.

ResourceWhat it manages
Maple.DashboardDashboards, their widgets, sections and variables
Maple.AlertDestinationPagerDuty, webhook, Discord, Telegram and email channels
Maple.AlertRuleAlert rules on any signal, delivering to destinations
Maple.ApiKeyScoped API keys, with in-place rotation
Maple.IngestKeysYour organization’s ingest keys, as outputs (read-only)

Every prop and output is in Alchemy resources.

Authentication

Maple.providers() reads two environment variables at deploy time:

VariableRequiredDefaultMeaning
MAPLE_API_KEYYesA maple_ak_… API key with the scopes your resources need
MAPLE_API_URLNohttps://api.maple.devAPI base URL. Use https://api.eu.maple.dev for EU orgs

The key needs these permissions, depending on which resources the stack declares:

ResourceScopeOrg admin key
Maple.IngestKeys (used by Telemetry)ingest_keys:readYes
Maple.Dashboarddashboards:writeNo
Maple.AlertRule, Maple.AlertDestinationalerts:writeYes
Maple.ApiKeyapi_keys:writeYes

A key with no scopes has full access. See API keys and scopes.

Custom configuration or transport

Maple.providers() reads the environment and uses the runtime’s global fetch. To source the key elsewhere, point at another deployment, or use your own HttpClient, build the providers with providersWithDependencies() and supply both services yourself:

import * as Maple from "@maple-dev/alchemy"
import { Layer, Redacted } from "effect"
import { HttpClient } from "effect/http"

const mapleProviders = Maple.providersWithDependencies().pipe(
	Layer.provide(
		Layer.succeed(Maple.MapleEnvironment, {
			baseUrl: "https://api.eu.maple.dev",
			apiKey: Redacted.make(mySecretStore.get("maple")),
		}),
	),
	Layer.provide(Layer.succeed(HttpClient.HttpClient, myHttpClient)),
)

Maple.MapleApiFromHttpClient() gives you the same API client outside the Alchemy provider collection, for scripts that call the Maple API directly.