Checkly: API และ Browser Monitoring as Code ด้วย Playwright

เมื่อทีม DevOps ต้องการระบบ monitoring ที่ปฏิบัติกับ check เหมือนกับโค้ด — review ผ่าน Pull Request, ทดสอบใน CI, deploy ผ่าน pipeline — เครื่องมือแบบคลิก UI สมัยเดิมไม่ตอบโจทย์อีกต่อไป Checkly เป็นแพลตฟอร์ม Monitoring as Code ที่ใช้ Playwright scripting ช่วยให้ API check และ browser check ถูกเขียนเป็น TypeScript, จัดเก็บใน Git repository และ deploy อัตโนมัติพร้อมกับแอปพลิเคชัน

บทความนี้จะอธิบายแนวคิด Monitoring as Code การติดตั้ง Checkly CLI การเขียน API check และ browser check ตัวอย่าง script สำหรับ e-commerce checkout flow การตั้งค่า alert channel, retry, และ schedule รวมถึงวิธีรวม Checkly เข้ากับ CI/CD pipeline เช่น GitHub Actions และ GitLab CI

Checkly คืออะไร

Checkly คือแพลตฟอร์ม synthetic monitoring และ API observability ที่ออกแบบมาเพื่อทีม DevOps และ SRE สมัยใหม่ จุดเด่นคือใช้ Playwright (framework browser automation ของ Microsoft) เป็น engine หลัก ทำให้สามารถเขียน check ด้วย TypeScript/JavaScript ที่ทีมคุ้นเคยอยู่แล้ว ไม่ต้องเรียนรู้ DSL ใหม่

นอกจาก browser check, Checkly ยังรองรับ API check, Multistep check (เรียก API หลายตัวต่อเนื่อง), Heartbeat monitor, Private Location (รัน check บน infrastructure ของตัวเอง) และมี Traces integration ที่เชื่อมต่อกับ OpenTelemetry เพื่อให้ได้ full-stack observability

แนวคิด Monitoring as Code

Monitoring as Code หมายถึงการจัดการ monitoring config ทั้งหมดเป็นโค้ดใน repository เดียวกับแอปพลิเคชัน แทนที่จะคลิกใน dashboard UI หลักการนี้ให้ประโยชน์หลายข้อ

  • Version control: ทุกการเปลี่ยน check ถูก track ใน Git — ย้อนกลับได้ตลอด
  • Code Review: การเพิ่ม/แก้ check ต้องผ่าน Pull Request ลดความผิดพลาดจากการคลิก UI ผิด
  • Consistency: ทุก check ถูกสร้างด้วยรูปแบบเดียวกัน — parameter, alert channel, retry policy
  • Testing: รัน check ใน CI ก่อน deploy เพื่อจับปัญหาจาก local
  • Environment Parity: ใช้ check ชุดเดียวกันกับ staging และ production เพียงเปลี่ยน env var
  • Ownership ชัดเจน: CODEOWNERS จัดการ check ของแต่ละทีมใน repo

การติดตั้ง Checkly CLI

Checkly CLI เป็น Node.js package ที่ใช้จัดการ resource ทั้งหมดบน Checkly การติดตั้งเริ่มต้นใน project ใหม่

# สร้าง project ใหม่
npm create @checkly/cli@latest my-monitoring
cd my-monitoring

# หรือเพิ่มเข้า project ที่มีอยู่
npm install --save-dev checkly @checkly/cli

# ตั้งค่า API key จาก checklyhq.com
npx checkly login

# ทดสอบ config
npx checkly test

# Deploy check ขึ้น Checkly
npx checkly deploy --preview
npx checkly deploy

หลังจาก login แล้ว CLI จะเก็บ API key ไว้ใน ~/.checkly/config.yml สำหรับ CI/CD แนะนำให้ใช้ service account + environment variable แทน

โครงสร้าง Project

my-monitoring/
├── checkly.config.ts           # config หลัก (account, baseUrl, location)
├── package.json
├── tsconfig.json
└── __checks__/
    ├── api.check.ts            # API checks
    ├── browser.check.ts        # Browser check construct
    ├── homepage.spec.ts        # Playwright test (browser check)
    └── checkout-flow.spec.ts   # Multi-step browser test

ไฟล์ checkly.config.ts เป็น entry point หลัก กำหนด account-wide setting และ include pattern สำหรับการค้นหา check

// checkly.config.ts
import { defineConfig } from 'checkly';

