惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
V
Visual Studio Blog
C
Check Point Blog
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
博客园 - 聂微东
量子位
T
Tailwind CSS Blog
罗磊的独立博客
I
InfoQ
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Y
Y Combinator Blog
L
LangChain Blog
小众软件
小众软件
Engineering at Meta
Engineering at Meta
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Security Latest
Security Latest
M
MIT News - Artificial intelligence
Know Your Adversary
Know Your Adversary
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
爱范儿
爱范儿
T
The Exploit Database - CXSecurity.com
有赞技术团队
有赞技术团队
V
Vulnerabilities – Threatpost
Martin Fowler
Martin Fowler
A
Arctic Wolf
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
Cyberwarzone
Cyberwarzone
阮一峰的网络日志
阮一峰的网络日志
The Hacker News
The Hacker News
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
GbyAI
GbyAI
Latest news
Latest news
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
腾讯CDC
AWS News Blog
AWS News Blog
aimingoo的专栏
aimingoo的专栏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
Lohrmann on Cybersecurity
博客园 - Franky
S
Securelist
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Threatpost
美团技术团队

Deno

Deno 2.8 | Deno Claw Patrol: an open-source security firewall for agents | Deno Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | Deno Deno 2.7: Temporal API, Windows ARM, and npm overrides | Deno Build a dinosaur runner game with Deno, pt. 5 | Deno Deno Deploy is Generally Available | Deno Introducing Deno Sandbox | Deno Build a dinosaur runner game with Deno, pt. 4 | Deno Build a dinosaur runner game with Deno, pt. 3 | Deno Build a dinosaur runner game with Deno, pt. 2 | Deno React / Next.js Denial-of-Service Vulnerability: Deno Deploy users protected | Deno Deno 2.6: dx is the new npx | Deno Build a dinosaur runner game with Deno, pt. 1 | Deno React Server Functions / Next.js Vulnerability: Deno Deploy users protected | Deno My highlights from the new Deno Deploy | Deno Deno's Other Open Source Projects | Deno How Deno protects against npm exploits | Deno Help Us Raise $200k to Free JavaScript from Oracle | Deno Deno 2.5: Permissions in the config file | Deno Fresh 2.0 Graduates to Beta, Adds Vite Support | Deno Deno 2.4: deno bundle is back | Deno JavaScript™ Trademark Update | Deno What's coming to JavaScript | Deno A brief history of JavaScript | Deno Reports of Deno's Demise Have Been Greatly Exaggerated | Deno An Update on Fresh | Deno How Plaid migrated 100 services to a new database platform 5x faster with Deno | Deno Deno 2.3: Improved deno compile, local npm packages, and more | Deno Add JSR packages with pnpm and Yarn | Deno Zero-config Debugging with Deno and OpenTelemetry | Deno Exploring Art with TypeScript, Jupyter, Polars, and Observable Plot | Deno Deno v Oracle Update 3: Fighting the JavaScript Trademark | Deno Build a custom RAG AI agent in TypeScript and Jupyter | Deno How to get deep traces in your Node.js backend with OTel and Deno | Deno toranoana.deno #20 登録受付中(2025年3月14日) | Deno Node just added TypeScript support. What does that mean for Deno? | Deno The Dino 🦕, the Llama 🦙, and the Whale 🐋 | Deno Publish a lint rule, get a prize | Deno Deno 2.2: OpenTelemetry, Lint Plugins, node:sqlite | Deno If you're not using npm specifiers, you're doing it wrong | Deno How Deno's documentation is evolving | Deno Oracle justified its JavaScript trademark with Node.js—now it wants that ignored | Deno Introducing the JSR open governance board | Deno Intro to Wasm in Deno | Deno Announcing OpenAI on JSR | Deno Deno in 2024 | Deno Goodbye WinterCG, welcome WinterTC | Deno Build a SolidJS app with Deno | Deno Run your Next.js SSR app on Deno Deploy | Deno Solve Advent of Code 2024 with Deno and Win Prizes! | Deno Deno v. Oracle: Canceling the JavaScript Trademark | Deno Deno 2.1: Wasm Imports and other enhancements | Deno Build a Typesafe API with tRPC and Deno | Deno Self-contained Executable Programs with Deno Compile | Deno Build a Database App with Drizzle ORM and Deno | Deno Introducing your new JavaScript package manager: Deno | Deno Announcing Growthbook on JSR | Deno Build an Astro site with Deno | Deno How to convert CommonJS to ESM | Deno Announcing Deno 2 | Deno The Final Touches: What’s New In v2.0.0-rc.10 | Deno Announcing Stable V8 Bindings for Rust | Deno Deno 2.0 Release Candidate | Deno Secure, efficient private npm registries with Cloudsmith and Deno | Deno Painting the Plane as We Fly It: Designing JSR | Deno Introducing Web Cache API support on Deno Deploy | Deno Deno 1.46: The Last 1.x Release | Deno Protect your cloud spend with new Deno Deploy spend limits | Deno What we got wrong about HTTP imports | Deno Benchmarking AWS Lambda Cold Starts Across JavaScript Runtimes | Deno Announcing Supabase on JSR | Deno Deno 1.45: Workspace and Monorepo Support | Deno Introducing KV Backup for Deno Subhosting | Deno A Gentle Intro to TypeScript | Deno Announcing Hono on JSR | Deno How We Made the Deno Language Server Ten Times Faster | Deno How the Guardian uses Deno to audit accessibility and performance across their 2.7 million articles | Deno Introducing More Flexible Domain Association for Deno Subhosting | Deno The stabilization process of the Standard Library has begun | Deno Deno 1.44: Private npm registries, improved Node.js compat, and performance boosts | Deno How we built a secure, performant, multi-tenant cloud platform to run untrusted code | Deno The Deno Standard Library is now available on JSR | Deno How to document your JavaScript package | Deno Your Low Code Solution Needs an Escape Hatch | Deno Deno 1.43: Improved Language Server performance | Deno How Slack used Deno to save months of engineering effort in launching their new platform | Deno JSR Is Not Another Package Manager | Deno Announcing the Hookdeck SDK on JSR | Deno Announcing the Neon Serverless Driver on JSR | Deno An intro to TSConfig for JavaScript Developers | Deno How we built JSR | Deno How Netlify used Deno Subhosting to build a successful edge functions product | Deno Introducing Simpler Project Creation in Deno Deploy | Deno Deno 1.42: Better dependency management with JSR | Deno Introducing deployctl, the command line interface for Deno Deploy | Deno Introducing JSR - the JavaScript Registry | Deno How to add Monaco to a Next.js app and securely run untrusted user code | Deno Survey Results and Roadmap | Deno Deno 1.41: smaller deno compile binaries | Deno Webhooks suck, but here are alternatives | Deno
Build a dinosaur runner game with Deno, pt. 6 | Deno
Jo Franchett · 2026-02-23 · via Deno

