Watchlog DocsWatchlog Docs
Home
Get Started
Gen AI Monitoring
Integrations
Log Watchlist
Home
Get Started
Gen AI Monitoring
Integrations
Log Watchlist
  • Watchlog
  • Get Started
  • Custom Events
  • APM
  • Real User Monitoring (RUM)
  • Kubernetes Cluster Monitoring
  • Generative AI Monitoring
  • AI Traces Client Libraries Documentation
  • Browser Synthetic Tests

Application Performance Monitoring (APM)

Access the APM dashboard at https://app.watchlog.io/apm to view your applications and performance metrics.

Overview

Watchlog APM uses OpenTelemetry for instrumentation. You don't need to install any Watchlog-specific packages. Simply:

  1. Install OpenTelemetry SDK for your platform (Node.js, Python, Go, Java, .NET, Ruby, PHP, etc.)
  2. Configure OTLP exporter to send traces to your Watchlog agent
  3. Set your application name - this will appear in the Watchlog dashboard

Format Support

Watchlog agent supports JSON format for OTLP traces. Most OpenTelemetry SDKs (Node.js, Go, Java, .NET) send JSON by default and work out of the box. However, Python SDK sends Protobuf by default, so you'll need to use a custom JSON exporter for Python (see Python section below).

Agent URL Format

The trace endpoint follows this format:

http://<agent-host>:3774/apm/<your-app-name>/v1/traces

Examples:

  • Local: http://localhost:3774/apm/my-service/v1/traces
  • Docker: http://watchlog-agent:3774/apm/my-service/v1/traces

Prerequisites

Before configuring APM, make sure you have:

  1. Watchlog Agent installed and running (see Host Map for installation)
  2. Agent accessible at http://localhost:3774 (local) or http://watchlog-agent:3774 (Docker)
  3. Your application name ready (this will be used to identify your service in Watchlog)

Node.js Icon Node.js

Installation

Install OpenTelemetry packages:

npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http

Basic Usage

Create an otel-config.js file (must be loaded first, before your application code):

// otel-config.js — must be first
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

// Your application name
const APP_NAME = process.env.APP_NAME || 'my-service';
// Watchlog agent URL (local: http://localhost:3774 or Docker: http://watchlog-agent:3774)
const WATCHLOG_URL = process.env.WATCHLOG_URL || 'http://localhost:3774';

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: APP_NAME,
    [SEMRESATTRS_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    url: `${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces`,
    headers: {},
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // Disable unnecessary instrumentations to reduce overhead
      '@opentelemetry/instrumentation-fs': {
        enabled: false,
      },
    }),
  ],
});

sdk.start();

console.log(`OpenTelemetry initialized for app: ${APP_NAME}`);
console.log(`Sending traces to: ${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces`);

// Graceful shutdown
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry terminated'))
    .catch((error) => console.log('Error terminating OpenTelemetry', error))
    .finally(() => process.exit(0));
});

Then load it before your application:

// index.js
// Load OpenTelemetry first
import './otel-config.js';

// Continue loading your application
import express from 'express';
const app = express();

app.get('/', (req, res) => res.send('Hello World!'));
app.listen(3000, () => console.log('Listening on 3000'));

Docker Setup

When running your Node.js app in Docker, set the WATCHLOG_URL environment variable to point to your Watchlog Agent container:

// otel-config.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const APP_NAME = process.env.APP_NAME || 'my-service';
// For Docker: use container name 'watchlog-agent'
// For local: use 'localhost'
const WATCHLOG_URL = process.env.WATCHLOG_URL || 'http://watchlog-agent:3774';

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: APP_NAME,
  }),
  traceExporter: new OTLPTraceExporter({
    url: `${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces`,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Docker Compose Example:

version: '3.8'

services:
  watchlog-agent:
    image: watchlog/agent:latest
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  node-app:
    build: .
    container_name: node-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - APP_NAME=my-service
      - WATCHLOG_URL=http://watchlog-agent:3774  # Use container name
    depends_on:
      - watchlog-agent
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Docker Run Example:

# 1. Create network
docker network create app-network

# 2. Run Watchlog Agent
docker run -d \
  --name watchlog-agent \
  --network app-network \
  -p 3774:3774 \
  -e WATCHLOG_APIKEY="your-api-key" \
  -e WATCHLOG_SERVER="https://log.watchlog.ir" \
  watchlog/agent:latest

# 3. Run Node.js app with environment variables
docker run -d \
  --name node-app \
  --network app-network \
  -p 3000:3000 \
  -e APP_NAME=my-service \
  -e WATCHLOG_URL=http://watchlog-agent:3774 \
  my-node-app

Important Notes:

  • When using Docker, use the container name as the hostname (e.g., watchlog-agent)
  • Both containers must be on the same Docker network
  • The agent must be running before your app starts
  • Set the WATCHLOG_URL environment variable to http://watchlog-agent:3774 for Docker
  • Set the APP_NAME environment variable to your application name
  • The trace endpoint format is: ${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces

Python Icon Python

Watchlog APM supports Python applications using OpenTelemetry.

⚠️ Important: The standard OpenTelemetry Python SDK uses Protobuf format by default, but Watchlog agent requires JSON format. Therefore, you need to use a custom JSON exporter for Python applications (see below).

Installation

Install OpenTelemetry packages:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation

For specific frameworks, install the corresponding instrumentation packages:

  • Flask: opentelemetry-instrumentation-flask
  • Django: opentelemetry-instrumentation-django
  • FastAPI: opentelemetry-instrumentation-fastapi
  • PostgreSQL: opentelemetry-instrumentation-psycopg2
  • Requests: opentelemetry-instrumentation-requests

See the OpenTelemetry Python Instrumentation documentation for a complete list of available instrumentations.

Why Custom Exporter?

The standard OTLPSpanExporter from OpenTelemetry Python SDK sends traces in Protobuf format, but Watchlog agent expects JSON format. Therefore, you need to use a custom JSON exporter that converts traces to OTLP JSON format.

Custom JSON Exporter

Create a custom exporter file (e.g., watchlog_exporter.py):

# watchlog_exporter.py
import os
import json
import http.client
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExporter, SpanExportResult
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace import Status, StatusCode
from typing import Sequence
import urllib.parse

# Custom JSON exporter for Watchlog
class WatchlogJSONExporter(SpanExporter):
    def __init__(self, endpoint: str, resource: Resource = None):
        self.endpoint = endpoint
        self.resource = resource
        # Parse endpoint URL
        parsed = urllib.parse.urlparse(endpoint)
        self.host = parsed.hostname
        self.port = parsed.port or (443 if parsed.scheme == 'https' else 80)
        self.path = parsed.path
        self.scheme = parsed.scheme
        self.use_ssl = parsed.scheme == 'https'
    
    def export(self, spans: Sequence) -> SpanExportResult:
        if not spans:
            return SpanExportResult.SUCCESS
        
        try:
            # Convert spans to JSON format
            json_data = self._spans_to_json(spans)
            json_str = json.dumps(json_data)
            
            # Send HTTP POST request
            conn = http.client.HTTPConnection(self.host, self.port) if not self.use_ssl else http.client.HTTPSConnection(self.host, self.port)
            
            headers = {
                'Content-Type': 'application/json',
            }
            
            conn.request('POST', self.path, json_str, headers)
            response = conn.getresponse()
            conn.close()
            
            if response.status == 200:
                return SpanExportResult.SUCCESS
            else:
                return SpanExportResult.FAILURE
        except Exception as e:
            import logging
            logging.getLogger(__name__).error(f"Error exporting spans to watchlog: {e}")
            return SpanExportResult.FAILURE
    
    def _spans_to_json(self, spans):
        """Convert OpenTelemetry spans to JSON format (OTLP JSON format)"""
        span_list = []
        
        for span in spans:
            span_data = {
                "traceId": format(span.context.trace_id, '032x'),
                "spanId": format(span.context.span_id, '016x'),
                "name": span.name,
                "kind": "SPAN_KIND_INTERNAL",
                "startTimeUnixNano": str(span.start_time),
                "endTimeUnixNano": str(span.end_time) if span.end_time else str(span.start_time),
                "attributes": self._attributes_to_array(span.attributes) if span.attributes else [],
            }
            
            # Add parent span ID if exists
            if span.parent and span.parent.span_id:
                span_data["parentSpanId"] = format(span.parent.span_id, '016x')
            
            # Add status if exists
            if span.status:
                span_data["status"] = {
                    "code": "STATUS_CODE_ERROR" if span.status.status_code == StatusCode.ERROR else "STATUS_CODE_OK"
                }
                if span.status.description:
                    span_data["status"]["message"] = span.status.description
            
            # Add events if exists
            if span.events:
                span_data["events"] = [
                    {
                        "timeUnixNano": str(event.timestamp),
                        "name": event.name,
                        "attributes": self._attributes_to_array(event.attributes) if event.attributes else []
                    }
                    for event in span.events
                ]
            
            span_list.append(span_data)
        
        # OTLP JSON format
        # Convert resource attributes to array format
        resource_attrs = []
        if self.resource and hasattr(self.resource, 'attributes') and self.resource.attributes:
            resource_attrs = self._attributes_to_array(self.resource.attributes)
        
        return {
            "resourceSpans": [{
                "resource": {
                    "attributes": resource_attrs
                },
                "scopeSpans": [{
                    "spans": span_list
                }]
            }]
        }
    
    def _attributes_to_array(self, attributes):
        """Convert OpenTelemetry attributes to OTLP JSON format array of {key, value}"""
        result = []
        for key, value in attributes.items():
            # OTLP JSON format: {key: string, value: {stringValue, intValue, doubleValue, boolValue, ...}}
            attr_obj = {"key": key}
            
            if isinstance(value, str):
                attr_obj["value"] = {"stringValue": value}
            elif isinstance(value, bool):
                attr_obj["value"] = {"boolValue": value}
            elif isinstance(value, int):
                attr_obj["value"] = {"intValue": str(value)}
            elif isinstance(value, float):
                attr_obj["value"] = {"doubleValue": value}
            elif isinstance(value, bytes):
                attr_obj["value"] = {"bytesValue": value.hex()}
            else:
                # Default to string
                attr_obj["value"] = {"stringValue": str(value)}
            
            result.append(attr_obj)
        return result
    
    def shutdown(self):
        pass

Basic Usage

For Flask

# otel_config.py
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from watchlog_exporter import WatchlogJSONExporter

# Your application name
APP_NAME = os.getenv('APP_NAME', 'my-python-service')
# Watchlog agent URL (local: http://localhost:3774 or Docker: http://watchlog-agent:3774)
WATCHLOG_URL = os.getenv('WATCHLOG_URL', 'http://localhost:3774')

# Set up OpenTelemetry
resource = Resource.create({
    "service.name": APP_NAME,
    "service.version": "1.0.0",
})

trace.set_tracer_provider(TracerProvider(resource=resource))

# Configure Watchlog JSON exporter
watchlog_exporter = WatchlogJSONExporter(
    endpoint=f"{WATCHLOG_URL}/apm/{APP_NAME}/v1/traces",
    resource=resource
)

# Use BatchSpanProcessor for production (or SimpleSpanProcessor for testing)
span_processor = BatchSpanProcessor(watchlog_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# Flask instrumentation - must be imported after setting up the tracer provider
from opentelemetry.instrumentation.flask import FlaskInstrumentor
FlaskInstrumentor().instrument()
# main.py
# Import OpenTelemetry configuration first
import otel_config

# Now import and use Flask
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, Watchlog APM!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=6000)

For Django

For Django, place the OpenTelemetry configuration in your project's __init__.py file (e.g., myproject/__init__.py):

# myproject/__init__.py
import os
import json
import http.client
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExporter, SpanExportResult
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace import Status, StatusCode
from typing import Sequence
import urllib.parse

# Custom JSON exporter for Watchlog (same as above)
class WatchlogJSONExporter(SpanExporter):
    # ... (copy the WatchlogJSONExporter class from above)
    pass

# Configure OpenTelemetry
resource = Resource.create({
    "service.name": os.getenv("OTEL_SERVICE_NAME", "django-app"),
})

trace.set_tracer_provider(TracerProvider(resource=resource))

# Get OTLP endpoint from environment variable
APP_NAME = os.getenv('APP_NAME', 'django-app')
WATCHLOG_URL = os.getenv('WATCHLOG_URL', 'http://watchlog-agent:3774')
otlp_endpoint = f"{WATCHLOG_URL}/apm/{APP_NAME}/v1/traces"

# Use custom JSON exporter for Watchlog
watchlog_exporter = WatchlogJSONExporter(endpoint=otlp_endpoint, resource=resource)

# Use BatchSpanProcessor for production (or SimpleSpanProcessor for testing)
span_processor = BatchSpanProcessor(watchlog_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# Django instrumentation - must be imported after setting up the tracer provider
from opentelemetry.instrumentation.django import DjangoInstrumentor
DjangoInstrumentor().instrument()

Important: This file (myproject/__init__.py) must be imported before Django settings are loaded. Django will automatically import this when the project starts.

For FastAPI

# otel_config.py
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from watchlog_exporter import WatchlogJSONExporter

# Your application name
APP_NAME = os.getenv('APP_NAME', 'my-fastapi-service')
WATCHLOG_URL = os.getenv('WATCHLOG_URL', 'http://localhost:3774')

# Set up OpenTelemetry
resource = Resource.create({
    "service.name": APP_NAME,
})

trace.set_tracer_provider(TracerProvider(resource=resource))

# Configure Watchlog JSON exporter
watchlog_exporter = WatchlogJSONExporter(
    endpoint=f"{WATCHLOG_URL}/apm/{APP_NAME}/v1/traces",
    resource=resource
)

span_processor = BatchSpanProcessor(watchlog_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# FastAPI instrumentation
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
FastAPIInstrumentor.instrument()
# main.py
import otel_config
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "Watchlog APM"}

Automatic Instrumentation

Once configured, OpenTelemetry will automatically create traces for:

  • HTTP requests - All incoming requests are automatically traced (e.g., GET /api/users, POST /api/login)
  • Database queries - If you use instrumented database libraries (e.g., opentelemetry-instrumentation-psycopg2 for PostgreSQL)
  • External HTTP calls - If you use instrumented HTTP libraries (e.g., opentelemetry-instrumentation-requests for Python requests library)
  • Framework operations - Django/Flask/FastAPI middleware operations are automatically traced

You don't need to manually create spans for these operations. The instrumentation will handle it automatically. For example:

# Django view - NO manual tracing needed!
def my_view(request):
    # This request is automatically traced by DjangoInstrumentor
    # You'll see a span named "GET /my-view/" in Watchlog
    return JsonResponse({"status": "ok"})

All traces (both automatic and manual) will be sent to Watchlog agent and appear in your APM dashboard.

Manual Tracing (Optional)

You can also create custom spans manually:

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def my_function():
    with tracer.start_as_current_span("my_custom_operation") as span:
        span.set_attribute("custom.attribute", "value")
        # Your code here
        pass

Docker Setup

Docker Compose Example for Django:

services:
  watchlog-agent:
    image: watchlog/agent:1.3.0
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    volumes:
      - watchlog-agent-config:/app/app/config
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  django-app:
    build: .
    container_name: django-app
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    environment:
      - APP_NAME=django-app
      - WATCHLOG_URL=http://watchlog-agent:3774
      - OTEL_SERVICE_NAME=django-app
    depends_on:
      - watchlog-agent
    networks:
      - app-network

volumes:
  watchlog-agent-config:

networks:
  app-network:
    driver: bridge

Docker Run Example:

# 1. Create network
docker network create app-network

# 2. Create volume for watchlog-agent
docker volume create watchlog-agent-config

# 3. Run Watchlog Agent
docker run -d \
  --name watchlog-agent \
  --network app-network \
  -p 3774:3774 \
  -v watchlog-agent-config:/app/app/config \
  -e WATCHLOG_APIKEY="your-api-key" \
  -e WATCHLOG_SERVER="https://log.watchlog.ir" \
  watchlog/agent:1.3.0

# 4. Run Python app with environment variables
docker run -d \
  --name python-app \
  --network app-network \
  -p 8000:8000 \
  -e APP_NAME=my-python-service \
  -e WATCHLOG_URL=http://watchlog-agent:3774 \
  my-python-app

Important Notes:

  • When using Docker, use the container name as the hostname (e.g., watchlog-agent)
  • Both containers must be on the same Docker network
  • The agent must be running before your app starts
  • Set the WATCHLOG_URL environment variable to http://watchlog-agent:3774 for Docker
  • Set the APP_NAME environment variable to your application name
  • The trace endpoint format is: ${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces
  • For Django, OpenTelemetry instrumentation must be initialized in myproject/__init__.py before Django setup
  • Install framework-specific instrumentation packages as needed
  • Remember: You must use the custom WatchlogJSONExporter instead of the standard OTLPSpanExporter because Watchlog requires JSON format, not Protobuf

Go Icon Go

Installation

Install OpenTelemetry packages:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/trace \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp

Basic Usage

// main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

var (
    appName    = getEnv("APP_NAME", "my-go-service")
    watchlogURL = getEnv("WATCHLOG_URL", "http://localhost:3774")
)

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func initTracer() func() {
    ctx := context.Background()

    // Create resource
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceNameKey.String(appName),
            semconv.ServiceVersionKey.String("1.0.0"),
        ),
    )
    if err != nil {
        log.Fatalf("failed to create resource: %v", err)
    }

    // Create OTLP exporter
    endpoint := fmt.Sprintf("%s/apm/%s/v1/traces", watchlogURL, appName)
    exporter, err := otlptracehttp.New(ctx,
        otlptracehttp.WithEndpoint(endpoint),
        otlptracehttp.WithInsecure(), // Use WithInsecure() for HTTP
    )
    if err != nil {
        log.Fatalf("failed to create exporter: %v", err)
    }

    // Create trace provider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
    ))

    return func() {
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        if err := tp.Shutdown(ctx); err != nil {
            log.Fatal(err)
        }
    }
}

func main() {
    shutdown := initTracer()
    defer shutdown()

    // Create HTTP handler with OpenTelemetry instrumentation
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, Watchlog APM + Go!"))
    })
    http.Handle("/", otelhttp.NewHandler(handler, "root"))

    log.Printf("Starting server on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Docker Setup

Docker Compose Example:

version: '3.8'

services:
  watchlog-agent:
    image: watchlog/agent:latest
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  go-app:
    build: .
    container_name: go-app
    ports:
      - "8080:8080"
    environment:
      - APP_NAME=my-go-service
      - WATCHLOG_URL=http://watchlog-agent:3774
    depends_on:
      - watchlog-agent
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Java Icon Java

Watchlog APM supports Java applications using OpenTelemetry. Java OpenTelemetry SDK sends JSON format by default, so it works out of the box with Watchlog agent.

Installation

Add OpenTelemetry dependencies to your pom.xml (Maven) or build.gradle (Gradle):

Maven:

<dependencies>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-api</artifactId>
        <version>1.32.0</version>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk</artifactId>
        <version>1.32.0</version>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
        <version>1.32.0</version>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-javaagent</artifactId>
        <version>1.32.0</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Gradle:

dependencies {
    implementation 'io.opentelemetry:opentelemetry-api:1.32.0'
    implementation 'io.opentelemetry:opentelemetry-sdk:1.32.0'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.32.0'
    runtimeOnly 'io.opentelemetry.instrumentation:opentelemetry-javaagent:1.32.0'
}

Basic Usage

Option 1: Using Java Agent (Recommended - Easiest)

The easiest way is to use the OpenTelemetry Java agent which automatically instruments your application:

# Download the agent
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar

# Run your application with the agent
java -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=my-java-service \
  -Dotel.exporter.otlp.endpoint=http://watchlog-agent:3774/apm/my-java-service/v1/traces \
  -jar my-app.jar

Option 2: Manual Instrumentation

// OtelConfig.java
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.ResourceAttributes;

public class OtelConfig {
    private static final String APP_NAME = System.getenv().getOrDefault("APP_NAME", "my-java-service");
    private static final String WATCHLOG_URL = System.getenv().getOrDefault("WATCHLOG_URL", "http://localhost:3774");
    
    public static OpenTelemetry initialize() {
        Resource resource = Resource.getDefault()
            .merge(Resource.create(Attributes.of(
                ResourceAttributes.SERVICE_NAME, APP_NAME,
                ResourceAttributes.SERVICE_VERSION, "1.0.0"
            )));
        
        OtlpHttpSpanExporter spanExporter = OtlpHttpSpanExporter.builder()
            .setEndpoint(WATCHLOG_URL + "/apm/" + APP_NAME + "/v1/traces")
            .build();
        
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
            .setResource(resource)
            .build();
        
        return OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .buildAndRegisterGlobal();
    }
}
// Main.java
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;

public class Main {
    public static void main(String[] args) {
        // Initialize OpenTelemetry
        OpenTelemetry otel = OtelConfig.initialize();
        Tracer tracer = otel.getTracer("my-app");
        
        // Your application code
        // Traces will be automatically sent to Watchlog
    }
}

Spring Boot Integration

For Spring Boot applications, add the OpenTelemetry Spring Boot starter:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
    <version>1.32.0-alpha</version>
</dependency>

Configure in application.properties:

otel.service.name=my-spring-service
otel.exporter.otlp.endpoint=http://watchlog-agent:3774/apm/my-spring-service/v1/traces

Docker Setup

Docker Compose Example:

services:
  watchlog-agent:
    image: watchlog/agent:1.3.0
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    volumes:
      - watchlog-agent-config:/app/app/config
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  java-app:
    build: .
    container_name: java-app
    ports:
      - "8080:8080"
    environment:
      - APP_NAME=my-java-service
      - WATCHLOG_URL=http://watchlog-agent:3774
      - OTEL_SERVICE_NAME=my-java-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://watchlog-agent:3774/apm/my-java-service/v1/traces
    depends_on:
      - watchlog-agent
    networks:
      - app-network

volumes:
  watchlog-agent-config:

networks:
  app-network:
    driver: bridge

.NET Icon .NET

Watchlog APM supports .NET applications using OpenTelemetry. .NET OpenTelemetry SDK sends JSON format by default, so it works out of the box with Watchlog agent.

Installation

Install OpenTelemetry packages via NuGet:

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http

Basic Usage

ASP.NET Core

// Program.cs
using OpenTelemetry;
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;

var builder = WebApplication.CreateBuilder(args);

// Configure OpenTelemetry
var appName = Environment.GetEnvironmentVariable("APP_NAME") ?? "my-dotnet-service";
var watchlogUrl = Environment.GetEnvironmentVariable("WATCHLOG_URL") ?? "http://localhost:3774";
var endpoint = $"{watchlogUrl}/apm/{appName}/v1/traces";

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(serviceName: appName, serviceVersion: "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(endpoint);
        }));