export default defineConfig({
  projectName: 'DE Knowledge Base Monitoring',
  logicalId: 'de-kb-monitoring',
  repoUrl: 'https://github.com/example/monitoring',
  checks: {
    frequency: 10,
    locations: ['ap-southeast-1', 'ap-northeast-1', 'eu-central-1'],
    tags: ['production'],
    runtimeId: '2024.02',
    checkMatch: '**/__checks__/**/*.check.ts',
    browserChecks: {
      frequency: 5,
      testMatch: '**/__checks__/**/*.spec.ts'
    }
  },
  cli: {
    runLocation: 'ap-southeast-1'
  }
});

ตัวอย่าง API Check

API check เป็น check ที่ง่ายที่สุด — ส่ง HTTP request แล้ว assert response

// __checks__/api.check.ts
import { ApiCheck, AssertionBuilder } from 'checkly/constructs';

new ApiCheck('products-list', {
  name: 'GET /v1/products returns list',
  frequency: 5,
  locations: ['ap-southeast-1', 'ap-northeast-1'],
  degradedResponseTime: 800,
  maxResponseTime: 2000,
  request: {
    url: 'https://api.example.com/v1/products?limit=10',
    method: 'GET',
    headers: [
      { key: 'Authorization', value: 'Bearer {{API_TOKEN}}' }
    ],
    assertions: [
      AssertionBuilder.statusCode().equals(200),
      AssertionBuilder.jsonBody('$.data').isNotNull(),
      AssertionBuilder.jsonBody('$.data.length').greaterThan(0),
      AssertionBuilder.responseTime().lessThan(1500)
    ]
  },
  alertChannels: []
});

ตัวอย่างนี้ใช้ AssertionBuilder ช่วยสร้าง assertion แบบ type-safe — IDE autocomplete ได้ และ compile-time error เมื่อเขียนผิด ตัวแปร {{API_TOKEN}} จะถูกแทนที่ด้วย environment variable ตอนรัน

ตัวอย่าง Browser Check

Browser check ประกอบด้วยสองส่วน — construct file ที่กำหนด config และ spec file ที่เขียน Playwright test

// __checks__/browser.check.ts
import { BrowserCheck, Frequency } from 'checkly/constructs';
import * as path from 'path';

new BrowserCheck('homepage-load', {
  name: 'Homepage loads with key elements',
  frequency: Frequency.EVERY_5M,
  locations: ['ap-southeast-1', 'ap-northeast-1', 'eu-central-1'],
  code: {
    entrypoint: path.join(__dirname, 'homepage.spec.ts')
  },
  retryStrategy: {
    type: 'LINEAR',
    baseBackoffSeconds: 30,
    maxRetries: 2
  }
});

new BrowserCheck('checkout-flow', {
  name: 'Checkout flow critical path',
  frequency: Frequency.EVERY_10M,
  locations: ['ap-southeast-1', 'ap-northeast-1'],
  code: {
    entrypoint: path.join(__dirname, 'checkout-flow.spec.ts')
  }
});
// __checks__/homepage.spec.ts
import { test, expect } from '@playwright/test';

test('homepage shows navigation and hero', async ({ page }) => {
  const response = await page.goto(process.env.ENVIRONMENT_URL || 'https://example.com');

  expect(response?.status()).toBeLessThan(400);
  await expect(page).toHaveTitle(/Example/);
  await expect(page.locator('nav')).toBeVisible();
  await expect(page.locator('[data-testid="hero-cta"]')).toBeVisible();

  const perf = await page.evaluate(() => {
    const entry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    return {
      ttfb: entry.responseStart - entry.requestStart,
      fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime
    };
  });

  console.log('Performance:', perf);
  expect(perf.ttfb).toBeLessThan(800);
});

Multi-Step Check: E-Commerce Checkout

Multi-step check คือ browser check ที่จำลองการใช้งานเต็มรูปแบบจากการค้นหาสินค้าจนถึงการชำระเงิน ตัวอย่างด้านล่างเป็น checkout flow ของเว็บไซต์ e-commerce ที่ใช้ Stripe test mode

// __checks__/checkout-flow.spec.ts
import { test, expect } from '@playwright/test';

test.describe.configure({ mode: 'serial' });

