Episode 24 builds production observability: structured logging with Pino, Apollo Studio setup for query analytics and schema checks, OpenTelemetry integration for distributed tracing, Prometheus metrics and Grafana dashboards, APM tools like Sentry, and health checks.

An API that can't be observed is like walking in the dark. Episode 24 builds observability for production GraphQL: logging, tracing, metrics, and health checks that let you see what's happening inside the server from the outside. We'll cover structured logging with Pino, query analytics with Apollo Studio, distributed tracing with OpenTelemetry, metrics with Prometheus and Grafana, APM for error tracking, and health checks.
Logs in production must be structured — JSON with consistent fields — so they can be filtered and aggregated. Pino is a popular choice because it's fast; install it with npm install pino:
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
});Integrate the logger into the context so every resolver uses the same logger — create a child logger per request with logger.child({ requestId: crypto.randomUUID() }):
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
logger: logger.child({ requestId: crypto.randomUUID() }),
user,
}),
});import { ApolloServerPlugin } from "@apollo/server";
const logPlugin = {
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
contextValue.logger.info({ body: response.body }, "response dikirim");
},
};
},
};Apollo Studio is Apollo's observability platform that shows GraphQL operations live. After registering your graph and filling in the key:
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginUsageReporting({ sendErrors: { unmodified: true } })],
});In a microservices architecture, a query passes through many services. OpenTelemetry standardizes tracing so one request's journey can be tracked across services; install it with npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node:
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const sdk = new NodeSDK({
traceExporter: { url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT },
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();The core concept: every request carries a trace id, is split into spans (one per resolver or per database query), and the context flows through propagation so the receiving service stays within the same trace. This makes inter-service bottlenecks clearly visible.
For metrics, expose Prometheus metrics from the server; install with npm install prom-client:
import { Counter, Histogram } from "prom-client";
const queriesTotal = new Counter({
name: "graphql_queries_total",
help: "Jumlah query",
labelNames: ["operationName"],
});
const queryDuration = new Histogram({
name: "graphql_query_duration_seconds",
help: "Durasi query",
labelNames: ["operationName"],
});Prometheus pulls metrics from the /metrics endpoint, and Grafana displays them as dashboards. Metrics you must have: total queries per operation, duration histograms, error counts, and database health.
For error tracking, integrate Sentry; install with npm install @sentry/node:
import * as Sentry from "@sentry/node";
Sentry.init({ dsn: process.env.SENTRY_DSN });
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
async requestDidStart() {
return {
async didEncounterErrors({ errors }) {
errors.forEach(Sentry.captureException);
},
};
},
},
],
});Sentry shows errors with their stack traces, users, and request context — replacing the habit of "checking logs on the server". Alternatives: Datadog and New Relic for integrated APM.
Health checks tell the orchestrator (Kubernetes, load balancer) whether the application is healthy:
app.get("/health/live", (req, res) => {
res.json({ status: "ok" });
});
app.get("/health/ready", async (req, res) => {
try {
await prisma.$queryRaw`SELECT 1`;
res.json({ status: "ok" });
} catch {
res.status(503).json({ status: "degraded" });
}
});The readiness check tests dependencies (database, Redis) so the load balancer knows when the app is actually ready. This is required before automated deployment — we'll use it again in episodes 31 and 35.
Key takeaways:
requestId makes tracking easy.In the next episode, episode 25, you'll learn about Apollo Client fundamentals — setting up the client and ApolloProvider, the useQuery hook with polling and refetch, useMutation with optimistic UI, InMemoryCache management, and local state with reactive variables. The frontend will be fully connected to GraphQL!