var app = builder.Build();

app.MapGet("/", () => "Hello, Watchlog APM!");

app.Run();

Docker Setup

Docker Compose Example:

services:
  watchlog-agent:
    image: watchlog/agent:1.3.0
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    volumes:
      - watchlog-agent-config:/app/app/config
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  dotnet-app:
    build: .
    container_name: dotnet-app
    ports:
      - "5000:5000"
    environment:
      - APP_NAME=my-dotnet-service
      - WATCHLOG_URL=http://watchlog-agent:3774
    depends_on:
      - watchlog-agent
    networks:
      - app-network

volumes:
  watchlog-agent-config:

networks:
  app-network:
    driver: bridge

Ruby Icon Ruby

Watchlog APM supports Ruby applications using OpenTelemetry. Ruby OpenTelemetry SDK sends JSON format by default, so it works out of the box with Watchlog agent.

Installation

Add to your Gemfile:

gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'

Then install:

bundle install

Basic Usage

Rails Application

# config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  c.service_name = ENV['APP_NAME'] || 'my-rails-service'
  c.service_version = '1.0.0'
  
  watchlog_url = ENV['WATCHLOG_URL'] || 'http://localhost:3774'
  app_name = ENV['APP_NAME'] || 'my-rails-service'
  endpoint = "#{watchlog_url}/apm/#{app_name}/v1/traces"
  
  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: endpoint)
    )
  )
  
  c.use_all
end

Sinatra Application

# app.rb
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'
require 'sinatra'

OpenTelemetry::SDK.configure do |c|
  c.service_name = ENV['APP_NAME'] || 'my-sinatra-service'
  watchlog_url = ENV['WATCHLOG_URL'] || 'http://localhost:3774'
  app_name = ENV['APP_NAME'] || 'my-sinatra-service'
  endpoint = "#{watchlog_url}/apm/#{app_name}/v1/traces"
  
  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: endpoint)
    )
  )
  
  c.use_all
end

get '/' do
  'Hello, Watchlog APM!'
end

Docker Setup

Docker Compose Example:

services:
  watchlog-agent:
    image: watchlog/agent:1.3.0
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    volumes:
      - watchlog-agent-config:/app/app/config
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  ruby-app:
    build: .
    container_name: ruby-app
    ports:
      - "3000:3000"
    environment:
      - APP_NAME=my-ruby-service
      - WATCHLOG_URL=http://watchlog-agent:3774
    depends_on:
      - watchlog-agent
    networks:
      - app-network

volumes:
  watchlog-agent-config:

networks:
  app-network:
    driver: bridge

PHP Icon PHP

Watchlog APM supports PHP applications using OpenTelemetry. PHP OpenTelemetry SDK sends JSON format by default, so it works out of the box with Watchlog agent.