test('complete checkout flow', async ({ page }) => {
  const baseUrl = process.env.ENVIRONMENT_URL || 'https://shop.example.com';

  // 1. เข้าหน้าสินค้า
  await test.step('browse product', async () => {
    await page.goto(`${baseUrl}/products/test-item`);
    await expect(page.locator('[data-testid="product-title"]')).toBeVisible();
  });

  // 2. เพิ่มสินค้าเข้าตะกร้า
  await test.step('add to cart', async () => {
    await page.click('[data-testid="add-to-cart"]');
    await expect(page.locator('[data-testid="cart-badge"]')).toHaveText('1');
  });

  // 3. ไปหน้า checkout
  await test.step('go to checkout', async () => {
    await page.click('[data-testid="cart-icon"]');
    await page.click('[data-testid="checkout-button"]');
    await page.waitForURL('**/checkout');
  });

  // 4. กรอกข้อมูล shipping
  await test.step('fill shipping info', async () => {
    await page.fill('input[name="email"]', '[email protected]');
    await page.fill('input[name="name"]', 'Synthetic User');
    await page.fill('input[name="address"]', '123 Test Street');
  });

  // 5. กรอกข้อมูลบัตร (Stripe test card)
  await test.step('fill payment info', async () => {
    const stripeFrame = page.frameLocator('iframe[name^="__privateStripeFrame"]').first();
    await stripeFrame.locator('[name="cardnumber"]').fill('4242424242424242');
    await stripeFrame.locator('[name="exp-date"]').fill('12/30');
    await stripeFrame.locator('[name="cvc"]').fill('123');
  });

  // 6. ยืนยันคำสั่งซื้อ
  await test.step('confirm order', async () => {
    await page.click('[data-testid="place-order"]');
    await page.waitForURL('**/order/success', { timeout: 15000 });
    await expect(page.locator('[data-testid="order-id"]')).toBeVisible();
  });
});

การใช้ test.step() ช่วยให้ Checkly แสดง timing แยกตามแต่ละขั้น ทำให้เวลาเกิด fail สามารถเห็นได้ทันทีว่าขั้นไหนช้าหรือพัง — เป็นข้อได้เปรียบสำคัญเหนือ check ที่วัดแค่ total duration

Alert Channel และการแจ้งเตือน

Checkly รองรับ alert channel หลายรูปแบบ — Email, Slack, PagerDuty, Opsgenie, SMS, Webhook การสร้าง alert channel ก็ทำเป็น code ได้

// __checks__/alert-channels.ts
import { SlackAlertChannel, EmailAlertChannel } from 'checkly/constructs';

export const slackSre = new SlackAlertChannel('slack-sre', {
  name: 'SRE Slack',
  url: process.env.SLACK_WEBHOOK_URL!,
  channel: '#sre-alerts',
  sendRecovery: true,
  sendFailure: true,
  sendDegraded: false,
  sslExpiry: true,
  sslExpiryThreshold: 30
});

export const emailOncall = new EmailAlertChannel('email-oncall', {
  name: 'Oncall Email',
  address: '[email protected]',
  sendRecovery: true,
  sendFailure: true
});

แล้วในไฟล์ check ก็อ้างอิง alert channel มาใช้

import { slackSre, emailOncall } from './alert-channels';

new BrowserCheck('critical-checkout', {
  name: 'Checkout must not fail',
  alertChannels: [slackSre, emailOncall],
  alertSettings: {
    escalationType: 'RUN_BASED',
    runBasedEscalation: { failedRunThreshold: 1 },
    reminders: { amount: 2, interval: 10 }
  },
  // ...
});

Environment Variable และ Secret

Checkly รองรับ environment variable ทั้งในระดับ global (account) และ check-specific โดยมี locked flag สำหรับเก็บ secret ที่ไม่แสดงใน UI

// ใน checkly.config.ts
export default defineConfig({
  checks: {
    environmentVariables: [
      { key: 'API_BASE_URL', value: 'https://api.example.com' },
      { key: 'API_TOKEN', value: process.env.API_TOKEN!, locked: true }
    ]
  }
});

ตอน deploy ผ่าน CI ให้ set CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID และ secret อื่น ๆ ใน secret manager ของ CI (GitHub Secrets, GitLab CI variables)

การรวมเข้ากับ CI/CD

Checkly CLI รองรับ deploy ผ่าน CI เพื่อให้ check ถูก sync กับ repository อัตโนมัติเมื่อ merge เข้า main

# .github/workflows/checkly-deploy.yml
name: Deploy Checkly
on:
  push:
    branches: [main]
    paths: ['__checks__/**', 'checkly.config.ts']

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - name: Test checks locally
        run: npx checkly test --reporter github
        env:
          CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
          CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
      - name: Deploy to Checkly
        run: npx checkly deploy --force
        env:
          CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
          CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}

