Skip to main content

Testing Cloudflare Workers with OpenTelemetry and Tracetest

Tracetest is a testing tool based on OpenTelemetry that allows you to test your distributed application. It allows you to use data from distributed traces generated by OpenTelemetry to validate and assert if your application has the desired behavior defined by your test definitions.

Cloudflare Workers are Cloudflare's answer to AWS Lambda. They let you deploy serverless code instantly across the globe and are blazing fast. You write code and deploy it to cloud environments without the need for traditional infrastructure.

Why is this important?

Serverless architectural patterns are notoriously difficult to troubleshoot in production and complex to test across development and staging environments including integration tests.

Using OpenTelemetry in Cloudflare Workers exposes telemetry that you can use for both production visibility and trace-based testing.

This sample shows how to run tests against Cloudflare Workers using OpenTelemetry and Tracetest by:

  1. Testing Cloudflare Workers in your local development environment.
  2. Testing Cloudflare Workers in live staging and production deployments.
  3. Integration testing Cloudflare Workers for CI pipelines.

The Cloudflare Worker will fetch data from an external API, transform the data and insert it into a D1 database. This particular flow has two failure points that are difficult to test.

  1. Validating that an external API request from a Cloudflare Worker is successful.
  2. Validating that a D1 database insert request is successful.

Prerequisites

Tracetest Account

Cloudflare Account

Cloudflare Workers Example

Clone the Tracetest GitHub Repo to your local machine, and open the Vercel example app.

git clone https://github.com/kubeshop/tracetest.git
cd tracetest/examples/testing-cloudflare-workers

Before moving forward, run npm i in the root folder to install the dependencies.

npm i

If you do not have npx installed, install it first.

npm i npx -g

Run the command to login to your Cloudflare Workers account.

npx wrangler login

Run the command to create a D1 database both locally and in your Cloudflare Workers account.

npx wrangler d1 create testing-cloudflare-workers

This will output the database_id credentials. Set them in your wrangler.toml as explained in this section below.

Run the command to create a schema in your D1 database with a schema.sql file.

DROP TABLE IF EXISTS Pokemon;
CREATE TABLE IF NOT EXISTS Pokemon (
id INTEGER PRIMARY KEY,
name TEXT,
createdAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
npx wrangler d1 execute testing-cloudflare-workers --local --file=./schema.sql
npx wrangler d1 execute testing-cloudflare-workers --file=./schema.sql

Docker

Have Docker and Docker Compose installed on your machine.

Project Structure

This is a project bootstrapped with C3 (create-cloudflare-cli).

It's using Cloudflare Workers with OpenTelemetry configured with otel-cf-workers.

1. Cloudflare Worker

The Cloudflare Worker code is in src/index.ts. The docker-compose.yaml file references the Cloudflare Worker with cloudflare-worker.

2. Tracetest

The docker-compose.yaml file also has a Tracetest Agent service and an integration tests service.

Docker Compose Network

All services in the docker-compose.yaml are on the same network and will be reachable by hostname from within other services. E.g. cloudflare-worker:8787 in the test/test-api.docker.yaml will map to the cloudflare-worker service.

Cloudflare Worker

The Cloudflare Worker is a simple API, contained in the src/index.ts file.

import { trace, SpanStatusCode, diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'
import { instrument, ResolveConfigFn } from '@microlabs/otel-cf-workers'
const tracer = trace.getTracer('pokemon-api')

diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG)

export interface Env {
DB: D1Database
TRACETEST_URL: string
}

export async function addPokemon(pokemon: any, env: Env) {
return await env.DB.prepare(
"INSERT INTO Pokemon (name) VALUES (?) RETURNING *"
).bind(pokemon.name).all()
}

export async function getPokemon(pokemon: any, env: Env) {
return await env.DB.prepare(
"SELECT * FROM Pokemon WHERE id = ?;"
).bind(pokemon.id).all();
}

async function formatPokeApiResponse(response: any) {
const { headers } = response
const contentType = headers.get("content-type") || ""
if (contentType.includes("application/json")) {
const data = await response.json()
const { name, id } = data

// Add manual instrumentation
const span = trace.getActiveSpan()
if(span) {
span.setStatus({ code: SpanStatusCode.OK, message: String("Pokemon fetched successfully!") })
span.setAttribute('pokemon.name', name)
span.setAttribute('pokemon.id', id)
}

return { name, id }
}
return response.text()
}

const handler = {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
const { pathname, searchParams } = new URL(request.url)

// Import a Pokemon
if (pathname === "/api/pokemon" && request.method === "POST") {
const queryId = searchParams.get('id')
const requestUrl = `https://pokeapi.co/api/v2/pokemon/${queryId || '6'}`
const response = await fetch(requestUrl)
const resPokemon = await formatPokeApiResponse(response)

// Add manual instrumentation
return tracer.startActiveSpan('D1: Add Pokemon', async (span) => {
const addedPokemon = await addPokemon(resPokemon, env)

span.setStatus({ code: SpanStatusCode.OK, message: String("Pokemon added successfully!") })
span.setAttribute('pokemon.name', String(addedPokemon?.results[0].name))
span.end()

return Response.json(addedPokemon)
})
}

return new Response("Hello Worker!")
} catch (err) {
return new Response(String(err))
}
},
}