Installation

Install via Composer:

composer require open-telemetry/opentelemetry
composer require open-telemetry/opentelemetry-exporter-otlp
composer require open-telemetry/opentelemetry-instrumentation-psr15
composer require open-telemetry/opentelemetry-instrumentation-psr18

Basic Usage

Laravel Application

// config/opentelemetry.php
<?php

return [
    'service_name' => env('APP_NAME', 'my-laravel-service'),
    'service_version' => '1.0.0',
    'exporter' => [
        'endpoint' => env('WATCHLOG_URL', 'http://localhost:3774') . 
                     '/apm/' . env('APP_NAME', 'my-laravel-service') . '/v1/traces',
    ],
];
// bootstrap/app.php or app/Providers/AppServiceProvider.php
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\Exporter\OTLP\Exporter;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceAttributes;

$watchlogUrl = env('WATCHLOG_URL', 'http://localhost:3774');
$appName = env('APP_NAME', 'my-laravel-service');
$endpoint = "{$watchlogUrl}/apm/{$appName}/v1/traces";

$exporter = new Exporter($endpoint);
$tracerProvider = new TracerProvider(
    new BatchSpanProcessor($exporter),
    ResourceInfo::create([
        ResourceAttributes::SERVICE_NAME => $appName,
    ])
);

