Getting started with OpenTelemetry
This guide covers how to enable OpenTelemetry in Posit Connect, common configuration patterns, and how to use OpenTelemetry alongside Prometheus.
Minimal setup
Enable OpenTelemetry in your rstudio-connect.gcfg:
[OpenTelemetry]
Enabled = trueWith this minimal configuration, Connect collects signals and persists them to disk. For details about its impact and retention, see Disk persistence.
Export to an observability backend
To send signals to your observability platform’s locally running agent, add an [OTLPEndpoint] section:
[OpenTelemetry]
Enabled = true
[OTLPEndpoint "mybackend"]
Endpoint = http://<host>:4318The Endpoint setting is the base URL of an OpenTelemetry Protocol (OTLP) HTTP receiver. Port 4318 is the default OTLP HTTP port used by most collectors. Connect appends the signal-specific paths (/v1/traces, /v1/logs, /v1/metrics) automatically so do not include them in the URL.
By default, Connect sends all three signal types (logs, traces, and metrics) to each endpoint. You can configure each endpoint to receive only specific signal types:
[OTLPEndpoint "mybackend"]
Endpoint = http://<host>:4318
Logs = true
Traces = false
Metrics = falseYou can configure multiple named endpoints to fan out signals to different observability platforms.
Most APM platforms require a locally running agent or collector to receive OTLP data. For example, Datadog requires the Datadog Agent running on the same host, which acts as the local collector and handles authentication to the Datadog backend. Ensure your platform’s collector agent is installed and running before configuring the endpoint.
Multiple instances on a shared cluster
When you run several Connect instances on the same Kubernetes cluster—for example, in separate namespaces—their signals arrive at your observability backend together. You cannot tell instances apart by service.name, because it identifies the process type rather than the instance: the server reports posit-connect, content jobs report posit-connect-content-<guid>, and package builders report posit-connect-python or posit-connect-nodejs. Every instance emits the same set of values. To group and distinguish them, Connect stamps two resource attributes that you control on its signals: service.namespace for the deployment or environment, and connect.node.name for the individual replica.
Group signals by deployment with a service namespace
Set OpenTelemetry.ServiceNamespace to a value that identifies the deployment or environment. Connect stamps it as the service.namespace resource attribute on every signal it emits—server, content jobs, and package builders—so all signals from one deployment share a single, stable value that you can group and filter by:
[OpenTelemetry]
Enabled = true
ServiceNamespace = data-scienceSet the value per deployment or environment. When you set nothing, service.namespace defaults to posit-connect-ns.
service.namespace and k8s.namespace.name are different concepts, even when they happen to hold the same value. service.namespace is an application-level, logical grouping from the OpenTelemetry semantic conventions: it groups related software components—for example, separating a billing application from an inventory application—regardless of where they run. k8s.namespace.name is an infrastructure-location attribute that records which Kubernetes namespace partition runs the pod. Set service.namespace to whatever logical grouping suits your deployment; it does not have to match your Kubernetes namespace.
Most APM backends promote service.namespace to a queryable label on their own, so this requires no extra collector configuration.
Label individual replicas with a node name
Within a deployment, connect.node.name distinguishes individual replicas. Connect derives it from Server.NodeName, stamping it on the server’s signals and propagating it to content job signals. In a Kubernetes deployment, NodeName defaults to the pod name (for example, rstudio-connect-7d9f8b6c5-x2k4p)—unique per replica, but ephemeral, because it changes on every rollout, restart, or rescheduling. That is useful for drilling into a specific replica, but prefer service.namespace for stable deployment-level grouping. Outside Kubernetes, set a distinct Server.NodeName per node:
[Server]
NodeName = connect-team-aFilter by Kubernetes metadata
Connect does not detect Kubernetes metadata on its own, but its server honors the standard OTEL_RESOURCE_ATTRIBUTES environment variable: any resource attributes you set there are stamped onto the server’s signals. Set it on the Connect pod using the Kubernetes Downward API to tag them with the namespace, pod, and node:
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: OTEL_RESOURCE_ATTRIBUTES
value: k8s.namespace.name=$(POD_NAMESPACE),k8s.pod.name=$(POD_NAME),k8s.node.name=$(NODE_NAME)Most APM backends map these standard attributes to their own conventions. Datadog’s OTLP pipeline, for example, maps them as follows:
| OpenTelemetry attribute | Datadog tag |
|---|---|
k8s.namespace.name |
kube_namespace |
k8s.pod.name |
pod_name |
k8s.node.name |
kube_node |
You can then distinguish instances in different namespaces by kube_namespace, independent of service.name.
These attributes describe the Connect server’s own pod. Under off-host execution, package builds run in their own pods; their signals carry k8s.namespace.name (the build namespace) and service.namespace, but not k8s.pod.name or k8s.node.name, which Kubernetes assigns at schedule time.
Best practices
- Service namespace: Set
OpenTelemetry.ServiceNamespaceper deployment, or let the Helm chart populate it from the Kubernetes namespace. This is the most reliable way to group and separate deployments, because Connect stamps it on server, content, and builder signals alike, independent of your collector’s Kubernetes enrichment. - Node name: Use
connect.node.name(fromServer.NodeName) to drill into individual replicas within a deployment. In Kubernetes it defaults to the ephemeral pod name; set it explicitly outside Kubernetes. - Collector config directory: Each replica needs its own private copy of
CollectorConfigDir, where the embedded collector writes its runtime config, log file, and an auto-generated Transport Layer Security (TLS) certificate (default/etc/rstudio-connect/otelcol). Connect binds this certificate to the replica’s own network address and regenerates it on each start, so replicas must never share this directory. In Kubernetes, the default path is the pod’s own ephemeral (container-local) filesystem, which is already private to each replica—leave it there. Do not move it onto the shared volume that backsServer.DataDir, and do not back it with ahostPathvolume, because replicas scheduled onto the same node would then share it. - Metric cardinality: Metric names are identical across instances—Connect does not prefix them per instance—so only resource attributes such as
service.namespaceandconnect.node.namedistinguish instances. Running many instances, or adding high-cardinality labels on top, multiplies the number of time series your backend stores. Prefer a small set of stable labels such asservice.namespaceover per-pod identifiers when you build dashboards and alerts.
Disk persistence
By default, Connect persists signals to disk as JSONL files. Diagnostic bundles include these files, which are useful for debugging. See the configuration reference for the full list of [OpenTelemetry] persistence settings: PersistenceEnabled, PersistenceDir, PersistenceMaxSizeMb, PersistenceNumBackups, and PersistenceMaxDays.
You can disable persistence, but we recommend keeping it on even if you export signals to an observability backend. This allows diagnostic bundles to include telemetry signals.
[OpenTelemetry]
Enabled = true
PersistenceEnabled = falseDisabling specific instrumentation
Connect enables both API and database instrumentation by default. You can selectively disable them:
[OpenTelemetry]
Enabled = true
APIInstrumentation = false ; Disable API request tracing
DatabaseInstrumentation = false ; Disable database metricsExporting logs via OpenTelemetry
You can export Connect’s server logs as structured OpenTelemetry logs. OpenTelemetry logs provide richer context through structured attributes (such as content GUIDs, user IDs, and request IDs) and automatic correlation with traces. Log scraping and OTel logs can also run side by side.
If your APM platform is already ingesting Connect’s structured logs from disk (for example, via a log-shipping agent), enabling OpenTelemetry log export sends the same data through a second path. You can disable disk-based log shipping in that case to avoid duplicate ingestion and extra costs.
To export logs via OpenTelemetry, configure an endpoint as follows. Logs defaults to true, but we set it in this example for clarity:
[OpenTelemetry]
Enabled = true
[OTLPEndpoint "mybackend"]
Endpoint = http://<host>:4318
Logs = trueTrace user enrichment
By default, trace spans carry content identifiers (such as content.guid) but not the identity of the user who owns that content. When TraceUserEnrichmentEnabled is set to true, Connect resolves user identity from span attributes before export, adding user.guid, user.username, and user.email to each span.
Connect resolves user identity using the following priority:
user.guid— If the span already carries auser.guidattribute, Connect looks up the corresponding username and email.content.guid— If nouser.guidis present but acontent.guidattribute exists, Connect resolves the content owner and adds theiruser.guid,user.username, anduser.email.
[OpenTelemetry]
Enabled = true
TraceUserEnrichmentEnabled = trueEnrichment only applies to exported trace spans. Metrics and logs are not affected.
Prometheus and OpenTelemetry
Connect supports both Prometheus scraping and native OpenTelemetry export, and you can use both at the same time. We recommend native OpenTelemetry export for new setups because it provides richer context and automatic correlation across signals. If you already have a working Prometheus pipeline, Connect fully supports it.
Enabling OpenTelemetry with an existing Prometheus setup
If you already have Prometheus configured, you can enable OpenTelemetry without disrupting your existing Prometheus pipeline. When both are enabled and Metrics.PrometheusCollectorOTelEnabled = true, the Prometheus /metrics endpoint includes the new OpenTelemetry metrics along with the legacy Prometheus metrics.
[Metrics]
PrometheusListen = :3232
PrometheusCollectorOTelEnabled = true
[OpenTelemetry]
Enabled = trueOpenTelemetry.Enabled must be true for OpenTelemetry metrics to appear on the Prometheus /metrics endpoint.
Migrating from legacy Prometheus HTTP metrics
When OpenTelemetry.Enabled = true and OpenTelemetry.APIInstrumentation = true, the legacy connect_http_* Prometheus HTTP metrics are automatically disabled and replaced by OpenTelemetry-standard http.server.* metrics. On the Prometheus /metrics endpoint, these appear with underscores and unit suffixes (for example, http.server.request.duration becomes http_server_request_duration_seconds).
The following table maps the legacy metrics to their OTel replacements as they appear on the Prometheus /metrics endpoint:
| Legacy Prometheus metric | OTel replacement (Prometheus format) | Notes |
|---|---|---|
connect_http_request_inflight_gauge |
— | No OTel replacement. In-flight request tracking is not available with OTel HTTP instrumentation. |
connect_http_request_count |
— | Use rate(http_server_request_duration_seconds_count[5m]) for request throughput. |
connect_http_request_duration_seconds |
http_server_request_duration_seconds |
Label changes: code → http_status_code, route → http_route, method → http_method |
connect_http_request_size_bytes |
http_server_request_body_size_bytes |
Label changes as above |
connect_http_response_size_bytes |
http_server_response_body_size_bytes |
Label changes as above |
Non-HTTP legacy Prometheus metrics (queue, applications, build, content, and platform runtime metrics) remain unaffected by this change.