const config: ResolveConfigFn = (env: Env, _trigger) => {
return {
exporter: {
url: env.TRACETEST_URL,
headers: { },
},
service: { name: 'pokemon-api' },
}
}

export default instrument(handler, config)

The OpenTelemetry tracing is included with the otel-cf-workers module. Traces will be sent to the Tracetest Agent as configured in the wrangler.toml.

name = "pokemon-api"
main = "src/index.ts"
compatibility_date = "2023-12-18"
compatibility_flags = [ "nodejs_compat" ]

# Set the IP to make the Cloudflare Worker available in Docker containers
[dev]
ip = "0.0.0.0"
port = 8787
local_protocol = "http"

# Development
[env.dev]
name = "pokemon-api-dev"
main = "src/index.ts"
compatibility_date = "2023-12-18"
compatibility_flags = [ "nodejs_compat" ]
d1_databases = [
{ binding = "DB", database_name = "testing-cloudflare-workers", database_id = "<YOUR_DATABASE_ID>" },
]
[env.dev.vars]
TRACETEST_URL = "http://localhost:4318/v1/traces"

# Prod
[env.prod]
name = "pokemon-api"
main = "src/index.ts"
compatibility_date = "2023-12-18"
compatibility_flags = [ "nodejs_compat" ]
workers_dev = true
d1_databases = [
{ binding = "DB", database_name = "testing-cloudflare-workers", database_id = "<YOUR_DATABASE_ID>" },
]
[env.prod.vars]
TRACETEST_URL = "https://<YOUR_TRACETEST_AGENT_URL>.tracetest.io:443/v1/traces"

# Docker
[env.docker]
name = "pokemon-api-docker"
main = "src/index.ts"
compatibility_date = "2023-12-18"
compatibility_flags = [ "nodejs_compat" ]
d1_databases = [
{ binding = "DB", database_name = "testing-cloudflare-workers", database_id = "<YOUR_DATABASE_ID>" },
]
[env.docker.vars]
TRACETEST_URL = "http://tracetest-agent:4318/v1/traces"

# D1
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "testing-cloudflare-workers"
database_id = "<YOUR_DATABASE_ID>"

Set up Environment Variables

Edit the wrangler.toml file. Add your D1 and Tracetest env vars.

[env.prod.vars]
TRACETEST_URL = "https://<YOUR_TRACETEST_AGENT_URL>.tracetest.io:443/v1/traces"

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "testing-cloudflare-workers"
database_id = "<YOUR_DATABASE_ID>"

Start the Cloudflare Worker

npx wrangler dev --env dev

# or:
# npm run dev

This starts the worker on http://localhost:8787/ and it will listen for POST requests on path /api/pokemon.

Testing the Cloudflare Worker Locally

Download the CLI for your operating system.

The CLI is bundled with Tracetest Agent that runs in your infrastructure to collect responses and traces for tests.

To start Tracetest Agent add the --api-key from your environment.

tracetest start --api-key YOUR_AGENT_API_KEY

Run a test with the test definition test/test-api.development.yaml.

type: Test
spec:
id: WMGTfM2Sg
name: Test API
trigger:
type: http
httpRequest:
method: POST
url: http://localhost:8787/api/pokemon?id=13
headers:
- key: Content-Type
value: application/json
specs:
- selector: span[tracetest.span.type="faas" name="POST" faas.trigger="http"]
name: Validate cold start
assertions:
- attr:faas.coldstart = "false"
- selector: "span[tracetest.span.type=\"http\" name=\"GET: pokeapi.co\"]"
name: Validate external API.
assertions:
- attr:http.response.status_code = 200
- selector: "span[tracetest.span.type=\"general\" name=\"D1: Add Pokemon\"]"
name: Validate Pokemon name.
assertions:
- attr:pokemon.name = "weedle"
tracetest run test -f ./test/test-api.development.yaml --required-gates test-specs --output pretty

[Output]
✘ Test API Prod (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/WMGTfM2Sg/run/1/test) - trace id: 59775e06cd96ee0a3973fa924fcf587a
✘ Validate cold start
✘ #2cff773d8ea49f9c
✘ attr:faas.coldstart = "false" (true) (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/WMGTfM2Sg/run/1/test?selectedAssertion=0&selectedSpan=2cff773d8ea49f9c)
✔ Validate external API.
✔ #d01b92c183b45433
✔ attr:http.response.status_code = 200 (200)
✔ Validate Pokemon name.
✔ #12443dd73de11a68
✔ attr:pokemon.name = "weedle" (weedle)