Plain PHP

<?php
// otel-init.php
require_once 'vendor/autoload.php';

use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\Exporter\OTLP\Exporter;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceAttributes;

$appName = getenv('APP_NAME') ?: 'my-php-service';
$watchlogUrl = getenv('WATCHLOG_URL') ?: 'http://localhost:3774';
$endpoint = "{$watchlogUrl}/apm/{$appName}/v1/traces";

$exporter = new Exporter($endpoint);
$tracerProvider = new TracerProvider(
    new BatchSpanProcessor($exporter),
    ResourceInfo::create([
        ResourceAttributes::SERVICE_NAME => $appName,
    ])
);

// Use in your application
$tracer = $tracerProvider->getTracer('my-app');

Docker Setup

Docker Compose Example:

services:
  watchlog-agent:
    image: watchlog/agent:1.3.0
    container_name: watchlog-agent
    ports:
      - "3774:3774"
    volumes:
      - watchlog-agent-config:/app/app/config
    environment:
      - WATCHLOG_APIKEY=your-api-key
      - WATCHLOG_SERVER=https://log.watchlog.ir
    networks:
      - app-network

  php-app:
    build: .
    container_name: php-app
    ports:
      - "8000:8000"
    environment:
      - APP_NAME=my-php-service
      - WATCHLOG_URL=http://watchlog-agent:3774
    depends_on:
      - watchlog-agent
    networks:
      - app-network

volumes:
  watchlog-agent-config:

networks:
  app-network:
    driver: bridge

Additional Resources

For more information about OpenTelemetry instrumentation for other languages and frameworks, visit:

  • OpenTelemetry Documentation
  • Node.js Instrumentation
  • Python Instrumentation
  • Go Instrumentation
  • Java Instrumentation
  • .NET Instrumentation

Common Configuration

Regardless of the language or framework, the key configuration is:

  1. Install OpenTelemetry SDK for your platform
  2. Configure OTLP exporter with endpoint: http://<agent-host>:3774/apm/<your-app-name>/v1/traces
  3. Set service name to identify your application in Watchlog
  4. Enable auto-instrumentation for your framework (see OpenTelemetry documentation for framework-specific instrumentations)

Format Support Summary

LanguageDefault FormatWorks with Watchlog?Notes
Node.jsJSON✅ YesWorks out of the box
GoJSON✅ YesWorks out of the box
JavaJSON✅ YesWorks out of the box
.NETJSON✅ YesWorks out of the box
RubyJSON✅ YesWorks out of the box
PHPJSON✅ YesWorks out of the box
PythonProtobuf⚠️ Requires custom exporterUse WatchlogJSONExporter (see Python section)

Troubleshooting

  • No traces appearing? Check that:

    • Watchlog agent is running and accessible
    • Agent URL is correct (use http://watchlog-agent:3774 for Docker)
    • Application name matches in both code and agent configuration
    • OpenTelemetry SDK is initialized before your application code
    • For Python: You are using the custom WatchlogJSONExporter (not the standard OTLPSpanExporter)
    • For Django: The instrumentation code is in myproject/__init__.py and loaded before Django starts
    • For Node.js: OpenTelemetry config is loaded before your application code
    • For Java: Java agent is loaded with -javaagent flag or manual instrumentation is set up
  • Connection errors? Verify:

    • Both agent and app are on the same Docker network (if using Docker)
    • Agent port 3774 is accessible
    • Firewall rules allow communication between containers
  • JSON parsing errors on server?

    • For Python only: Make sure you're using WatchlogJSONExporter which sends JSON format
    • For Python: The standard OTLPSpanExporter sends Protobuf format which won't work with Watchlog
    • For Python: Check that attributes are in the correct format: array of {key, value} objects
    • For other languages: Most SDKs send JSON by default, so this shouldn't be an issue
  • Automatic traces not working?

    • Ensure framework instrumentation is imported after setting up the tracer provider
    • Python/Django: DjangoInstrumentor().instrument() must be called after trace.set_tracer_provider()
    • Python/Flask: FlaskInstrumentor().instrument_app(app) must be called after setting up the tracer
    • Python/FastAPI: FastAPIInstrumentor.instrument_app(app) must be called
    • Node.js: Use getNodeAutoInstrumentations() for automatic instrumentation
    • Java: Use Java agent with -javaagent flag for automatic instrumentation
    • .NET: Use AddAspNetCoreInstrumentation() and AddHttpClientInstrumentation()
    • Check that instrumentation packages are installed for your framework
Last Updated:: 12/23/25, 3:31 PM
Contributors: mohammad
Prev
Custom Events
Next
Real User Monitoring (RUM)