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:
- Install OpenTelemetry SDK for your platform (Node.js, Python, Go, Java, .NET, Ruby, PHP, etc.)
- Configure OTLP exporter to send traces to your Watchlog agent
- 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:
- Watchlog Agent installed and running (see Host Map for installation)
- Agent accessible at
http://localhost:3774(local) orhttp://watchlog-agent:3774(Docker) - Your application name ready (this will be used to identify your service in Watchlog)
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_URLenvironment variable tohttp://watchlog-agent:3774for Docker - Set the
APP_NAMEenvironment variable to your application name - The trace endpoint format is:
${WATCHLOG_URL}/apm/${APP_NAME}/v1/traces
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-psycopg2for PostgreSQL) - External HTTP calls - If you use instrumented HTTP libraries (e.g.,
opentelemetry-instrumentation-requestsfor 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_URLenvironment variable tohttp://watchlog-agent:3774for Docker - Set the
APP_NAMEenvironment 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__.pybefore Django setup - Install framework-specific instrumentation packages as needed
- Remember: You must use the custom
WatchlogJSONExporterinstead of the standardOTLPSpanExporterbecause Watchlog requires JSON format, not Protobuf
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
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
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
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
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:
- Install OpenTelemetry SDK for your platform
- Configure OTLP exporter with endpoint:
http://<agent-host>:3774/apm/<your-app-name>/v1/traces - Set service name to identify your application in Watchlog
- Enable auto-instrumentation for your framework (see OpenTelemetry documentation for framework-specific instrumentations)
Format Support Summary
| Language | Default Format | Works with Watchlog? | Notes |
|---|---|---|---|
| Node.js | JSON | ✅ Yes | Works out of the box |
| Go | JSON | ✅ Yes | Works out of the box |
| Java | JSON | ✅ Yes | Works out of the box |
| .NET | JSON | ✅ Yes | Works out of the box |
| Ruby | JSON | ✅ Yes | Works out of the box |
| PHP | JSON | ✅ Yes | Works out of the box |
| Python | Protobuf | ⚠️ Requires custom exporter | Use WatchlogJSONExporter (see Python section) |
Troubleshooting
No traces appearing? Check that:
- Watchlog agent is running and accessible
- Agent URL is correct (use
http://watchlog-agent:3774for 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 standardOTLPSpanExporter) - For Django: The instrumentation code is in
myproject/__init__.pyand loaded before Django starts - For Node.js: OpenTelemetry config is loaded before your application code
- For Java: Java agent is loaded with
-javaagentflag 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
WatchlogJSONExporterwhich sends JSON format - For Python: The standard
OTLPSpanExportersends 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
- For Python only: Make sure you're using
Automatic traces not working?
- Ensure framework instrumentation is imported after setting up the tracer provider
- Python/Django:
DjangoInstrumentor().instrument()must be called aftertrace.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
-javaagentflag for automatic instrumentation - .NET: Use
AddAspNetCoreInstrumentation()andAddHttpClientInstrumentation() - Check that instrumentation packages are installed for your framework
