Chapter 45 · Amazon Opensearch Service
Subchapter 45.13
references/log-analytics-guide.mdMarkdown16 KBView on GitHub
This file is the entry point for the log-analytics capability. It covers log search at scale, observability, PPL queries, anomaly detection, OpenSearch Dashboards, alerting, and SIEM patterns — including replatforming from Splunk, Datadog, or self-managed ELK.
SKILL.md routes here when the user is doing log analytics or observability on AOS / AOSS. Concrete triggers:
| User need | File |
|---|---|
| Full log analytics workflow | this file |
| Set up OSI ingestion pipelines | log-analytics-osi-pipelines.md |
| Replatform from Splunk / Datadog / ELK | observability.md |
| Troubleshoot ingestion or query issues | log-analytics-troubleshooting.md |
Cross-cutting refs you may also load: observability.md (ISM / UltraWarm / Cold tiering details), security.md, personas.md.
provisioning-reference.md.assessment-workflow.md (use the Splunk-replatform shape).trace-analytics-trace-queries.md.This guide instructs you on how to perform log analytics against an existing OpenSearch domain or collection. The approach is discovery-first: understand what indices exist, learn the schema, sample the data, then build queries. Do not assume any particular index pattern or field names — discover them.
Use awscurl for SigV4-authenticated HTTP requests to AOS/AOSS endpoints.
pip install awscurl| Variable | Example | Description |
|---|---|---|
OPENSEARCH_ENDPOINT | https://my-domain.us-east-1.es.amazonaws.com | AOS domain or AOSS collection endpoint |
AWS_REGION | us-east-1 | AWS region |
AWS_PROFILE | default | AWS CLI profile (optional) |
AOS (managed domains):
awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query": "<PPL_QUERY>"}'AOSS (serverless collections):
awscurl --service aoss --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query": "<PPL_QUERY>"}'Use
--service esfor AOS domains,--service aossfor AOSS collections.
Determine the domain or collection type and endpoint using the AWS CLI (or call_aws if the AWS MCP server is available):
aws opensearch describe-domain --domain-name <name> → extract Endpoint and ARN (region from ARN). With AWS MCP: call_aws opensearch describe-domain.aws opensearchserverless batch-get-collection --names <name> → extract collectionEndpoint. With AWS MCP: call_aws opensearchserverless batch-get-collection.aws opensearch list-domain-names or aws opensearchserverless list-collectionsThis is important because the connection method, authentication, and available features differ between AOS domains and AOSS collections.
AOSS Note: OpenSearch Serverless does not support
_catAPIs. Use--service aossinstead of--service esfor all AOSS requests. For index discovery on AOSS, use PPL:source = * | stats count() by index.
Before writing any query, find out what log indices exist on the domain or collection.
awscurl --service es --region $AWS_REGION \
"$OPENSEARCH_ENDPOINT/_cat/indices?format=json&h=index,health,docs.count,store.size&s=docs.count:desc"Look for indices that suggest logs: names containing log, logs, events, audit, access, syslog, otel, cwl (CloudWatch Logs), or date-based patterns like logs-2024.01.15.
awscurl --service es --region $AWS_REGION \
"$OPENSEARCH_ENDPOINT/_cat/aliases?format=json&h=alias,index&s=alias"awscurl --service es --region $AWS_REGION \
"$OPENSEARCH_ENDPOINT/_data_stream"After discovering indices, ask the user which index or index pattern they want to analyze if it’s not obvious. If there are multiple log indices, ask about the relationship between them (e.g., are they daily rollover indices for the same data? different applications? different log levels?).
Once you know the target index pattern, inspect its mapping to learn the available fields.
awscurl --service es --region $AWS_REGION \
"$OPENSEARCH_ENDPOINT/<INDEX_PATTERN>/_mapping"Via PPL:
awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query": "describe <INDEX_NAME>"}'Use a concrete index name (e.g.,
logs-2024.01.15) fordescribe, not a wildcard pattern.
From the mapping, identify:
@timestamp, timestamp, time, or event.createdlevel, log.level, severity, severityText, loglevelmessage, msg, body, log, event.originalservice, service.name, host.name, source, kubernetes.pod.name, resource.attributes.service.nameerror.message, error.stack_trace, exception.typetraceId, trace_id, spanId, request_id, correlation_idIf the mapping is large or unclear, ask the user: “I see fields like X, Y, Z — which field contains the log message? Which one is the log level?”
Always look at a few real documents to understand the actual data shape — mappings alone can be misleading (e.g., dynamic fields, nested objects, multi-value fields):
Via PPL:
awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query": "source=<INDEX_PATTERN> | head 5"}'Review the sample documents to confirm:
If the schema is not self-explanatory, ask the user:
trace_id field — do you want to correlate logs with traces?”Do not skip this step if the data is ambiguous. Getting the schema right upfront saves failed queries later.
With the schema understood, build PPL queries using the actual field names discovered above. All examples below use placeholder field names — substitute with the real ones.
awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query": "<PPL_QUERY>"}'For AOSS, use
--service aossinstead of--service es.
source=<INDEX_PATTERN> | stats count() as volume by span(<TIMESTAMP_FIELD>, 1h)source=<INDEX_PATTERN> | where <LEVEL_FIELD> = 'ERROR' | stats count() as errors by <SERVICE_FIELD> | sort - errorssource=<INDEX_PATTERN> | stats count() as total, sum(case(<LEVEL_FIELD> = 'ERROR', 1 else 0)) as errors by span(<TIMESTAMP_FIELD>, 1h)source=<INDEX_PATTERN> | where <LEVEL_FIELD> = 'ERROR' | fields <TIMESTAMP_FIELD>, <SERVICE_FIELD>, <MESSAGE_FIELD> | sort - <TIMESTAMP_FIELD> | head 20source=<INDEX_PATTERN> | where match(<MESSAGE_FIELD>, 'connection timeout') | sort - <TIMESTAMP_FIELD> | head 20source=<INDEX_PATTERN> | where <LEVEL_FIELD> = 'ERROR' | top 10 <MESSAGE_FIELD>source=<INDEX_PATTERN> | where <LEVEL_FIELD> = 'ERROR' | rare <MESSAGE_FIELD>Automatically cluster similar log messages:
source=<INDEX_PATTERN> | where <LEVEL_FIELD> = 'ERROR' | patterns <MESSAGE_FIELD> | fields <MESSAGE_FIELD>, patterns_field | head 30source=<INDEX_PATTERN> | stats count() by <LEVEL_FIELD>, <SERVICE_FIELD>source=<INDEX_PATTERN> | where <TIMESTAMP_FIELD> > DATE_SUB(NOW(), INTERVAL 1 HOUR) | stats count() by <LEVEL_FIELD>source=<INDEX_PATTERN> | stats distinct_count(<SERVICE_FIELD>) as services, distinct_count(<HOST_FIELD>) as hostsIf logs contain a duration/latency field:
source=<INDEX_PATTERN> | stats avg(<DURATION_FIELD>) as avg_ms, percentile(<DURATION_FIELD>, 95) as p95_ms, percentile(<DURATION_FIELD>, 99) as p99_ms by <SERVICE_FIELD>If the message field contains unstructured text, use grok or parse to extract fields:
source=<INDEX_PATTERN> | grok <MESSAGE_FIELD> '%{IP:client_ip} %{WORD:method} %{URIPATHPARAM:path} %{NUMBER:status}' | stats count() by statusCaveat:
grokprocesses all matching rows in memory. Add| head Nbeforegrokon large indices to avoid resource errors.
If logs span multiple indices (e.g., application logs + access logs), correlate using shared fields like request_id, trace_id, or timestamp proximity:
Step 1 — Find an event of interest in one index:
source=<APP_LOGS> | where <LEVEL_FIELD> = 'ERROR' | fields <CORRELATION_FIELD>, <TIMESTAMP_FIELD>, <MESSAGE_FIELD> | head 10Step 2 — Look up the same correlation ID in the other index:
source=<ACCESS_LOGS> | where <CORRELATION_FIELD> = '<VALUE>' | fields <TIMESTAMP_FIELD>, <MESSAGE_FIELD>Use PPL’s built-in anomaly detection on numeric fields (e.g., log volume, error count):
source=<INDEX_PATTERN> | stats count() as volume by span(<TIMESTAMP_FIELD>, 5m) | ad time_field=<TIMESTAMP_FIELD>The
adcommand auto-detects input fields from the pipeline. It works best on time-series data with regular intervals.
For queries that PPL doesn’t support well (nested aggregations, scripted fields), fall back to Query DSL:
awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/<INDEX_PATTERN>/_search" \
-H 'Content-Type: application/json' \
-d '{
"size": 0,
"query": {
"bool": {
"must": [{"match": {"<LEVEL_FIELD>": "ERROR"}}],
"filter": [{"range": {"<TIMESTAMP_FIELD>": {"gte": "now-1h"}}}]
}
},
"aggs": {
"by_service": {
"terms": {"field": "<SERVICE_FIELD>", "size": 20},
"aggs": {
"over_time": {
"date_histogram": {"field": "<TIMESTAMP_FIELD>", "fixed_interval": "5m"}
}
}
}
}
}'When you encounter these common schemas, use the field mappings below:
Timestamp: @timestamp, Level: log.level, Message: message, Service: service.name, Host: host.name, Error: error.message
Timestamp: @timestamp, Level: severityText, Message: body, Service: `resource.attributes.service.name` (backtick-quoted), Trace: traceId, Span: spanId
Timestamp: timestamp or @timestamp, Level: level, Message: message or msg, Service: service, Host: host
Timestamp: @timestamp, Level: severity, Message: message, Host: host, Program: program, Facility: facility
Client: clientip, Request: request, Status: response, Bytes: bytes, Method: verb, Agent: agent
`log.level`, `host.name`head N before memory-intensive commands (grok, streamstats, eventstats)span(<timestamp>, <interval>) for time bucketing — common intervals: 5m, 15m, 1h, 1dmatch() for full-text search, like for wildcard patterns, match_phrase() for exact phrasespatterns for automatic log message clusteringdedup to find unique error messages: dedup <MESSAGE_FIELD> | fields <MESSAGE_FIELD>awscurl --service es --region $AWS_REGION \
-X PUT "$OPENSEARCH_ENDPOINT/application-logs" \
-H 'Content-Type: application/json' \
-d '{
"settings": {"number_of_shards": 1, "number_of_replicas": 1},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"level": {"type": "keyword"},
"message": {"type": "text"},
"service": {"type": "keyword"},
"trace_id": {"type": "keyword"}
}
}
}'awscurl --service es --region $AWS_REGION \
-X POST "$OPENSEARCH_ENDPOINT/_bulk" \
-H 'Content-Type: application/x-ndjson' \
-d '{"index": {"_index": "application-logs"}}
{"@timestamp": "2024-01-15T10:30:00Z", "level": "ERROR", "message": "Connection timeout to database", "service": "order-service"}
{"index": {"_index": "application-logs"}}
{"@timestamp": "2024-01-15T10:30:05Z", "level": "INFO", "message": "Retry succeeded", "service": "order-service"}
'awscurl --service es --region $AWS_REGION \
-X PUT "$OPENSEARCH_ENDPOINT/_plugins/_ism/policies/log-rotation-policy" \
-H 'Content-Type: application/json' \
-d '{
"policy": {
"description": "Hot-warm-delete lifecycle for logs",
"default_state": "hot",
"states": [
{"name": "hot", "actions": [], "transitions": [{"state_name": "warm", "conditions": {"min_index_age": "7d"}}]},
{"name": "warm", "actions": [{"read_only": {}}], "transitions": [{"state_name": "delete", "conditions": {"min_index_age": "30d"}}]},
{"name": "delete", "actions": [{"delete": {}}], "transitions": []}
],
"ism_template": [{"index_patterns": ["application-logs*"], "priority": 100}]
}
}'# Create index template for data stream
awscurl --service es --region $AWS_REGION \
-X PUT "$OPENSEARCH_ENDPOINT/_index_template/logs-template" \
-H 'Content-Type: application/json' \
-d '{
"index_patterns": ["logs-*"],
"data_stream": {},
"template": {
"settings": {"number_of_shards": 1},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"message": {"type": "text"},
"level": {"type": "keyword"}
}
}
}
}'
# Create the data stream
awscurl --service es --region $AWS_REGION \
-X PUT "$OPENSEARCH_ENDPOINT/_data_stream/logs-stream"This file