tutorial

How to Monitor Effect-TS with Vigilmon

Observable production systems with Effect-TS — structured health checks using Effect's fiber model, uptime monitoring, typed error alerting, and heartbeat supervision for background services.

How to Monitor Effect-TS with Vigilmon

Effect-TS gives you typed errors, structured concurrency, and built-in observability primitives. It's the right choice for complex backend services where "it crashed" is not an acceptable incident report.

But Effect's observability covers your process internals. You still need external monitoring — something outside your process that detects when it stops responding entirely. This tutorial connects your Effect-TS service to Vigilmon for that external layer.

By the end you'll have:

  • A health endpoint that exposes Effect layer health via HTTP
  • HTTP uptime monitoring from Vigilmon
  • Slack alerts on downtime and recovery
  • Heartbeat supervision for Effect background fibers and scheduled services

Why External Monitoring Complements Effect's Built-In Observability

Effect's built-in telemetry (structured logging, tracing via OpenTelemetry, metrics) all run inside your Effect runtime. When they work, they're excellent. When your process is OOM-killed, the JVM equivalent hangs, or your HTTP server crashes before the supervisor can restart it — internal observability goes dark.

External monitoring detects:

  • Process crash — server stops accepting connections
  • Port conflict on restart — process starts but HTTP server fails to bind
  • Frozen fibers — Effect runtime stuck in deadlock, no I/O processed
  • Deployment failure — new container starts but health check fails, traffic routes to zero instances

These don't appear in Effect's internal metrics because the runtime itself isn't running.


Key Metrics to Watch

| Metric | What it means | |--------|---------------| | HTTP health endpoint status | Effect runtime and HTTP server alive | | Fiber error rate | Unhandled defects surfacing to users | | Service layer health | Critical services (DB, queue, cache) reachable | | Background fiber heartbeat | Long-running fibers still executing | | Scheduled service execution | Effect Schedule-based jobs firing on time |


Step 1: Health Endpoint Using Effect Layers

Structure your health check as an Effect service — the same dependency injection model as the rest of your app:

// src/health/HealthService.ts
import { Effect, Layer, Context } from 'effect';

export interface HealthStatus {
  status: 'ok' | 'degraded' | 'error';
  checks: Record<string, 'ok' | 'error'>;
}

export class HealthService extends Context.Tag('HealthService')<
  HealthService,
  { check: Effect.Effect<HealthStatus> }
>() {}

Implement it with checks for your actual dependencies:

// src/health/HealthServiceLive.ts
import { Effect, Layer } from 'effect';
import { HealthService, HealthStatus } from './HealthService';
import { DatabaseService } from '../database/DatabaseService';
import { CacheService } from '../cache/CacheService';

export const HealthServiceLive = Layer.effect(
  HealthService,
  Effect.gen(function* () {
    const db = yield* DatabaseService;
    const cache = yield* CacheService;

    return {
      check: Effect.gen(function* () {
        const checks: Record<string, 'ok' | 'error'> = {};

        // Database check
        const dbResult = yield* Effect.either(db.ping());
        checks.database = dbResult._tag === 'Right' ? 'ok' : 'error';

        // Cache check
        const cacheResult = yield* Effect.either(cache.ping());
        checks.cache = cacheResult._tag === 'Right' ? 'ok' : 'error';

        const allOk = Object.values(checks).every((v) => v === 'ok');
        const status: HealthStatus['status'] = allOk ? 'ok' : 'degraded';

        return { status, checks } satisfies HealthStatus;
      }),
    };
  }),
);

Expose it as an HTTP endpoint (example with Node.js http; adapt for your HTTP layer):

// src/http/healthRoute.ts
import { Effect, Runtime } from 'effect';
import type { IncomingMessage, ServerResponse } from 'http';
import { HealthService } from '../health/HealthService';

export function handleHealth(runtime: Runtime.Runtime<HealthService>) {
  return async (req: IncomingMessage, res: ServerResponse) => {
    const result = await Runtime.runPromiseExit(runtime)(
      Effect.flatMap(HealthService, (s) => s.check),
    );

    if (result._tag === 'Failure') {
      res.writeHead(503, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ status: 'error', error: 'Runtime failure' }));
      return;
    }

    const health = result.value;
    const httpStatus = health.status === 'ok' ? 200 : 503;
    res.writeHead(httpStatus, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(health));
  };
}

Test locally:

curl http://localhost:3000/health
# {"status":"ok","checks":{"database":"ok","cache":"ok"}}

# Simulate a DB failure:
# {"status":"degraded","checks":{"database":"error","cache":"ok"}}  → HTTP 503

Vigilmon reacts to the 503, so the alert fires automatically when any dependency degrades.


Step 2: Set Up HTTP Monitoring in Vigilmon

  1. Sign up at vigilmon.online (free, no card required)
  2. Click New Monitor → HTTP
  3. Enter https://api.yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Expected status code: 200
  6. Save

