Back to blog
monitoringobservabilitydevopssaasloggingmetricstracing

Monitoring & Observability for Your SaaS: From Blind Spots to Full Visibility

By SaaS Masters9 maart 20267 min read
Monitoring & Observability for Your SaaS: From Blind Spots to Full Visibility

Your SaaS is running, customers are flowing in, and everything seems fine — until you wake up on Monday morning to an inbox full of complaints. "The app is slow." "I can't log in." "My data is gone." What went wrong? When did it start? No idea, because you have no monitoring.

This scenario is all too familiar for SaaS founders. In this article, we'll dive deep into monitoring and observability: what it is, why it's crucial, and how to implement it practically — without needing a full DevOps team.

Monitoring vs. Observability: The Difference

Monitoring tells you that something is wrong. You set up alerts for known problems: CPU above 90%, response time above 2 seconds, error rate above 1%.

Observability tells you why something is wrong. It gives you the tools to investigate unknown problems by understanding your system from the inside.

The difference is crucial. Monitoring is reactive — you check for what you expect. Observability is proactive — you can ask questions you hadn't thought of beforehand.

The Three Pillars of Observability

1. Logs: Your Application's Story

Logs are the most basic form of insight. But not all logs are created equal.

Bad:

Error occurred
Something went wrong

Good:

{
  "timestamp": "2026-03-09T07:15:23Z",
  "level": "error",
  "service": "payment-service",
  "traceId": "abc-123-def",
  "userId": "usr_5k2j1",
  "tenantId": "tenant_acme",
  "message": "Stripe webhook processing failed",
  "error": "Card declined",
  "stripeEventId": "evt_1234",
  "duration_ms": 342
}

Best practices for logging in SaaS:

  • Structured logging (JSON) — make logs machine-readable
  • Always add context: userId, tenantId, requestId, traceId
  • Log at the right level: DEBUG for development, INFO for flow, WARN for recoverable issues, ERROR for real problems
  • Avoid PII (Personally Identifiable Information) in logs — GDPR!
  • Centralize your logs — don't SSH into 5 servers to investigate
// Example: structured logger with Pino (Node.js)
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  redact: ['req.headers.authorization', 'user.email'],
});

// Use with context
export function createRequestLogger(req: Request) {
  return logger.child({
    requestId: req.headers['x-request-id'],
    tenantId: req.tenantId,
    userId: req.userId,
  });
}

2. Metrics: Your System's Heartbeat

Metrics are numerical values over time. They tell you how your system performs at a macro level.

The four golden signals (Google SRE):

  1. Latency — How long do requests take? (p50, p95, p99)
  2. Traffic — How many requests per second?
  3. Errors — What percentage of requests fail?
  4. Saturation — How full are your resources?

Business metrics you should also track:

  • Signup conversion per step
  • Time-to-first-value (how quickly does a user achieve their first success?)
  • Feature adoption rates
  • API usage per tenant (for fair-use and upselling)
// Example: custom metrics with Prometheus client
import { Counter, Histogram, register } from 'prom-client';

const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code', 'tenant'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});

const businessEvents = new Counter({
  name: 'business_events_total',
  help: 'Business event counter',
  labelNames: ['event', 'tenant', 'plan'],
});

// In your middleware
app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  res.on('finish', () => {
    end({
      method: req.method,
      route: req.route?.path || 'unknown',
      status_code: res.statusCode,
      tenant: req.tenantId,
    });
  });
  next();
});

3. Traces: Follow a Request Through Your Entire System

In a microservices architecture (or even a monolith with external services), a single user action often passes through multiple systems: API → database → cache → external API → queue → worker.

Distributed tracing lets you follow this entire path.

// Example: OpenTelemetry setup (the open standard)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_ENDPOINT,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
      '@opentelemetry/instrumentation-redis': { enabled: true },
    }),
  ],
});

sdk.start();

With OpenTelemetry, you automatically get traces for HTTP requests, database queries, and Redis calls. You see exactly where time is being lost.

The Practical Stack: What Should You Use?

You don't need a €50,000/year Datadog contract. There are excellent options for every stage:

Bootstrap / Early Stage (€0-50/month)

  • Logs: Axiom (free tier), Logtail, or Grafana Cloud
  • Uptime: BetterStack, UptimeRobot
  • Error tracking: Sentry (free for small teams)
  • Metrics: Grafana Cloud free tier