This series of blog posts will guide you through building a simple browser-based dinosaur runner game using Deno.

  1. Setup a basic project
  2. Game loop, canvas, and controls
  3. Obstacles and collision detection
  4. Databases and global leaderboards
  5. Player profiles and customization
  6. Observability, metrics, and alerting

Observability, Metrics, and Alerting

In Stage 5, we gave players an identity: a name, a dino colour, a background theme, and a difficulty level, all persisted in PostgreSQL and loaded back on every visit. The game is now feature-complete. But shipping features is only half the story. The other half is understanding what’s happening once real players arrive.

Are the API routes fast enough? Is the leaderboard staying healthy? Is there a spike in errors after a deployment? You can’t answer those questions with console.log("Server is running!"). In this final stage, we’ll wire up an observability layer that makes every question answerable, leaning on Deno Deploy’s built-in logs, traces, and metrics dashboards so you don’t need to reach for a separate monitoring product to get started.

Keep reading and build along or view the entire source here.

What you’ll build

By the end of this post you will have:

  • Structured logs: every HTTP request emits a JSON log line, and key game events (score submissions, customization saves) emit their own structured events that you can search and filter in the Logs dashboard.
  • Custom traces: key operations (score submission, leaderboard fetch, loading player settings) each get their own span, so you can see exactly where time is spent inside a request.

Both are visible in Deno Deploy’s built-in dashboards alongside the platform’s automatic metrics, no extra infrastructure required.

The three pillars of observability

Observability is the ability to understand what your system is doing from the outside, by examining the data it produces. That data typically comes in three forms:

Pillar What it answers
Logs What happened, and when?
Traces Where did the time go?
Metrics How is the system trending over time?

Deno Deploy provides a dashboard for all three. Logs and traces support custom instrumentation, you can add your own structured log events and custom spans. The Metrics dashboard shows platform-level data that Deno Deploy captures automatically, which we’ll look at in its own section.

Setting up the telemetry module

We need one package: the OpenTelemetry API. On Deno Deploy, the runtime wires up the SDK and exporter for you, no other configuration required. The API package gives us the interfaces we call in our code; the platform handles where the data goes.

