Instrumentation
Connect your application to JuhJuh observability. Create API keys, install OpenTelemetry SDKs for Python, Node.js, Go, or Java, or send simple JSON over HTTP. Step-by-step setup for every language.
Instrumentation is how your application sends observability data to JuhJuh. You have two options: use an OpenTelemetry SDK for automatic instrumentation, or send simple JSON over HTTP for quick integration without dependencies.
This guide walks through authentication, SDK setup for four languages, and the simple JSON format. Pick the approach that fits your stack.
Authentication¶
Every request to the ingestion endpoints requires an API key.
Create an API key¶
- Open your organization's Settings
- Go to API Keys
- Click Create API Key
- Copy the key immediately. It starts with
jjk_and you will not see the full key again after this screen - Store the key in your application's environment configuration
How authentication works¶
Include the key in the Authorization header on every request:
Authorization: Bearer jjk_YOUR_API_KEY
Keys are hashed before storage. JuhJuh never stores your raw key. Each key can be deactivated or set to expire on a specific date from the API Keys settings page.
Key management¶
| Action | How |
|---|---|
| Deactivate | Toggle the key's active status. Deactivated keys are rejected immediately |
| Set expiry | Choose an expiration date. Expired keys stop working at midnight UTC on that date |
| Delete | Remove the key permanently. Any application still using it will get 401 errors |
Ingestion endpoints¶
Three endpoints accept observability data. All three support both OTLP and simple JSON formats, detected automatically from the request body.
| Signal | Endpoint |
|---|---|
| Logs | POST /api/ingest/logs/ |
| Metrics | POST /api/ingest/metrics/ |
| Traces | POST /api/ingest/traces/ |
An additional OTLP-standard path is available for traces: POST /api/ingest/v1/traces/. Both trace endpoints accept the same payloads.
Common behavior¶
Compression. All endpoints accept gzip and deflate compression via the Content-Encoding header. Compressed payloads are decompressed up to a 2 MB limit. Payloads exceeding 2 MB return a 413 status.
Batch processing. Requests with 1 to 50 items are processed synchronously and return 200. Requests with more than 50 items return 202 and are processed asynchronously in the background. Async processing retries up to 3 times with 10-second delays on failure.
Rate limits. Each API key allows 200 requests per 60 seconds per endpoint. Exceeding this returns 429. Batch more items per request to stay within limits.
OpenTelemetry SDK setup¶
OpenTelemetry is the recommended approach. It provides automatic instrumentation for HTTP frameworks, database clients, and cache libraries with minimal code changes.
Python¶
Install the SDK and OTLP exporter:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
Configure the exporter in your application startup:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
resource = Resource.create({"service.name": "my-python-service"})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(
endpoint="https://your-org.juhjuh.com/api/ingest/v1/traces/",
headers={"Authorization": "Bearer jjk_YOUR_API_KEY"},
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
Add auto-instrumentation for your framework:
pip install opentelemetry-instrumentation-django # or flask, fastapi, etc.
from opentelemetry.instrumentation.django import DjangoInstrumentor
DjangoInstrumentor().instrument()
For metrics and logs, add the corresponding exporters:
pip install opentelemetry-exporter-otlp-proto-http
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
metric_exporter = OTLPMetricExporter(
endpoint="https://your-org.juhjuh.com/api/ingest/metrics/",
headers={"Authorization": "Bearer jjk_YOUR_API_KEY"},
)
reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=60000)
meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
Node.js¶
Install the packages:
npm install @opentelemetry/api @opentelemetry/sdk-node \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/exporter-metrics-otlp-proto \
@opentelemetry/auto-instrumentations-node
Create a tracing.js file and require it before your application starts:
const { NodeSDK } = require("@opentelemetry/sdk-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-proto");
const { OTLPMetricExporter } = require("@opentelemetry/exporter-metrics-otlp-proto");
const { PeriodicExportingMetricReader } = require("@opentelemetry/sdk-metrics");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
const traceExporter = new OTLPTraceExporter({
url: "https://your-org.juhjuh.com/api/ingest/v1/traces/",
headers: { Authorization: "Bearer jjk_YOUR_API_KEY" },
});
const metricExporter = new OTLPMetricExporter({
url: "https://your-org.juhjuh.com/api/ingest/metrics/",
headers: { Authorization: "Bearer jjk_YOUR_API_KEY" },
});
const sdk = new NodeSDK({
serviceName: "my-node-service",
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 60000,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Start your application with the tracing file loaded first:
node -r ./tracing.js app.js
Go¶
Install the packages:
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Configure the exporter:
package main
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func initTracer() (*sdktrace.TracerProvider, error) {
exporter, err := otlptracehttp.New(
context.Background(),
otlptracehttp.WithEndpointURL("https://your-org.juhjuh.com/api/ingest/v1/traces/"),
otlptracehttp.WithHeaders(map[string]string{
"Authorization": "Bearer jjk_YOUR_API_KEY",
}),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("my-go-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Java¶
Add the dependencies to your build file:
<!-- Maven -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
Configure the exporter:
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes;
Resource resource = Resource.getDefault()
.merge(Resource.create(io.opentelemetry.api.common.Attributes.of(
ResourceAttributes.SERVICE_NAME, "my-java-service"
)));
OtlpHttpSpanExporter exporter = OtlpHttpSpanExporter.builder()
.setEndpoint("https://your-org.juhjuh.com/api/ingest/v1/traces/")
.addHeader("Authorization", "Bearer jjk_YOUR_API_KEY")
.build();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.setResource(resource)
.build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
Simple JSON (no SDK)¶
If you prefer not to use an SDK, send simple JSON directly over HTTP. This works from any language, any script, or any tool that can make HTTP requests.
Sending logs¶
curl -X POST https://your-org.juhjuh.com/api/ingest/logs/ \
-H "Authorization: Bearer jjk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"logs": [
{
"message": "User login successful",
"level": "info",
"service": "auth-service",
"timestamp": "2026-04-09T14:30:00Z",
"attributes": {
"user_id": "usr_12345",
"method": "oauth"
}
}
]
}'
Sending metrics¶
curl -X POST https://your-org.juhjuh.com/api/ingest/metrics/ \
-H "Authorization: Bearer jjk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"metrics": [
{
"name": "http.request.duration",
"value": 142.5,
"metric_type": "gauge",
"unit": "ms",
"tags": {
"service": "api-gateway",
"endpoint": "/api/users",
"method": "GET"
}
}
]
}'
Sending traces¶
curl -X POST https://your-org.juhjuh.com/api/ingest/traces/ \
-H "Authorization: Bearer jjk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"spans": [
{
"trace_id": "abc123def456",
"span_id": "span_001",
"parent_span_id": "",
"operation_name": "GET /api/users",
"service_name": "api-gateway",
"span_kind": "server",
"status_code": "ok",
"duration_ms": 142,
"started_at": "2026-04-09T14:30:00Z",
"http_method": "GET",
"http_url": "/api/users",
"http_status_code": 200,
"attributes": {
"user.id": "usr_12345"
}
}
]
}'
OpenTelemetry Collector¶
For applications that already use an OpenTelemetry Collector, point the OTLP HTTP exporter at JuhJuh:
exporters:
otlphttp/juhjuh:
endpoint: "https://your-org.juhjuh.com/api/ingest"
headers:
Authorization: "Bearer jjk_YOUR_API_KEY"
compression: gzip
retry_on_failure:
enabled: true
max_elapsed_time: 300s
service:
pipelines:
traces:
exporters: [otlphttp/juhjuh]
metrics:
exporters: [otlphttp/juhjuh]
logs:
exporters: [otlphttp/juhjuh]
The Collector handles batching, retry, and compression automatically.
Compression¶
All ingestion endpoints accept compressed payloads. Set the Content-Encoding header to gzip or deflate. This reduces network bandwidth significantly for large batches.
Most OpenTelemetry SDKs and the Collector support gzip compression out of the box. For simple JSON, compress with your language's standard library:
import gzip
import json
import requests
payload = json.dumps({"logs": [{"message": "compressed log", "level": "info"}]})
compressed = gzip.compress(payload.encode())
requests.post(
"https://your-org.juhjuh.com/api/ingest/logs/",
data=compressed,
headers={
"Authorization": "Bearer jjk_YOUR_API_KEY",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
)
The decompressed payload must be under 2 MB. Requests exceeding this limit return 413.
Error responses¶
| Status | Meaning |
|---|---|
| 200 | Data accepted and processed |
| 202 | Data accepted, processing asynchronously (batch > 50 items) |
| 400 | Invalid JSON or missing required fields |
| 401 | Missing, invalid, expired, or deactivated API key |
| 413 | Decompressed payload exceeds 2 MB |
| 429 | Rate limit exceeded (200 requests per 60 seconds per key per endpoint) |
Next steps¶
With your application connected, explore each signal:
- Logging for searching and filtering your log data
- Metrics for understanding metric types and building dashboards
- Tracing for distributed request tracing and service dependency maps
- Alerts for setting up threshold-based notifications