Log forwarding
Add an observability destination and Truto streams your environment's logs into the tool your team already searches: Datadog, Splunk, Microsoft Sentinel, any OpenTelemetry collector, Elasticsearch, or an S3-compatible bucket.
Skip the reading — fast path for coding agents
truto observability-destinations list
/skill truto
Before you start
- One environment per destination. A destination belongs to one environment and receives only that environment's logs. Add one per environment you want to watch. An environment can have several destinations; each receives its own copy.
- Public HTTPS only. Every URL you enter must be a public
httpsURL. Truto rejectslocalhost, private and reserved IP addresses, hostnames without a dot, and URLs with a username or password in them. Redirects are not followed. - Secrets are write-only. API keys, tokens, client secrets and passwords go in a separate
secretsobject. Truto never returns them, in the console or the API. - Console access. The Observability tab under Settings is shown to workspace admins.
1. Add a destination
From the console
- Open Settings → Observability (
app.truto.one/settings/observability) and pick the environment. - Click Add Destination.
- Fill in the modal:
- Name — a label for the destination, for example
Production logs. - Type — Datadog, Splunk HEC, Microsoft Sentinel, OpenTelemetry (OTLP), Elasticsearch, or Amazon S3 / GCS / R2 archive. The type can't be changed after the destination is created.
- The type's own fields and credentials. See Destination reference.
- Active — whether the destination receives logs.
- Log Types — which logs to forward. A new destination starts with only API checked. See Log types.
- Name — a label for the destination, for example
- Click Create.
From the API
POST /observability-destination creates a destination. With an API token, the destination is created in the token's environment.
| Field | Type | Meaning |
|---|---|---|
type |
string | datadog, splunk_hec, sentinel, otlp, elasticsearch, or s3. Required. |
name |
string | Label for the destination. Required. |
config |
object | The type's settings. Required. See Destination reference. |
secrets |
object | The type's credentials. Write-only. Required for every type except otlp. |
is_active |
boolean | Whether the destination receives logs. Defaults to true. |
truto observability-destinations create \
--type datadog \
--name "Production logs" \
--config '{"site":"eu","tags":["team:integrations"],"log_types":["api","durable_object"]}' \
--secrets '{"api_key":"<DATADOG_API_KEY>"}'curl -X POST https://api.truto.one/observability-destination \
-H "Authorization: Bearer <API token>" \
-H "Content-Type: application/json" \
-d '{
"type": "datadog",
"name": "Production logs",
"config": {
"site": "eu",
"tags": ["team:integrations"],
"log_types": ["api", "durable_object"]
},
"secrets": { "api_key": "<DATADOG_API_KEY>" }
}'import asyncio
from truto_python_sdk import TrutoApi
truto_api = TrutoApi(token="<API token>")
async def main():
destination = await truto_api.observability_destinations.create({
"type": "datadog",
"name": "Production logs",
"config": {
"site": "eu",
"tags": ["team:integrations"],
"log_types": ["api", "durable_object"],
},
"secrets": {"api_key": "<DATADOG_API_KEY>"},
})
print(destination["id"])
asyncio.run(main())The response is the destination without its secrets:
{
"id": "6c1f6f0e-5b0e-4c55-9a43-2f1c3d6c8a10",
"environment_id": "ac15abdc-b38e-47d0-97a2-69194017c199",
"type": "datadog",
"name": "Production logs",
"is_active": true,
"config": {
"site": "eu",
"tags": ["team:integrations"],
"log_types": ["api", "durable_object"]
},
"created_at": "2026-11-02T10:12:03.000Z",
"updated_at": "2026-11-02T10:12:03.000Z"
}2. Test the connection
POST /observability-destination/:id/test sends one synthetic api log, with the message truto observability destination test, to the destination and reports what the destination answered. In the console, open the destination's menu and click Test connection, or click Test connection inside the edit modal.
To test settings before you save them, send config and secrets in the body. They replace the stored values for this one request, exactly as a save would. When you change the host of a URL that credentials are sent to, you must send secrets too (see Changing a destination).
truto observability-destinations test <destination_id>curl -X POST https://api.truto.one/observability-destination/<destination_id>/test \
-H "Authorization: Bearer <API token>"result = await truto_api.observability_destinations.test("<destination_id>"){ "ok": true, "status": 202 }On failure, ok is false, status is the destination's HTTP status (absent when Truto couldn't connect), and error explains it. Splunk, Elasticsearch and S3 failures carry the destination's own message, for example Splunk HEC: Incorrect index (code 7) or S3: AccessDenied: Access Denied.
For S3, a test writes a real object to your bucket.
Destination reference
Each type takes a config object and a secrets object. Every type also accepts config.log_types (see Log types).
Datadog
| Field | Where | Required | Meaning |
|---|---|---|---|
site |
config | Yes | Your Datadog site: us1 (datadoghq.com), us3 (us3.datadoghq.com), us5 (us5.datadoghq.com), eu (datadoghq.eu), or ap1 (ap1.datadoghq.com). Console label: Site / Region. |
tags |
config | No | Extra tags added to every log, such as team:integrations. Console label: Tags. |
api_key |
secrets | Yes | A Datadog API key. Console label: API Key. |
Logs arrive with service:truto-api and the tags log_type:<log type> and env:<deployment>, plus your own tags. HTTP fields are also nested under http (http.url, http.status_code, http.method, http.useragent, and so on) to match Datadog's standard attributes.
Splunk HEC
| Field | Where | Required | Meaning |
|---|---|---|---|
hec_url |
config | Yes | Your HTTP Event Collector URL, for example https://http-inputs-acme.splunkcloud.com. Truto posts to /services/collector/event; you can include that path or leave it off. Console label: HEC URL. |
index |
config | No | Splunk index. Leave it empty to use the token's default index. If you set it, the token must allow that index. |
source |
config | No | Defaults to truto. |
sourcetype |
config | No | Console label: Source type. Defaults to _json. |
token |
secrets | Yes | The HEC token. Console label: HEC token. |
Turn indexer acknowledgment off on the HEC token: Truto doesn't send a channel header, and Splunk answers Data channel is missing without one. The HEC endpoint must present a publicly trusted TLS certificate.
Each log is one HEC event: time (epoch seconds), host (truto-api), source, sourcetype, index when set, and event, which holds the log record.
Microsoft Sentinel
Truto sends logs through the Azure Monitor Logs Ingestion API. In Azure you need a Log Analytics workspace with a custom table, a data collection endpoint, a data collection rule whose input stream routes to that table, and an Entra app registration with a client secret that holds the Monitoring Metrics Publisher role on the rule.
| Field | Where | Required | Meaning |
|---|---|---|---|
tenant_id |
config | Yes | Microsoft Entra tenant ID. Console label: Tenant ID. |
client_id |
config | Yes | App registration (client) ID. Console label: Client ID. |
dce_endpoint |
config | Yes | Data collection endpoint URL, for example https://my-dce.eastus-1.ingest.monitor.azure.com. Console label: Data collection endpoint. |
dcr_immutable_id |
config | Yes | The rule's immutable ID, for example dcr-0123456789abcdef. Console label: DCR immutable ID. |
stream_name |
config | Yes | The rule's input stream, for example Custom-TrutoLogs_CL. Console label: Stream name. |
cloud |
config | No | public (default), usgov, or china. Console label: Azure cloud. |
client_secret |
secrets | Yes | The app registration's client secret. Console label: Client secret. |
Declare these columns on the stream:
| Column | Type |
|---|---|
TimeGenerated |
datetime |
LogType |
string |
AppEnv |
string |
Service |
string |
EnvironmentId |
string |
IntegratedAccountId |
string |
Message |
string |
HttpMethod |
string |
HttpUrl |
string |
HttpStatusCode |
int |
Duration |
real |
RawData |
dynamic |
RawData holds the full log record. Azure role assignments can take several minutes to apply; until they do, ingestion answers 403 and those logs are not retried.
OpenTelemetry (OTLP)
| Field | Where | Required | Meaning |
|---|---|---|---|
endpoint |
config | Yes | Your collector's OTLP/HTTP receiver, for example https://otel.example.com. Truto appends /v1/logs unless the URL already ends with it. Console label: OTLP/HTTP endpoint. |
resource_attributes |
config | No | String key-value pairs added to the resource of every log, such as {"deployment.region": "eu"}. They override the defaults. In the console, enter one key=value per line under Resource attributes. |
token |
secrets | No | Sent as Authorization: Bearer <token>. |
username, password |
secrets | No | Sent as basic auth. Provide both or neither. |
In the console, pick None, Bearer token, or Basic (username and password) under Authentication.
Truto sends OTLP/HTTP with JSON encoding; gRPC is not supported. The resource carries service.name=truto-api and deployment.environment. Each log is one log record: the body is the log's message, severity comes from the level in the message (falling back to the HTTP status: 5xx is ERROR, 4xx is WARN), and the remaining fields are attributes, including log_type. The HTTP fields are also added under their semantic-convention names: http.request.method, http.response.status_code, url.full, user_agent.original, and client.address.
Elasticsearch
| Field | Where | Required | Meaning |
|---|---|---|---|
url |
config | Yes | Your Elasticsearch endpoint, for example https://my-deployment.es.us-east-1.aws.elastic.cloud. No query string or fragment. Console label: Elasticsearch URL. |
index |
config | No | Index or data stream name: lowercase, starting with a letter or digit. Defaults to the logs-truto-default data stream. Console label: Index or data stream. |
pipeline |
config | No | An ingest pipeline to run on every document. Console label: Ingest pipeline. |
api_key |
secrets | One of | The encoded API key from Kibana (Stack Management → API keys). Sent as Authorization: ApiKey <api_key>. |
username, password |
secrets | One of | Basic auth. Provide both. |
Truto writes with the _bulk API using create actions, which work for data streams and plain indices alike. The key needs the create_doc and auto_configure privileges on the index.
Each log is an ECS-shaped document: @timestamp, message, log.level, service.name, service.environment, and where present http.request.method, http.response.status_code, url.full, user_agent.original, and client.address. Every Truto field sits under truto (including truto.log_type), with nested values stored as JSON strings so they don't grow your mapping.
Amazon S3, Google Cloud Storage and Cloudflare R2
| Field | Where | Required | Meaning |
|---|---|---|---|
bucket |
config | Yes | Bucket name. Console label: Bucket. |
region |
config | For Amazon S3 | An AWS region such as us-east-1. Leave it empty for GCS or R2. Console label: Region. |
endpoint |
config | For GCS and R2 | Leave it empty for Amazon S3. Use https://storage.googleapis.com for GCS or https://<account_id>.r2.cloudflarestorage.com for R2. Console label: S3-compatible endpoint. |
prefix |
config | No | Key prefix. Letters, digits and - _ . = / only, up to 512 characters. Console label: Key prefix. |
access_key_id |
secrets | Yes | Access key ID. For GCS, an HMAC key from Cloud Storage → Settings → Interoperability. |
secret_access_key |
secrets | Yes | Secret access key. |
The credentials only need permission to put objects: s3:PutObject on arn:aws:s3:::<bucket>/<prefix>* for AWS, or roles/storage.objectCreator on the bucket for GCS.
Each batch is written as one gzipped NDJSON object, partitioned by upload time in UTC:
<prefix>/year=YYYY/month=MM/day=DD/hour=HH/<timestamp>-<uuid>.ndjson.gz
Each line is one log record plus log_type and app_env. Objects are stored as application/gzip, and existing objects are never overwritten.
Log types
config.log_types picks which logs a destination receives. Leave it out to receive all four. An empty list [] forwards nothing.
| Value | Console label | What it covers |
|---|---|---|
api |
API | One record per request to the Truto API: Unified, Proxy and Custom API calls, admin API calls, MCP server calls, and webhooks that integrations send to Truto. Carries the HTTP fields, duration, request_type, and the integrated account, integration and resource involved. |
durable_object |
Durable Object | Long-running work tied to one record: RapidBridge sync job runs, webhook deliveries to your endpoints, processing of incoming integration webhooks, workflow runs, scheduled actions, and connection flows. The durable_object field names which. Sync job runs carry sync_job_id, sync_job_run_id, and sync_job_run_event; webhook deliveries carry webhook_id, webhook_endpoint_status, and webhook_successful. |
queue |
Queue | Background events that start sync job runs, workflows and batch jobs, and fan out webhook events. Carries queue and event. |
alarm |
Alarm | Scheduled timers firing: sync job and daemon job cron triggers, scheduled actions, credential refreshes, and expiring tokens. Carries alarm_type and entity_id. |
What a forwarded log looks like
Every record has these fields. Fields with no value are left out.
| Field | Meaning |
|---|---|
timestamp |
ISO 8601 time the log was written. |
environment_id |
Your Truto environment. |
service |
Always truto-api. |
message |
One-line summary. For api logs: time, level (INFO, WARN for 4xx, ERROR for 5xx), client IP, request ID, method, URL and status. |
logs |
Log lines written while handling the event. Each string is cut to 200 characters. |
log_type |
api, durable_object, queue, or alarm. Added by every destination type, in the form that destination expects. |
app_env |
The Truto deployment that wrote the log. |
api records add host, http_url, http_method, http_status_code, http_useragent, http_referer, http_version, http_ip, http_request_id, http_country, the http_url_details_* fields, response_headers, duration (milliseconds), request_type (unified, proxy, mcp, admin), integrated_account_id, integration, resource, method, and tenant_id, among others. MCP calls also carry mcp_server_id, mcp_method, and tool_name.
A Unified API call, as written to S3 or inside a Splunk event:
{
"timestamp": "2026-11-02T10:15:42.120Z",
"environment_id": "ac15abdc-b38e-47d0-97a2-69194017c199",
"app_env": "production",
"service": "truto-api",
"message": "2026-11-02T10:15:42.120Z INFO 203.0.113.7 (8d2f9b1c4e7a1234) GET https://api.truto.one/unified/crm/contacts?integrated_account_id=0f4c1a2e-7d3b-4f8e-9a61-5c2d8e9b7f10 200",
"host": "api.truto.one",
"http_method": "GET",
"http_url": "https://api.truto.one/unified/crm/contacts?integrated_account_id=0f4c1a2e-7d3b-4f8e-9a61-5c2d8e9b7f10",
"http_status_code": 200,
"duration": 412,
"request_type": "unified",
"integrated_account_id": "0f4c1a2e-7d3b-4f8e-9a61-5c2d8e9b7f10",
"integration": "hubspot",
"resource": "contacts",
"log_type": "api"
}Datadog, Sentinel, OTLP and Elasticsearch reshape the same fields as described under each type in Destination reference.
Delivery
Truto sends logs in batches. When a destination answers 429, a 5xx, or can't be reached, the batch is retried up to five times, waiting for the Retry-After value or 60 seconds. Any other error, such as 400, 401 or 403, drops that batch. A retry can resend logs the destination already accepted, so deduplicate on your side if exact counts matter. In S3, a retry writes a new object rather than overwriting one.
To pause forwarding, turn Active off or send "is_active": false.
Changing a destination
PATCH /observability-destination/:id updates a destination. In the console, click the destination's name, then Change next to the stored credentials to replace them.
configreplaces the stored config whole, so send every field the type needs. For Datadog, that meanssiteeven when you only changetags.secretsreplaces the stored secrets whole. Leave it out to keep them.- If you change the host of
hec_url,dce_endpoint, the OTLPendpoint, or the Elasticsearchurl, you must sendsecretsin the same request. Truto won't send stored credentials to a new host. - Changing
typerequires bothconfigandsecrets.
DELETE /observability-destination/:id deletes a destination. GET /observability-destination lists them and GET /observability-destination/:id reads one.
truto observability-destinations update <destination_id> \
--config '{"site":"eu","log_types":["api"]}'curl -X PATCH https://api.truto.one/observability-destination/<destination_id> \
-H "Authorization: Bearer <API token>" \
-H "Content-Type: application/json" \
-d '{ "config": { "site": "eu", "log_types": ["api"] } }'await truto_api.observability_destinations.update(
"<destination_id>",
{"config": {"site": "eu", "log_types": ["api"]}},
)Next
The same logs are searchable in the console; see Viewing logs. To get Slack or email alerts about API errors and API tokens instead of raw logs, set up Notifications. Every endpoint is in the observability destinations API reference.