Add it to deno.json:

{
  "imports": {
    "@oak/oak": "jsr:@oak/oak@17",
    "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
    "npm:pg": "npm:pg@^8.11.0"
  }
}

Create src/telemetry.ts to hold the shared tracer instance:

import { SpanStatusCode, trace } from "@opentelemetry/api";


export const tracer = trace.getTracer("dino-game", "1.0.0");

export { SpanStatusCode };

Keeping this in one place means every file uses the same tracer name and version, which groups all your custom spans together in the dashboard.

Logs

Every console.log call you make is captured by Deno Deploy and shown in the Logs tab of your app’s dashboard. But plain-text logs are hard to filter. The upgrade is to emit structured JSON, a single parseable object per event that you can search by any field.

Create a logging middleware in src/middleware/logging.ts:

import type { Context } from "@oak/oak";

export async function loggingMiddleware(
  ctx: Context,
  next: () => Promise<unknown>,
): Promise<void> {
  const start = performance.now();
  const method = ctx.request.method;
  const path = ctx.request.url.pathname;

  await next();

  const status = ctx.response.status;
  const durationMs = Math.round(performance.now() - start);

  console.log(
    JSON.stringify({
      event: "http_request",
      method,
      path,
      status,
      durationMs,
    }),
  );
}

Register it at the top of your middleware stack in src/main.ts, so it wraps every request:

import { loggingMiddleware } from "./middleware/logging.ts";



app.use(loggingMiddleware);
app.use(corsMiddleware);

After deploying, your Logs dashboard will show a clean stream of structured entries like this:

{"event":"http_request","method":"POST","path":"/api/scores","status":200,"durationMs":43}
{"event":"http_request","method":"GET","path":"/api/leaderboard","status":200,"durationMs":18}

You can filter by any field, for example, typing status:500 to find all errors, or path:/api/scores to see only score submissions.

Business event logs

Beyond request logging, you can emit structured events from inside your route handlers for application-level insight. When a score is saved, we log everything useful about that game:

console.log(
  JSON.stringify({
    event: "score_submitted",
    playerName,
    score,
    globalRank: rank,
    isNewRecord: rank === 1,
    obstaclesAvoided,
    gameDurationSeconds: gameDuration,
    difficulty,
  }),
);

This creates a searchable audit trail of every game that was played. Open the Logs dashboard, filter by event:score_submitted, and you have a live feed of player activity without any dedicated analytics infrastructure. We do the same for customization saves:

console.log(
  JSON.stringify({
    event: "customization_saved",
    playerName,
    backgroundTheme,
    dinoColor,
    difficultyPreference,
  }),
);

Errors get the same treatment, using console.error means they’re easy to distinguish in the dashboard:

console.error(
  JSON.stringify({
    event: "score_submit_error",
    error: (error as Error).message,
  }),
);

Tip: Logs emitted inside a custom span (see the next section) are automatically correlated with that trace in the dashboard. You can click a log line and jump straight to the trace it belongs to.

Traces

A trace is a record of a single operation as it flows through your system. It’s made up of spans. These are named, timed units of work that nest inside each other to form a waterfall diagram.

Deno Deploy automatically creates a root span for every incoming HTTP request and for every outbound fetch call your code makes. What we’re adding here are child spans around the business logic inside each route handler, so you can see exactly where time goes: is a slow /api/leaderboard response caused by the database query, or something in the response serialisation?

Creating a custom span

The pattern is the same everywhere: call tracer.startActiveSpan() with a name, do your work inside the callback, and call span.end() in a finally block so the span is always closed, even if an error is thrown.

import { SpanStatusCode, tracer } from "../telemetry.ts";

router.get("/api/leaderboard", async (ctx: Context) => {
  await tracer.startActiveSpan("leaderboard.fetch", async (span) => {
    try {
      const limit = parseInt(ctx.request.url.searchParams.get("limit") || "10");
      span.setAttribute("leaderboard.limit", limit);

      

      span.setAttribute("leaderboard.rows_returned", rows.length);
      ctx.response.body = { success: true, leaderboard: rows };
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      throw error;
    } finally {
      span.end();
    }
  });
});

Span attributes are key/value pairs attached to a span. They appear in the trace detail view and can be used to filter traces. For example, filtering for spans where leaderboard.rows_returned is 0 would highlight times the database returned an empty result.

span.recordException() captures the full error object, including the stack trace, and attaches it as a span event. This makes it much easier to debug production errors than reading plain error logs.

Spans for score submission