Growth Stage (€100-500/month)

  • All-in-one: Grafana Cloud, Datadog Essentials
  • APM: New Relic (generous free tier), Elastic APM
  • Tracing: Jaeger (self-hosted) or Grafana Tempo

Scale Stage

  • Full observability: Datadog, Grafana Enterprise, Splunk
  • Custom dashboards per tenant for enterprise customers

Alerts: The Art of Not Being Annoying

The biggest mistake with monitoring? Too many alerts. Alert fatigue is real — when everything is urgent, nothing is urgent.

Good alert strategy:

# Example: alert levels
critical: # Wake me up (PagerDuty/Opsgenie)
  - Error rate > 5% for 5 minutes
  - No successful health checks for 2 minutes
  - Database connection pool exhausted
  - Payment processing failing

warning: # Slack notification
  - Response time p95 > 2s for 10 minutes
  - Disk usage > 80%
  - Memory usage > 85%
  - Queue backlog > 1000 items

info: # Dashboard only
  - Deployment completed
  - New tenant signed up
  - Daily report

SLOs (Service Level Objectives) as your foundation:

Define what "good enough" means before setting up alerts:

  • 99.9% uptime = max 43 minutes downtime per month
  • p95 latency < 500ms
  • Error rate < 0.1%

Multi-Tenant Monitoring: The SaaS-Specific Challenge

In a multi-tenant SaaS, you need to monitor not just your system as a whole, but also per tenant. One "noisy neighbor" can impact your entire platform.

// Middleware: track per-tenant resource usage
app.use(async (req, res, next) => {
  const tenantId = req.tenantId;
  const start = performance.now();

  res.on('finish', () => {
    const duration = performance.now() - start;

    // Track per tenant
    metrics.tenantRequestDuration
      .labels(tenantId)
      .observe(duration / 1000);

    // Detect noisy neighbors
    if (duration > SLOW_REQUEST_THRESHOLD) {
      logger.warn({
        tenantId,
        duration,
        route: req.route?.path,
        message: 'Slow request detected — possible noisy neighbor',
      });
    }
  });

  next();
});

What you want to know per tenant:

  • Request volume and patterns
  • Error rates
  • Storage and bandwidth usage
  • API quota consumption
  • Cost per tenant (for your own margin calculations)

Health Checks: More Than a Ping

A good health check doesn't just verify your app is running — it verifies it's functional.

// Comprehensive health check endpoint
app.get('/health', async (req, res) => {
  const checks = {
    database: await checkDatabase(),
    redis: await checkRedis(),
    stripe: await checkStripeAPI(),
    storage: await checkS3(),
    queue: await checkQueueConnection(),
  };

  const healthy = Object.values(checks).every(c => c.status === 'ok');

  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION,
    checks,
  });
});

async function checkDatabase() {
  try {
    const start = Date.now();
    await prisma.$queryRaw\`SELECT 1\`;
    return { status: 'ok', latency_ms: Date.now() - start };
  } catch (error) {
    return { status: 'error', message: error.message };
  }
}

Step-by-Step Plan: From Zero to Observability

Week 1: The Basics

  • Implement structured logging (Pino/Winston)
  • Add request IDs to all logs
  • Set up Sentry for error tracking
  • Configure an uptime monitor

Week 2: Metrics

  • Add the four golden signals
  • Create a basic dashboard (Grafana)
  • Set up 3-5 critical alerts
  • Start per-tenant tracking

Week 3: Deep Dive

  • Implement OpenTelemetry for tracing
  • Add business metrics
  • Build a health check endpoint
  • Define your SLOs

Week 4: Culture

  • Document your runbooks (what do you do when alert X fires?)
  • Train your team on the dashboards
  • Schedule a monthly review of your alerts
  • Remove alerts that nobody takes action on

Conclusion

Monitoring and observability aren't a luxury — they're a requirement for any serious SaaS. The difference between a SaaS that scales and one that crashes under pressure often isn't in the code, but in how much you see of what's happening.

Start small: structured logging, error tracking, and uptime monitoring. Build from there toward full observability. Your future self — the one debugging a production incident at 3 AM — will thank you.

The cost of good monitoring is always lower than the cost of an outage you didn't see coming.