DEV Community

Cover image for How to Improve Your App Observability (Easily) with Grafana and OpenTelemetry
bright inventions
bright inventions

Posted on • Originally published at brightinventions.pl

How to Improve Your App Observability (Easily) with Grafana and OpenTelemetry

Have you heard about the Pareto Principle (aka the 80/20 rule)? Briefly, it states that 80% of all outcomes are derived from 20% of causes. In other words, a small amount of work can cause a great effect. I want to show you how to greatly improve observability with just a small amount of effort using Grafana and OpenTelemetry.

What is observability and why is it important?

Application observability is the practice of monitoring and understanding the behavior and performance of applications in production environments. 

Why is observability important?

  • It helps developers gain better visibility into their application's behavior.
  • It enables us to identify issues before they impact users.
  • It assists in quickly diagnosing and resolving problems when they occur.
  • It improves application reliability.
  • It enables faster problem resolution.
  • It allows us to provide a better customer experience.

How can we achieve better observability [tutorial]?

You may think that introducing additional tools to your application would be challenging. Due to the fact that your application is too big, or because it already works in production and you don’t want to introduce too many changes. Another reason to hesitate might be that it would simply be too difficult. But that is not the case!

What app observability tools are we going to use?

In this tutorial, I will show you how to set up basic monitoring for your application using the OpenTelemetry and Grafana's LGTM set of tools. "OpenTelemetry is a collection of tools, APIs, and SDKs that you can use to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) to help you analyze your software’s performance and behavior."

OpenTelemetry provides a standard and vendor-neutral way to instrument, collect, and export telemetry data (metrics, logs, and traces) from applications and infrastructure. It allows developers to easily add observability to their applications without being locked into any specific vendor or technology stack. It is available for most popular languages and frameworks, so it will most likely be available for you too.

Initial Application

Let's take an example application:

import express from "express";

const app = express();

app.get('/hello', (req, res) => {
   res.send('Hello World!');
   console.log("Hello World log!")
});

app.listen(8080, () => {
   console.log('Example app listening on port 8080! Access it: http://localhost:8080/hello');
});
Enter fullscreen mode Exit fullscreen mode

…which logs “Hello World log!” after accessing its endpoint.

Add Logging

First, we need to use a logger that supports multiple transports to apply additional logging configuration. I’m going to use winston, but feel free to use any other logger library.

npm install --save winston
Enter fullscreen mode Exit fullscreen mode

We need to add logger configuration:

import * as winston from "winston";

export const logger = winston.createLogger({
   transports: [
       new winston.transports.Console()
   ],
})
Enter fullscreen mode Exit fullscreen mode

include it in app.ts (in your production application you will probably use dependency injection to use logger in modules) and replace console.log() with logger.info():

import express from "express";
import {logger} from "./logger";

const app = express();

app.get('/hello', (req, res) => {
   res.send('Hello World!');
   logger.info("Hello World log!")
});

app.listen(8080, () => {
   logger.info('Example app listening on port 8080! Access it: http://localhost:8080/hello');
});
Enter fullscreen mode Exit fullscreen mode

And now after accessing our endpoint we receive log in json format:

{"level":"info","message":"Hello World log!"}
Enter fullscreen mode Exit fullscreen mode

That’s better! It’s usually better to use json format for logging since it’s easier for someone unfamiliar with web server logs to understand what the message contains, as each field is labeled.

Add Tracing

I believe it’s safe to assume that if your application is already released to production, you’re not using console.log() for logging and you already had this first step implemented. Now we can take care of tracing. Following OpenTelemetry manual let’s add required OpenTelemetry dependencies:

npm install --save @opentelemetry/sdk-node \
 @opentelemetry/auto-instrumentations-node
Enter fullscreen mode Exit fullscreen mode

Now, we can add example tracing configuration from OpenTelemetry docs. Let’s create tracing.js in main directory:

import {NodeSDK} from '@opentelemetry/sdk-node';
import {ConsoleSpanExporter} from '@opentelemetry/sdk-trace-node';
import {getNodeAutoInstrumentations} from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
   traceExporter: new ConsoleSpanExporter(),
   instrumentations: \[getNodeAutoInstrumentations()]
});

sdk.start()
   .then(() => {
       console.log('Tracing initialized');
   })
   .catch((error) => console.log('Error initializing tracing', error));
Enter fullscreen mode Exit fullscreen mode

We can add new script to our package.json, to run our application with tracing:

"scripts": {
 "start": "tsc && node dist/app.js",
 "start_with_tracing": "tsc && node --require ./dist/tracing.js dist/app.js"
},
Enter fullscreen mode Exit fullscreen mode

Just like that, we enriched our logs with tracing:

{"level":"info","message":"Hello World log!","span_id":"449dfd613318f0dc","trace_flags":"01","trace_id":"f929031d4ad745017a83ed92819ac563"}
Enter fullscreen mode Exit fullscreen mode

and traces started to appear in our console! 

Add Grafana-Agent

Traces logged in the console are not really readable. Let’s send our telemetry data to a monitoring system which provides telemetry data visualisation. There are plenty of monitoring systems available, like DataDog, New Relic, Sentry, etc. 

I will use Grafana Cloud. We need to start with creating a free Grafana Cloud account: https://grafana.com/. After your account is created, on the left sidebar select Connections and Hosted Logs:

Hosted Logs

Go to the Configuration Details tab, provide Choose your use case to be “Send logs from a standalone host”, provide “API Key name” and select “Create API Key''.

Instead of running promtail, we will use grafana-agent, which supports other types of telemetry (metrics, traces) and instead of starting it from a command, we will use docker-compose. You can also consider using Otel Collector, which is not tangled with Grafana. We also don’t want to commit our credentials to the source code repository, so we will store them in the .env file.

To run grafana agent using docker compose, create docker-compose.yml file:

version: "2.1"

services:
 grafana_agent:
   container_name: grafana_agent
   image: grafana/agent:v0.30.1
   ports:
     - "3500:3500"
   volumes:
     - "${PWD}/grafana-agent/agent.yaml:/etc/agent/agent.yaml"
   command: [
     "-config.file=/etc/agent/agent.yaml",
     "-config.expand-env",
   ]
   env_file:
     .env
Enter fullscreen mode Exit fullscreen mode

Create grafana configuration file in {your-project}/grafana-agent/agent.yml:

server:
 log_level: info

logs:
 configs:
   - name: default
     positions:
       filename: /tmp/positions.yaml
     clients:
       - url: ${LOKI_URL}
         basic_auth:
           username: ${LOKI_USERNAME}
           password: ${LOKI_PASSWORD}
     scrape_configs:
       - job_name: http_logs
         loki_push_api:
           server:
             http_listen_port: ${LOGS_HTTP_PORT}
Enter fullscreen mode Exit fullscreen mode

And .env file from template:

LOKI_USERNAME={your-username}
LOKI_PASSWORD={your-password}
LOKI_URL={your-loki-url}

LOGS_HTTP_HOST=localhost
LOGS_HTTP_PORT=3500
Enter fullscreen mode Exit fullscreen mode

Now we can run docker compose up to start grafana-agent.

Configure Loki Transport

Let’s update our app configuration, to provide telemetry data to grafana-agent, instead of standard output (console).
First, install winston loki transport(https://www.npmjs.com/package/winston-loki):

npm install --save winston-loki
Enter fullscreen mode Exit fullscreen mode

and add loki transport to winston configuration in logger.ts:

import * as winston from "winston";
import LokiTransport from "winston-loki";

let winstonTransports = []

winstonTransports.push(new winston.transports.Console())

let httpTransport = new LokiTransport({
   host: `http://${process.env.LOGS_HTTP_HOST}:${process.env.LOGS_HTTP_PORT}`,
   json: true
})
winstonTransports.push(httpTransport)

export const logger = winston.createLogger({
   transports: winstonTransports
})
Enter fullscreen mode Exit fullscreen mode

as you can see, we started to use environment variables from defined in .env file, so we need to import them using Dotenv library:

npm install -- save dotenv
Enter fullscreen mode Exit fullscreen mode

And import dotenv configuration in app.ts:

import 'dotenv/config'

Enter fullscreen mode Exit fullscreen mode

Now, when we restart our app, logs should be transported to grafana-agent(which we run locally) and sent to the Grafana Cloud instance. Let's see them:

Grafana Cloud instance
Looks great!

Configure Trace Exporter

Now we can apply a similar configuration for tracing. First, install opentelemetry trace exporter:

import {NodeSDK} from '@opentelemetry/sdk-node';
import {getNodeAutoInstrumentations} from '@opentelemetry/auto-instrumentations-node';
import {OTLPTraceExporter} from "@opentelemetry/exporter-trace-otlp-grpc";

let traceExporter = new OTLPTraceExporter()

const sdk = new NodeSDK({
   traceExporter: traceExporter,
   instrumentations: [getNodeAutoInstrumentations()],
   serviceName: "my-example-service",
});

sdk.start()
   .then(() => {
       console.log('Tracing initialized');
   })
   .catch((error) => console.log('Error initializing tracing', error));
Enter fullscreen mode Exit fullscreen mode

By default, OTLPTraceExporter sends grpc traces to http://localhost:4317, so let’s configure our grafana-agent, to listen on that port, by adding traces configuration to agent.yaml file:

traces:
 configs:
   - name: default
     receivers:
       otlp:
         protocols:
           grpc:
     remote_write:
       - endpoint: ${TEMPO_ENDPOINT}
         basic_auth:
           username: ${TEMPO_USERNAME}
           password: ${TEMPO_PASSWORD}
Enter fullscreen mode Exit fullscreen mode

and missing envs in .env:

TEMPO_ENDPOINT={your-endpoint}
TEMPO_USERNAME={your-username}
TEMPO_PASSWORD={your-password}
Enter fullscreen mode Exit fullscreen mode

We also need to export this default port in docker-compose.yml:

ports:
 - "3500:3500"
 - "4317:4317"
Enter fullscreen mode Exit fullscreen mode

Now when we restart grafana-agent and example application, our application started to send traces:

Grafana-Agent

Summary

That's it! Now you know how to configure remote logging and tracing for your TypeScript application. Let us know if you like this tutorial and you want to know more, like how to start working with metrics.

You can find all steps in example repository.

Good luck!


By Maciej Nawrocki, Backend Developer @ Bright Inventions.

Top comments (0)