ขั้นตอน checkly test จะรัน check บน ephemeral runner ก่อน deploy — ถ้าผ่านถึงจะ deploy จริง — ทำให้จับ bug ก่อนที่จะส่งผลกระทบต่อ alerting

Private Location

กรณี API อยู่หลัง VPN หรือ internal network ที่ public Checkly location ไม่สามารถเข้าถึงได้ สามารถรัน Private Location agent บน infrastructure ของตัวเอง (เช่น Kubernetes cluster หรือ VM) agent จะ pull งานจาก Checkly แล้ว execute check ใน network ส่วนตัว ผลลัพธ์ส่งกลับ Checkly เพื่อแสดงใน dashboard เดียวกัน

# รัน Private Location agent ด้วย Docker
docker run -d --name checkly-agent \
  -e API_KEY=pls_xxxx \
  -e LOCATION_ID=private-datacenter-th \
  --restart unless-stopped \
  checkly/agent:latest

Trace และ OpenTelemetry Integration

Checkly Traces ช่วยเชื่อม synthetic check กับ trace จริงใน backend ผ่าน OpenTelemetry เมื่อ check fail ทีมสามารถ drill-down ไปดู trace ของ request นั้นใน APM (เช่น Grafana Tempo, Jaeger, Datadog APM) เพื่อหา root cause ได้ทันที ไม่ต้องคลำหา request ID เอง

การเปิด feature นี้ทำได้ใน Playwright test ด้วย

test('api call with trace context', async ({ request }) => {
  const response = await request.get('https://api.example.com/v1/orders', {
    headers: {
      'traceparent': `00-${process.env.CHECKLY_TRACE_ID}-${process.env.CHECKLY_SPAN_ID}-01`
    }
  });
  expect(response.status()).toBe(200);
});

Best Practices

  • แบ่ง check ตาม criticality: critical check ตั้งทุก 1-5 นาที + alert เข้า PagerDuty — non-critical ตั้งทุก 10-15 นาที + alert เข้า Slack
  • ใช้ data-testid: แทน CSS selector ที่เปลี่ยนง่าย ทำให้ check ทนต่อการ refactor frontend
  • แยก spec file: 1 spec = 1 user journey — อย่ารวมหลาย journey ไว้ด้วยกัน
  • Group ด้วย tag: ใช้ tag เช่น critical, checkout, api-v2 เพื่อ filter และ manage bulk
  • Version runtime: กำหนด runtimeId ชัดเจน ไม่ใช้ latest เพื่อไม่ให้ check แตกเมื่อ Checkly update Playwright version
  • Preview deployment: ใช้ checkly deploy --preview ก่อน deploy จริง เพื่อดู diff
  • Mock external dependency: ถ้า check เรียก 3rd party API ที่ไม่น่าเชื่อถือ ให้ mock ด้วย Mockoon หรือ WireMock

Pitfalls ที่ควรหลีกเลี่ยง

  • Flaky selector: ใช้ selector ที่เปลี่ยนตาม build hash — แก้ด้วย data-testid
  • Hard-coded wait: ใช้ page.waitForTimeout(5000) — ควรใช้ waitForURL, waitForResponse แทน
  • Test data pollution: synthetic สร้าง order จริงใน production — ต้องมี flag พิเศษบน account synthetic
  • Over-alerting: ทุก check alert เข้า PagerDuty — ทีมเบื่อและเพิกเฉย ควรจำแนก criticality
  • ไม่ทดสอบ check ใน CI: deploy ทันทีโดยไม่ test แล้ว check พัง alert ทั้ง production

สรุป

Checkly ทำให้ synthetic monitoring กลายเป็นส่วนหนึ่งของ development workflow ปกติ ทีมเขียน check ด้วย TypeScript/Playwright ที่คุ้นเคย review ผ่าน Pull Request, test ใน CI และ deploy อัตโนมัติเมื่อ merge เข้า main แนวทางนี้เหมาะกับองค์กรที่ให้ความสำคัญกับ infrastructure-as-code และต้องการ consistency สูงในการจัดการ monitoring

เริ่มต้นจาก API check ง่าย ๆ แล้วขยายไปสู่ browser check และ multi-step check สำหรับ critical user journey ใช้ Private Location เมื่อต้อง monitor service หลัง VPN และเชื่อมต่อกับ OpenTelemetry เพื่อ trace ลงไปถึง backend ได้เมื่อเกิดปัญหา