The score submission route does the most work: it validates input, inserts the score, and checks the resulting global rank. All of that lives in a single span, with the most useful facts attached as attributes:

router.post("/api/scores", async (ctx: Context) => {
  await tracer.startActiveSpan("score.submit", async (span) => {
    try {
      

      span.setAttributes({
        "game.player_name": playerName,
        "game.score": score,
        "game.difficulty": difficulty,
      });

      

      if (rank === 1) {
        span.addEvent("new_global_record", { score, playerName });
      }

      span.setAttribute("game.global_rank", rank);
      span.setAttribute("game.is_new_record", rank === 1);
    } finally {
      span.end();
    }
  });
});

span.addEvent() adds a timestamped marker inside the span, this is useful for significant moments that aren’t worth a whole new span. The new_global_record event will appear as a point on the timeline in the trace viewer every time someone sets a new record.

We add the same pattern to the customization routes at customization.load and customization.save, each carrying attributes for player name, theme, and the source of the settings (database, anonymous, or defaults):

span.setAttributes({
  "game.player_name": playerName,
  "customization.theme": backgroundTheme,
  "customization.difficulty": difficultyPreference,
});

Once deployed, open the Traces dashboard in Deno Deploy and click on any POST /api/scores request. You’ll see a waterfall: the outer HTTP span wrapping your score.submit span. Hover over your span to see the attributes and events you attached, and click through to any correlated log lines.

Metrics

The Metrics dashboard in Deno Deploy shows platform-level data that the runtime collects automatically, no code changes required. For this game, these are the most useful panels and what they tell you:

HTTP req/min by status code is the clearest signal of overall health. A jump in 5xx responses after a deployment means something broke; a steady stream of 4xx responses on /api/scores might mean the client is sending malformed data.

HTTP mean latency is your API’s average response time. If this climbs after a deploy, check the Traces dashboard for whichever route has become slow. The latency graph and the trace waterfall work together: the graph tells you something is slow, and the trace tells you which part is slow.

CPU time and memory usage are useful for understanding the cost of traffic spikes. If the leaderboard gets shared and hundreds of players pile in, you’ll see CPU and memory spike here. It’s also a good baseline check after adding new database queries. A query without an index will show up as a CPU spike.

V8 garbage collection time is the high GC time relative to total CPU time is a sign of memory pressure, usually from allocating and discarding large objects in hot paths (like serialising a large leaderboard response on every request).

Total incoming / outgoing bandwidth is helpful for spotting unexpectedly large responses. If outgoing bandwidth is high, the leaderboard response might be returning more rows than expected, or a static asset might not be cached correctly.

Together, these panels give you a good picture of your app’s health without any extra configuration. When something looks wrong, the workflow is: spot the anomaly in Metrics, narrow it to a route using the status-code breakdown, then jump to Traces to find the slow or failing span.

Viewing it all locally with the tunnel

Deno Deploy’s tunnel feature lets you run your server locally while routing telemetry through to the real Deno Deploy dashboards. This means you can verify your instrumentation is working before you deploy:

The first time you run this, a browser will open to authenticate and ask which app to connect to. After that, your local traffic appears in the Deno Deploy dashboard under the context:local filter.

Play the game locally, submit a score, then open the Traces dashboard and confirm that the score.submit span is there with the right attributes. Check the Logs dashboard to see your score_submitted JSON line. Once you’re happy with what you see, deploy for real:

What you can see now

Dashboard What you’ll find
Logs One JSON line per HTTP request (event: "http_request"); structured events for score_submitted, customization_saved, and errors
Traces Custom spans: leaderboard.fetch, score.submit, scores.fetch_personal_bests, customization.load, customization.save, each with attributes for player name, score, rank, theme, and source
Metrics Automatic platform data: HTTP req/min by status, mean latency, CPU time, memory usage, GC time, and bandwidth

Wrapping up the series

Over six posts, we’ve gone from a blank directory to a fully instrumented, globally distributed dinosaur runner game:

  1. A basic Oak server serving static files
  2. A canvas game loop with a playable dino character
  3. Cactus obstacles and pixel-perfect collision detection
  4. A PostgreSQL leaderboard with score submission via API
  5. Player profiles: custom dino colors, themes, and difficulty settings
  6. Full observability: structured logs, custom traces, and platform metrics

The instrumentation from this post gives you the visibility you need to operate the game in production, with the confidence to know when something breaks before your players do, and to understand how players are actually interacting with what you’ve built.

The full source is available on GitHub. Happy running! We’d love to see what you build next — share it with us on Twitter, Bluesky, or Discord. 🦕