Skip to content

Remixing the OpenTelemetry

Updated: at 04:57 PMSuggest Changes

For the past week I’ve been trying to integrate OpenTelemetry into my Remix app. However, I had trouble finding any working guide to set it up correctly. Maybe it is my lack of general Node and Remix knowledge, but in the case that this stuff is not well known here is a short guide for any wandering soul and for my future self should I need it again. As a sidenote I won’t be going over what is OpenTelemetry or why you need it, as some much smarter people have already written about it and the official site gives a pretty good overview.

The main issue?

The focus will be on enabling server side instrumentation, since client side configuration is rather straightforward (all you need is to add instrumentation hook call in the entry.client.tsx file). To enable OTel instrumentation on Remix Server I will use the opentelemetry-instrumentation-remix package which has necessary hooks to track Remix loaders and actions. While the package docs give instructions on how to add the instrumentation code the problem is where to put it or how to initialize it. Naively at first I would just import it to the entry.server.tsx file hoping it would work (of course it didn’t). The problem is that this code needs to run before the Remix Server is bootstrapped so that hooks are in place to listen for loader/action calls. Otherwise the Remix server code is already in cache so the instrumentation is not working.

Important: this guide doesn’t support deployment with Cloudflare workers due to the limitations of the instrumentation package.

OTel provider

Before we continue we need to setup a server which will collect our traces. If you already have a provider setup you can skip this section. There are many good paid options but here I would like to give shout-out to some good OSS alternatives like:

Both SigNoz and hyperdx are powered by ClickhouseDB, making them a reliable choice for production loads. Openobserve can be used with file backend making it much easier to deploy and experiment with, while also having a HA deployment mode for production. You can use following docker-compose script to quickly deploy Openobserve:

services:
  openobserve:
    container_name: otel
    image: public.ecr.aws/zinclabs/openobserve:latest
    restart: always
    volumes:
      - ./data:/data
    environment:
      ZO_DATA_DIR: /data
      ZO_ROOT_USER_EMAIL: ${LOGS_USER}
      ZO_ROOT_USER_PASSWORD: ${LOGS_PASSWORD}
    ports:
      - "5080:5080"

This deployment path can work quite well on lower end Hetzner VPS, while handling low to mid level loads. As an added bonus Openobserve provides an easy way to add Real User Monitoring (RUM), with session replay to your React apps.

Instrumentation file

First let’s create instrumentation file which will contain necessary code to bootstrap necessary OTel hooks. You can put the script in project root, but generally the location of the file is not crucial as long as you modify the package.json scripts accordingly:

import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { RemixInstrumentation } from "opentelemetry-instrumentation-remix";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { Resource } from "@opentelemetry/resources";
import "dotenv/config";

if (process.env.OTEL_ENABLED == "true") {
  const resource = new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_NAME,
    [ATTR_SERVICE_VERSION]: "2.4.4",
  });

  const otlpTraceExporter = new OTLPTraceExporter({
    url: process.env.OTEL_URL,
    headers: {
      Authorization: `Basic ${process.env.OTEL_TOKEN}`,
    },
  });

  const tracerProvider = new NodeTracerProvider({ resource });

  tracerProvider.addSpanProcessor(new BatchSpanProcessor(otlpTraceExporter));

  tracerProvider.register();

  registerInstrumentations({
    instrumentations: [
      new RemixInstrumentation(),
      new HttpInstrumentation({ disableIncomingRequestInstrumentation: true }),
    ],
  });

  console.log("OpenTelemetry tracing initialized for Remix");
}

Usage of environment variables is entirely optional. It is important that for the OTLP exporter correct URL and authorization methods are specified. If you are using local Openobserve server from previous section url should be http://localhost:5080, while the token can be found on Web interface under Data Sources section.

Besides RemixInstrumentation you can also add full Node instrumentation. Here I am choosing only to instrument outbound HTTP calls using HttpInstrumentation, so that I can connect my frontend traces with backend service operations.

Startup scripts

Next crucial step is modifying package.json scripts so that OTel instrumentation is added when you start your app. Since I still wanted to rely on Remix provided scripts for dev and production I found that the best way to do this is to edit NODE_OPTIONS. As the word suggest NODE_OPTIONS is environment variable meant to inject additional Node configuration when standard node command is not available. The most important thing is that environment is correctly set (if you are using Windows CMD or fish shell below example will work correctly, but for bash/zsh you can ommit set keyword). Here is my example of modified scripts section of package.json:

{
  "scripts": {
    "build": "remix vite:build",
    "dev": "set NODE_OPTIONS='--import ./instrumentation.mjs' && remix vite:dev",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "set NODE_OPTIONS='--import ./instrumentation.mjs' && remix-serve ./build/server/index.js",
    "typecheck": "tsc"
  }
}

The most important thing is to modify instrumentation file path relative to the package.json file.

Conclusion

Integrating OpenTelemetry into my Remix app turned out to be more challenging than I expected, mostly due to the lack of clear guides on how to set it up properly. I hope this guide saves you some time and helps you get up and running with OTel quickly. Happy tracing!