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 dependencies | alchemy >=2.0.0-beta.80 <3, effect ^4.0.0 |
| Source | packages/alchemy-maple |
Quick start
Get a Worker’s telemetry into Maple in four steps.
1. Install
npm install @maple-dev/alchemypnpm add @maple-dev/alchemybun add @maple-dev/alchemyalchemy 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_KEYis set on the Worker another way, callTelemetry({ serviceName: "api" })withoutingestKey. You then don’t needMaple.providers()or an API key at all.
Telemetry options
Telemetry binds these onto the Worker at deploy time:
| Option | Bound as | Notes |
|---|---|---|
ingestKey | MAPLE_INGEST_KEY (secret) | A Maple.IngestKeys resource (its private key is used) or a Redacted key |
endpoint | MAPLE_ENDPOINT | Defaults to https://ingest.maple.dev. Use https://ingest.eu.maple.dev for EU orgs |
environment | MAPLE_ENVIRONMENT | Becomes 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.
| Resource | What it manages |
|---|---|
Maple.Dashboard | Dashboards, their widgets, sections and variables |
Maple.AlertDestination | PagerDuty, webhook, Discord, Telegram and email channels |
Maple.AlertRule | Alert rules on any signal, delivering to destinations |
Maple.ApiKey | Scoped API keys, with in-place rotation |
Maple.IngestKeys | Your 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:
| Variable | Required | Default | Meaning |
|---|---|---|---|
MAPLE_API_KEY | Yes | A maple_ak_… API key with the scopes your resources need | |
MAPLE_API_URL | No | https://api.maple.dev | API base URL. Use https://api.eu.maple.dev for EU orgs |
The key needs these permissions, depending on which resources the stack declares:
| Resource | Scope | Org admin key |
|---|---|---|
Maple.IngestKeys (used by Telemetry) | ingest_keys:read | Yes |
Maple.Dashboard | dashboards:write | No |
Maple.AlertRule, Maple.AlertDestination | alerts:write | Yes |
Maple.ApiKey | api_keys:write | Yes |
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.