OpenTelemetry
Metrics

Metrics

TracePath ingests OpenTelemetry metrics via the OTLP/HTTP protocol at POST /api/otel/v1/metrics.

Metrics are their own OTLP signal. A trace exporter does not send them, so the SDK needs a metric reader of its own. See Three signals, three exporters.

Supported Metric Types

OTel TypeSupportedTracePath Handling
GaugeYesStored as-is
SumYesStored as-is
HistogramYesComputes average and count (see below)
ExponentialHistogramNoDropped
SummaryNoDropped

ExponentialHistogram and Summary are dropped without an error. TracePath answers 200 OK and stores nothing. The metric never shows up in dashboards or in metric discovery, so the only symptom is an empty widget. If a metric is missing, check its aggregation first. Switch the SDK's histogram aggregation to explicit-bucket (Aggregation.Explicit, explicit_bucket_histogram, or EXPLICIT_BUCKET_HISTOGRAM for the Java agent), or convert in a Collector before exporting. Summary is a Prometheus-only legacy type; export the same data as a Gauge or a Histogram instead.

Histogram Handling

Histograms are converted into two separate metric records:

Derived MetricNameValue
Average{name}.avgsum / count
Count{name}.countTotal number of observations

For example, a histogram named http.request.duration produces:

  • http.request.duration.avg: the average request duration
  • http.request.duration.count: the total number of requests observed

{name}.count is registered as a counter with unit count. {name}.avg keeps the histogram's own unit and is registered as a gauge.

{name}.avg is only emitted for data points that have both a non-zero count and a sum. A delta histogram with no observations in an interval, or a histogram exported without a sum, produces {name}.count alone. An average chart then shows a gap for those intervals rather than a zero.

Histogram bucket boundaries are not stored, so percentiles cannot be computed from a Histogram. Use the Endpoints page, which keeps raw request durations, for P50/P95/P99.

Resource Attributes

service.name is copied onto every metric point as the server_name tag.

Two other things live on the Resource rather than on the data point, and TracePath lifts both onto each point's tags so you can group and filter by them.

The first is the identity of the thing being measured. The hostmetrics process scraper emits one ResourceMetrics block per process, and docker_stats, kubeletstats and the postgresql receiver do the same for containers, pods, nodes and databases. Without lifting those, every process.cpu.utilization point would look identical.

The second is host and platform metadata, which is what the organization overview shows next to each server.

AttributeTypical source
host.name, host.id, host.arch, os.type, os.descriptionhostmetrics receiver / resourcedetection processor
cloud.provider, cloud.region, cloud.availability_zoneresourcedetection processor on cloud VMs
process.pid, process.executable.name, process.command_line, process.ownerhostmetrics process scraper
container.name, container.image.namedocker_stats receiver
k8s.cluster.name, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.deployment.name, k8s.container.namekubeletstats and k8s receivers, see Kubernetes
postgresql.database.namepostgresql receiver

This is a fixed allowlist, not a passthrough, because each distinct tag value multiplies the number of stored series. Anything outside it, deployment.environment for example, is dropped. If you need to slice by something not on the list, set it as a data-point attribute in your instrumentation, or add it with the Collector's attributes processor, rather than as a resource attribute.

A data-point attribute wins when both carry the same key, except server_name, which service.name always overwrites.

As with all attributes, only strings, numbers and booleans survive. Array and map values are dropped.

Example: Node.js Metric Export

npm install @opentelemetry/sdk-node @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/sdk-metrics @opentelemetry/api
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
 
const sdk = new NodeSDK({
  serviceName: "my-service",
  metricReaders: [
    new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({
        url: "https://ingest.tracepath.dev/api/otel/v1/metrics",
        headers: {
          Authorization: "Bearer your-project-token",
        },
      }),
      exportIntervalMillis: 30000,
    }),
  ],
});
 
sdk.start();

Use metricReaders (an array). The singular metricReader option still works but is deprecated. If you already have a NodeSDK for traces, add metricReaders to that instance instead of starting a second SDK. The overview page shows one SDK wiring all three signals.

Recording a custom metric then looks like this:

import { metrics } from "@opentelemetry/api";
 
const meter = metrics.getMeter("shop");
const ordersPlaced = meter.createCounter("orders.placed", {
  description: "Orders successfully placed",
});
 
ordersPlaced.add(1, { channel: "web" });

The counter is exported on the next 30-second interval and appears in metric discovery as orders.placed, with channel as a tag you can group by.

Example: Python Metric Export

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
 
resource = Resource.create({"service.name": "my-python-service"})
 
exporter = OTLPMetricExporter(
    endpoint="https://ingest.tracepath.dev/api/otel/v1/metrics",
    headers={"Authorization": "Bearer your-project-token"},
)
 
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=30000)
provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)
 
meter = metrics.get_meter("shop")
 
orders = meter.create_counter("orders.placed", unit="1", description="orders placed")
orders.add(1, {"channel": "web"})
 
checkout = meter.create_histogram("checkout.latency", unit="ms")
checkout.record(12.5, {"channel": "web"})
 
# Short-lived script: flush before exiting, or the batch never leaves.
provider.force_flush()
provider.shutdown()

The provider on its own exports nothing. An instrument has to be created and a value recorded, and in a script that exits quickly the flush has to happen before the interpreter tears down. A long-running service can drop the last two lines, since the reader exports on its own interval.

checkout.latency arrives as checkout.latency.avg and checkout.latency.count, per the histogram split above.

The SDK's default export interval is 60 seconds. With export_interval_millis left out, or on the zero-code opentelemetry-instrument path, a working pipeline still shows nothing for the first minute, which reads as failure. Set export_interval_millis=10000, or OTEL_METRIC_EXPORT_INTERVAL=10000 on the env-var path, while you are verifying the integration.

The zero-code path is simpler: under opentelemetry-instrument the meter provider is already configured, so drop everything above and start at metrics.get_meter(...). See the Python guide.

Example: Java (Agent)

The Java agent exports JVM runtime metrics (heap, GC, thread counts) and HTTP server metrics by default, because otel.metrics.exporter is otlp. There is nothing to add:

java \
  -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=my-spring-app \
  -Dotel.exporter.otlp.protocol=http/protobuf \
  -Dotel.exporter.otlp.endpoint=https://ingest.tracepath.dev/api/otel \
  -Dotel.exporter.otlp.headers="Authorization=Bearer <project_token>" \
  -jar target/my-app.jar

Remove any -Dotel.metrics.exporter=none flag and the metrics appear in metric discovery, ready to chart.

Leave otel.exporter.otlp.metrics.default.histogram.aggregation at its default EXPLICIT_BUCKET_HISTOGRAM. Setting it to BASE2_EXPONENTIAL_BUCKET_HISTOGRAM makes TracePath drop every histogram silently, which takes http.server.request.duration and the JVM latency histograms off your dashboards.

Next Steps

  • Traces: how OTel spans map to TracePath concepts
  • Logs: export logs and link them to your traces
  • Overview: endpoint, authentication, limits, quota, and a "nothing is showing up" checklist