Add a second monitor for your main API base path so you can distinguish "health check failed" from "entire server unreachable":

https://api.yourdomain.com/health   → Effect layers healthy
https://api.yourdomain.com/         → HTTP server alive

Step 3: Heartbeat Supervision for Background Fibers

Effect's Fiber and Schedule primitives make it easy to run long-lived background processes. The risk: a fiber can be quietly blocked on a semaphore, stuck waiting for a resource that will never be released, or silently error-looped without making progress.

Add a heartbeat ping at the end of each successful job cycle:

// src/jobs/syncJob.ts
import { Effect, Schedule, Duration } from 'effect';
import { HttpClient } from '@effect/platform';

const HEARTBEAT_URL = process.env.VIGILMON_SYNC_HEARTBEAT;

const syncCycle = Effect.gen(function* () {
  // Your sync logic here
  yield* processPendingItems();

  // Ping heartbeat on success
  if (HEARTBEAT_URL) {
    yield* HttpClient.get(HEARTBEAT_URL).pipe(
      Effect.ignoreLogged, // don't fail the job if heartbeat ping fails
    );
  }
});

// Run every 10 minutes, forever
export const syncJobLayer = Effect.repeat(
  syncCycle,
  Schedule.fixed(Duration.minutes(10)),
).pipe(
  Effect.catchAllCause((cause) =>
    Effect.logError('Sync job failed', cause),
    // No heartbeat sent → Vigilmon alerts on missed ping
  ),
  Effect.forkDaemon,
  Layer.effectDiscard,
);

In Vigilmon:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 20 minutes (2× the job period as a buffer)
  3. Copy the ping URL → set as VIGILMON_SYNC_HEARTBEAT in your environment
  4. Save

If the fiber gets stuck, the Effect runtime restarts it (depending on your supervisor config), or it exits — either way the heartbeat stops and you're alerted.


Step 4: Typed Error Alerting via Log Integration

Effect's structured logging can be forwarded to your log aggregator. But for immediate alerts on specific typed errors, use a custom log handler that pings Vigilmon's heartbeat when critical errors exceed a threshold:

// src/telemetry/alertingLogger.ts
import { Logger, Effect, Ref } from 'effect';

const CRITICAL_HEARTBEAT = process.env.VIGILMON_CRITICAL_HEARTBEAT;
let recentErrors = 0;
const ERROR_THRESHOLD = 5;
const WINDOW_MS = 60_000;

// Reset counter every minute
setInterval(() => { recentErrors = 0; }, WINDOW_MS);

export const alertingLogger = Logger.make(({ logLevel, message }) => {
  if (logLevel._tag === 'Fatal' || logLevel._tag === 'Error') {
    recentErrors++;
    if (recentErrors >= ERROR_THRESHOLD && CRITICAL_HEARTBEAT) {
      // Stop pinging the "no critical errors" heartbeat
      // Vigilmon will alert on missed ping after the interval
      console.error(`[ALERT] ${recentErrors} errors in 60s: ${message}`);
    }
  }
});

This pattern inverts the heartbeat: you have a monitor that expects a ping every minute (meaning "everything is fine"). When errors spike, the ping stops and you get alerted.


Step 5: Slack Alerts

Go to Notifications → New Channel → Slack in Vigilmon and paste your incoming webhook URL. Enable it on all your monitors.

When your Effect service goes down:

🔴 DOWN: api.yourdomain.com/health
Status: 503 Service Unavailable
Region: EU-Central
2 minutes ago

On recovery:

✅ RECOVERED: api.yourdomain.com/health
Downtime: 7 minutes

For heartbeat monitors:

🔴 MISSED HEARTBEAT: sync-job
Last ping: 25 minutes ago (expected every 20 min)

Step 6: Status Page

In Vigilmon, go to Status Pages → New Status Page, add your monitors, and publish the URL.

Reference it in your Effect service's error responses:

const notAvailable = Effect.fail({
  _tag: 'ServiceUnavailable' as const,
  message: 'Service temporarily unavailable',
  statusPage: 'https://status.yourdomain.com',
});

What You've Built

| What | How | |------|-----| | Health endpoint | Effect service layer exposing HTTP 200/503 | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Background fiber supervision | Heartbeat ping in Effect.repeat cycle | | Error rate alerting | Inverted heartbeat — stops pinging on error spike | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Effect's internal observability and Vigilmon's external monitoring complement each other: Effect tells you what failed inside your process; Vigilmon tells you when your process stops being reachable at all.


Next Steps

  • Forward Effect's OpenTelemetry traces to your observability stack alongside Vigilmon's uptime data
  • Add separate heartbeat monitors for each critical scheduled Effect service
  • Use Vigilmon's response time history to correlate latency spikes with Effect fiber contention events

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →