Your checkout service runs on Python. Authentication uses Go for speed. The recommendation engine needs Node.js for real-time updates. Each language handles what it does best, but monitoring them creates a nightmare of incompatible tools and disconnected traces.
Traditional monitoring treats each language as a separate world. Python teams install psutil and Flask exporters. Go developers add pprof and custom metrics. Node.js engineers configure PM2 monitoring. When a request touches all three services, you get three disconnected data streams that don't correlate. Finding why checkout failed means switching between three different dashboards, each with its own metric names and trace formats.
This fragmentation breaks observability. OpenTelemetry solves it by providing a single standard that works identically across languages. The same concepts, the same semantic conventions, the same trace context propagation. This article shows how to instrument Python, Node.js, and Go services so they speak the same observability language.
Polyglot Challenges
A payment request starts in your Python API gateway. It calls Go-based fraud detection, which queries a Node.js session service, which hits a Python billing engine. Four services, three languages, one user waiting for a response.
Each service generates telemetry data differently. Python's Flask has built-in request timing. Go's net/http package tracks connections. Node.js Express middleware logs differently than Fastify. Without coordination, these data points remain isolated. You see that billing took 2 seconds but can't trace back to which session triggered it.
The instrumentation burden multiplies with each language. Python services need Prometheus client libraries configured one way. Go services require a different setup. Node.js demands yet another approach. Teams maintain three separate monitoring stacks, each with its own learning curve and edge cases.
Correlation becomes impossible. A trace ID generated in Python won't propagate to Go without explicit header management. Go's trace won't reach Node.js unless you write custom middleware. Each language boundary breaks the observability chain.
OpenTelemetry Standard
OpenTelemetry provides vendor-neutral APIs and SDKs for every major language. The instrumentation looks different syntactically but follows identical concepts. A span in Python works the same as a span in Go or Node.js. Semantic conventions ensure metrics use consistent names across languages.
The SDK handles trace context propagation automatically. When your Python service calls a Go service, OpenTelemetry injects trace headers into the HTTP request. The Go service extracts these headers and continues the same trace. No custom code needed, no header mapping to maintain.
Instrumentation libraries exist for every framework. Flask and FastAPI for Python. Gin and Echo for Go. Express and NestJS for Node.js. These libraries auto-instrument common operations like HTTP requests, database queries, and message queue interactions. You add one initialization block and get distributed tracing.
Semantic conventions standardize metric and span names. All HTTP servers use http.server.duration
regardless of language. Database queries use db.statement
everywhere. This consistency lets you compare performance across services written in different languages. A 100ms database query looks the same whether Python or Go executed it.
Python Instrumentation
Python microservices typically use Flask or FastAPI. OpenTelemetry provides automatic instrumentation for both frameworks (Flask and FastAPI) that requires minimal configuration.
from flask import Flask
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
# Configure tracer
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
# Export to Uptrace
otlp_exporter = OTLPSpanExporter(
endpoint="https://otlp.uptrace.dev:4317",
headers={"uptrace-dsn": "your-project-dsn"}
)
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(otlp_exporter)
)
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
@app.route('/api/orders')
def create_order():
with tracer.start_as_current_span("validate_order") as span:
span.set_attribute("order.items", 3)
# Validation logic
# Automatic instrumentation captures HTTP details
return {"order_id": "12345"}
The FlaskInstrumentor captures every HTTP request automatically. Request method, path, status code, and duration all become span attributes. When this service calls another microservice using the requests library, OpenTelemetry injects trace context into the HTTP headers.
Database instrumentation works similarly. Install the appropriate library and connections get traced automatically.
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
import psycopg2
Psycopg2Instrumentor().instrument()
conn = psycopg2.connect("dbname=orders")
cursor = conn.cursor()
# This query is automatically traced with statement and timing
cursor.execute("SELECT * FROM orders WHERE user_id = %s", [user_id])
Each database query becomes a span with the SQL statement, execution time, and result count. These spans nest under the parent HTTP request span, showing exactly which endpoint triggered which queries.
Go Instrumentation
Go microservices prioritize performance. OpenTelemetry's Go SDK adds minimal overhead while providing complete observability. The Gin framework is popular for HTTP APIs and has native OpenTelemetry support.
package main
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"github.com/gin-gonic/gin"
)
func main() {
// Configure exporter
exporter, _ := otlptracegrpc.New(
context.Background(),
otlptracegrpc.WithEndpoint("otlp.uptrace.dev:4317"),
otlptracegrpc.WithHeaders(map[string]string{
"uptrace-dsn": "your-project-dsn",
}),
)
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
)
otel.SetTracerProvider(tp)
// Setup Gin with middleware
r := gin.Default()
r.Use(otelgin.Middleware("fraud-service"))
r.POST("/verify", verifyTransaction)
r.Run(":8080")
}
func verifyTransaction(c *gin.Context) {
ctx := c.Request.Context()
tracer := otel.Tracer("fraud-service")
_, span := tracer.Start(ctx, "check_fraud_rules")
defer span.End()
// Rules engine logic
span.SetAttributes(
attribute.String("transaction.id", txnID),
attribute.Bool("fraud.detected", false),
)
c.JSON(200, gin.H{"status": "approved"})
}
The otelgin middleware automatically creates a span for each request. When your Python service calls this Go endpoint, the middleware extracts the trace context from the incoming HTTP headers and continues the distributed trace.
Database calls need explicit instrumentation in Go, but the pattern stays consistent with OpenTelemetry conventions.
import (
"database/sql"
"go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql"
)
db, err := otelsql.Open("postgres", dsn,
otelsql.WithAttributes(
semconv.DBSystemPostgreSQL,
),
)
Every SQL query executed through this connection generates a span with the statement, timing, and connection details. The spans link to the parent HTTP request span automatically.
Node.js Instrumentation
Node.js microservices often use Express or NestJS. OpenTelemetry provides automatic instrumentation that hooks into these frameworks at startup.
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'https://otlp.uptrace.dev:4317',
headers: {
'uptrace-dsn': 'your-project-dsn'
}
}),
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
sdk.start();
const express = require('express');
const app = express();
app.get('/sessions/:id', async (req, res) => {
const tracer = require('@opentelemetry/api').trace.getTracer('session-service');
const span = tracer.startSpan('load_session');
// Session loading logic
span.setAttribute('session.id', req.params.id);
span.setAttribute('session.active', true);
span.end();
res.json({ sessionId: req.params.id });
});
app.listen(3000);
The HttpInstrumentation captures all incoming and outgoing HTTP requests. When a Go service calls this Node.js endpoint, the trace context propagates seamlessly. The ExpressInstrumentation adds route information and middleware timing to each span.
Database instrumentation follows the same pattern as other languages.
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
new MongoDBInstrumentation().enable();
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
// All MongoDB operations are now traced automatically
const db = client.db('sessions');
const sessions = await db.collection('sessions').findOne({ id: sessionId });
MongoDB queries become spans with the operation name, collection, and query shape. These spans nest under the HTTP request span that triggered them.
Trace Propagation
A complete payment flow demonstrates how OpenTelemetry maintains trace context across language boundaries. The request starts in Python, moves to Go, calls Node.js, and returns through the same path.
# Python API Gateway
import requests
from opentelemetry import trace
@app.route('/checkout')
def checkout():
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("checkout_flow") as span:
span.set_attribute("user.id", user_id)
# Call Go fraud detection
fraud_response = requests.post(
"http://fraud-service:8080/verify",
json={"amount": 99.99}
)
if fraud_response.json()["status"] == "approved":
# Call Node.js session service
session_response = requests.get(
f"http://session-service:3000/sessions/{session_id}"
)
return {"status": "success"}
The requests library automatically injects trace context into the HTTP headers when calling the Go service. The Go service extracts it and continues the trace.
// Go Fraud Service
func verifyTransaction(c *gin.Context) {
// Trace context automatically extracted from headers
ctx := c.Request.Context()
tracer := otel.Tracer("fraud-service")
_, span := tracer.Start(ctx, "fraud_check")
defer span.End()
// Call Node.js session service
req, _ := http.NewRequestWithContext(ctx, "GET",
"http://session-service:3000/sessions/abc", nil)
// Context propagates to Node.js automatically
resp, _ := http.DefaultClient.Do(req)
c.JSON(200, gin.H{"status": "approved"})
}
The http.NewRequestWithContext ensures the trace context propagates to the Node.js service. Each service adds its own spans to the same distributed trace. In Uptrace, you see one continuous trace showing the entire request flow across all three languages.
Unified Metrics
OpenTelemetry semantic conventions ensure metrics have consistent names and attributes across languages. HTTP server duration uses the same metric name whether measured in Python, Go, or Node.js.
# Python metrics
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
metrics.set_meter_provider(MeterProvider())
meter = metrics.get_meter(__name__)
orders_counter = meter.create_counter(
"orders.created",
description="Number of orders created",
unit="1"
)
orders_counter.add(1, {"status": "success", "payment_method": "card"})
// Go metrics
meter := otel.Meter("fraud-service")
verifications := meter.Int64Counter(
"verifications.performed",
metric.WithDescription("Number of fraud verifications"),
metric.WithUnit("1"),
)
verifications.Add(ctx, 1,
metric.WithAttributes(
attribute.String("result", "approved"),
),
)
// Node.js metrics
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('session-service');
const sessionsLoaded = meter.createCounter('sessions.loaded', {
description: 'Number of sessions loaded',
unit: '1'
});
sessionsLoaded.add(1, { status: 'active' });
All three services export metrics with consistent naming. In Uptrace, you can compare orders.created
, verifications.performed
, and sessions.loaded
on the same dashboard. Attributes follow the same conventions, making cross-service analysis straightforward.
Service Dependencies
Distributed tracing reveals how services interact across language boundaries. A single trace shows the complete request path with timing for each service.
Python API Gateway calls Go fraud detection, which queries Node.js session service, which reads from its database. The trace visualization shows each step with its duration and any errors.
Service dependency maps generate automatically from trace data. Uptrace analyzes spans to build a graph showing which services call which endpoints. Python to Go connections appear as edges in the graph. Go to Node.js connections show up with request rates and error percentages.
Performance bottlenecks become obvious. If the Node.js session service consistently takes 500ms while other services complete in under 50ms, the trace flamegraph highlights this immediately. You can drill down to see that database queries in Node.js account for most of the latency.
Error correlation works across languages. When the Go fraud service returns an error, the trace shows the complete context. Which Python endpoint triggered the verification? What session ID was involved? Which database query failed in Node.js? All this information exists in one trace.
Performance Comparison
OpenTelemetry overhead stays minimal across languages. Python adds roughly 1-2ms per instrumented request. Go adds under 0.5ms due to its compiled nature. Node.js falls between them at around 1ms.
Batch processing reduces export overhead. All three SDKs buffer spans in memory and export them in batches. This means most requests complete without waiting for network calls to the tracing backend. Configure batch size based on your service's request volume.
Sampling controls data volume. High-traffic services can sample 10% of requests while still maintaining full trace context. When a sampled request calls an unsampled service, OpenTelemetry ensures the entire trace gets recorded. This prevents incomplete traces while keeping costs down.
Memory usage scales with span complexity. Simple HTTP request spans use minimal memory. Spans with large attributes or many child spans consume more. The Go SDK uses around 1KB per span. Python and Node.js use slightly more due to runtime overhead.
Implementation with Uptrace
Uptrace provides a unified dashboard for polyglot microservices instrumented with OpenTelemetry. Connect your Python, Go, and Node.js services to the same project.
Configure each service with your Uptrace DSN. The configuration looks identical across languages, just the syntax differs.
# Python
otlp_exporter = OTLPSpanExporter(
endpoint="https://otlp.uptrace.dev:4317",
headers={"uptrace-dsn": "your-project-dsn"}
)
// Go
exporter, _ := otlptracegrpc.New(
context.Background(),
otlptracegrpc.WithEndpoint("otlp.uptrace.dev:4317"),
otlptracegrpc.WithHeaders(map[string]string{
"uptrace-dsn": "your-project-dsn",
}),
)
// Node.js
const exporter = new OTLPTraceExporter({
url: 'https://otlp.uptrace.dev:4317',
headers: {
'uptrace-dsn': 'your-project-dsn'
}
});
Traces from all three languages appear in the same interface. Filter by service name to focus on one language or view cross-service traces to see the complete request flow. The trace timeline shows Python API calls leading to Go processing leading to Node.js session loading.
Service performance metrics aggregate across languages. Compare P95 latency for Python endpoints against Go endpoints. Identify which language handles which workload most efficiently. Set alerts that trigger when any service exceeds its latency threshold.
Error tracking works uniformly. An exception in Python appears with the same detail level as a panic in Go or an error in Node.js. Stack traces link to the specific code line. Related spans show what led to the error.
Ready to unify your polyglot observability? Start with Uptrace.
You may also be interested in:
Top comments (0)