✘ Required gates
✘ test-specs

Testing the Clodflare Worker in Staging and Production

Select to run the Tracetest Agent in the Cloud. OpenTelemetry will be selected as the default tracing backend. You'll find the OTLP endpoint to send traces to.

Copy the HTTP URL and paste it in the wrangler.toml and append v1/traces to the end of the Tracetest URL.

# Production
[env.prod]
name = "pokemon-api"
main = "src/index.ts"
compatibility_date = "2023-12-18"
compatibility_flags = [ "nodejs_compat" ]
workers_dev = true
d1_databases = [
{ binding = "DB", database_name = "testing-cloudflare-workers", database_id = "<YOUR_DATABASE_ID>" },
]
[env.prod.vars]
TRACETEST_URL = "https://<YOUR_TRACETEST_AGENT_URL>.tracetest.io:443/v1/traces"

Deploy the Cloudflare Worker.

npx wrangler deploy --env prod

# or
# npm run deploy

Run a test with the test definition test/test-api.prod.yaml. Replace the Cloudflare Worker URL with your endpoint.

type: Test
spec:
id: WMGTfM2Sg
name: Test API Prod
trigger:
type: http
httpRequest:
method: POST
url: https://pokemon-api.<YOUR_URL>.workers.dev/api/pokemon?id=13
headers:
- key: Content-Type
value: application/json
specs:
- selector: span[tracetest.span.type="faas" name="POST" faas.trigger="http"]
name: Validate cold start
assertions:
- attr:faas.coldstart = "false"
- selector: "span[tracetest.span.type=\"http\" name=\"GET: pokeapi.co\"]"
name: Validate external API.
assertions:
- attr:http.response.status_code = 200
- selector: "span[tracetest.span.type=\"general\" name=\"D1: Add Pokemon\"]"
name: Validate Pokemon name.
assertions:
- attr:pokemon.name = "weedle"
tracetest run test -f ./test/test-api.prod.yaml --required-gates test-specs --output pretty

[Output]
✘ Test API Prod (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/WMGTfM2Sg/run/1/test) - trace id: 59775e06cd96ee0a3973fa924fcf587a
✘ Validate cold start
✘ #2cff773d8ea49f9c
✘ attr:faas.coldstart = "false" (true) (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/WMGTfM2Sg/run/1/test?selectedAssertion=0&selectedSpan=2cff773d8ea49f9c)
✔ Validate external API.
✔ #d01b92c183b45433
✔ attr:http.response.status_code = 200 (200)
✔ Validate Pokemon name.
✔ #12443dd73de11a68
✔ attr:pokemon.name = "weedle" (weedle)

✘ Required gates
✘ test-specs

Integration Testing the Cloudflare Worker

Edit the docker-compose.yaml in the root directory. Add your TRACETEST_API_KEY.

  # [...]
tracetest-agent:
image: kubeshop/tracetest-agent:latest
environment:
- TRACETEST_API_KEY=ttagent_<api_key> # Find the Agent API Key here: https://docs.tracetest.io/configuration/agent
ports:
- 4317:4317
- 4318:4318
command: ["--mode", "verbose"]
networks:
- tracetest

Edit the run.bash. Add your TRACETEST_API_TOKEN.

#/bin/bash

# Find the API Token here: https://docs.tracetest.io/concepts/environment-tokens
tracetest configure -t <YOUR_TRACETEST_API_TOKEN> # add your token here
tracetest run test -f ./test-api.docker.yaml --required-gates test-specs --output pretty
# Add more tests here! :D

Now you can run the Vercel function and Tracetest Agent!

docker compose up -d --build

And, trigger the integration tests.

docker compose run integration-tests

[Ouput]
[+] Creating 1/0
✔ Container integration-testing-vercel-functions-tracetest-agent-1 Running 0.0s
SUCCESS Successfully configured Tracetest CLI
✔ Test API Docker (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/p00W82OIR/run/8/test) - trace id: d64ab3a6f52a98141d26679fff3373b6
✘ Validate cold start
✘ #2cff773d8ea49f9c
✘ attr:faas.coldstart = "false" (true) (https://app.tracetest.io/organizations/<YOUR_ORG>/environments/<YOUR_ENV>/test/WMGTfM2Sg/run/1/test?selectedAssertion=0&selectedSpan=2cff773d8ea49f9c)
✔ Validate external API.
✔ #d01b92c183b45433
✔ attr:http.response.status_code = 200 (200)
✔ Validate Pokemon name.
✔ #12443dd73de11a68
✔ attr:pokemon.name = "weedle" (weedle)

✘ Required gates
✘ test-specs

Learn More

Feel free to check out our examples in GitHub and join our Slack Community for more info!