I Tried Turning Logs Insights Query Results into Alarms with CloudWatch's New "Log Based Alarm" Feature

I Tried Turning Logs Insights Query Results into Alarms with CloudWatch's New "Log Based Alarm" Feature

I tried CloudWatch's new Log Based Alarm to turn Logs Insights query results into alarms, covering creation via the PutLogAlarm API, notifications with log lines (ActionLogLineCount), EventBridge integration, and ratio alarms using if/case.
2026.06.30

This page has been translated by machine translation. View original

Introduction

On June 29, 2026, a new alarm type, "Log Based Alarm", was added to CloudWatch. The AWS CLI v2 Changelog contains the following entry.

2.35.12

api-change:cloudwatch: This release adds the API (PutLogAlarm) to manage a new CloudWatch resource, Log Based Alarms. Log Based Alarms allows customers to alarm directly on CloudWatch Logs query results.

https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst

The CLI reference for PutLogAlarm is published at the following URL.

https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/put-log-alarm.html

Previously, creating an alarm based on the contents of CloudWatch Logs required creating a custom metric via a metric filter. With Log Based Alarm this is no longer necessary: you can aggregate the results extracted and processed by a Logs Insights query with AggregationExpression and use that value directly for alarm evaluation. The main differences from the conventional approach are as follows.

Item Conventional (metric filter + MetricAlarm) New (Log Based Alarm)
Setup steps Create metric filter → Verify metric → Create alarm Completed with a single PutLogAlarm API call
Query flexibility Fixed pattern matching only Filtering and field processing via Logs Insights queries
Log lines in notifications Not possible Up to 50 lines with ActionLogLineCount
Logs Insights link None Notification includes a direct link to the query results
Evaluation method EvaluationPeriods / DatapointsToAlarm QueryResultsToEvaluate / QueryResultsToAlarm
Chatbot support ○ × (as of 2026-06-30)
Cost Metric filter: free / Custom metric: $0.30/month / Alarm: $0.10/month Alarm: $0.10/month + Logs Insights query: $0.005/GB scanned

https://dev.classmethod.jp/articles/cloudwatch-log-based-alarm-multi-contributor-by-clause/

How Log Based Alarm Works

Log Based Alarm operates as a pipeline of "periodic query execution → result aggregation → threshold evaluation".

Scheduled Query

When you run PutLogAlarm, a Scheduled Query is automatically created in CloudWatch Logs. The Logs Insights query is executed periodically on the specified schedule (e.g., rate(5 minutes)), and the results are used for alarm evaluation.

The time range targeted by the query is controlled with StartTimeOffset and EndTimeOffset.

AggregationExpression

This is an expression that aggregates the query results into a single numeric value. You specify one of the functions count(*), sum(fieldName), avg(fieldName), min(fieldName), or max(fieldName). This aggregated result is compared against the threshold.

M-of-N Evaluation

The evaluation logic is controlled by QueryResultsToEvaluate (evaluates the most recent N query results) and QueryResultsToAlarm (the alarm goes to ALARM when M of them meet the threshold condition).

Verification: Creating the Alarm

Let's actually create an alarm with PutLogAlarm.

Preparing the IAM Role

Log Based Alarm requires an IAM role for running the scheduled query. It must trust both logs.amazonaws.com and cloudwatch.amazonaws.com.

aws iam create-role \
  --role-name "log-alarm-query-role" \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {"Service": "logs.amazonaws.com"},
        "Action": "sts:AssumeRole"
      },
      {
        "Effect": "Allow",
        "Principal": {"Service": "cloudwatch.amazonaws.com"},
        "Action": "sts:AssumeRole"
      }
    ]
  }'

Grant permissions to query the target log group with an inline policy.

aws iam put-role-policy \
  --role-name "log-alarm-query-role" \
  --policy-name "LogAlarmQueryPermissions" \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "logs:StartQuery",
          "logs:GetQueryResults",
          "logs:StopQuery",
          "logs:FilterLogEvents",
          "logs:GetLogEvents"
        ],
        "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/ecs/your-app:*"
      }
    ]
  }'

Running PutLogAlarm

aws cloudwatch put-log-alarm \
  --alarm-name "timeout-log-alarm" \
  --alarm-description "ECS timeout errors - 5min aggregation, threshold 20" \
  --scheduled-query-configuration '{
    "QueryString": "filter @message like /timeout request to/",
    "LogGroupIdentifiers": ["/aws/ecs/your-app"],
    "ScheduledQueryRoleARN": "arn:aws:iam::123456789012:role/log-alarm-query-role",
    "ScheduleConfiguration": {
      "ScheduleExpression": "rate(5 minutes)",
      "StartTimeOffset": 360,
      "EndTimeOffset": 60
    },
    "AggregationExpression": "count(*)"
  }' \
  --action-log-line-count 10 \
  --action-log-line-role-arn "arn:aws:iam::123456789012:role/log-alarm-query-role" \
  --actions-enabled \
  --alarm-actions "arn:aws:sns:ap-northeast-1:123456789012:your-alarm-topic" \
  --ok-actions "arn:aws:sns:ap-northeast-1:123456789012:your-alarm-topic" \
  --query-results-to-evaluate 1 \
  --query-results-to-alarm 1 \
  --threshold 20.0 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "notBreaching" \
  --region ap-northeast-1

Key parameters:

  • QueryString: The Logs Insights query. Describe log filtering and field definitions (the final numeric aggregation is specified in AggregationExpression)
  • LogGroupIdentifiers: The log groups to query
  • ScheduleExpression: The query execution interval
  • StartTimeOffset / EndTimeOffset: Specify the query time range as seconds relative to the execution time. Since 360 / 60 are specified here, the 5-minute window from 6 minutes before to 1 minute before the execution time is evaluated
  • AggregationExpression: The aggregation function for the query results
  • ActionLogLineCount: The number of log lines to include in the alarm notification (max 50)
  • QueryResultsToEvaluate / QueryResultsToAlarm: N and M of the M-of-N evaluation

Confirming Automatic Creation of the Scheduled Query

After creation, you can check the ARN of the Scheduled Query with describe-alarms.

aws cloudwatch describe-alarms \
  --alarm-names "timeout-log-alarm" \
  --region ap-northeast-1
Response (LogAlarms section)
{
  "LogAlarms": [
    {
      "AlarmName": "timeout-log-alarm",
      "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:timeout-log-alarm",
      "AlarmDescription": "ECS timeout errors - 5min aggregation, threshold 20",
      "ActionsEnabled": true,
      "OKActions": ["arn:aws:sns:ap-northeast-1:123456789012:your-alarm-topic"],
      "AlarmActions": ["arn:aws:sns:ap-northeast-1:123456789012:your-alarm-topic"],
      "InsufficientDataActions": [],
      "StateValue": "INSUFFICIENT_DATA",
      "ScheduledQueryConfiguration": {
        "QueryString": "filter @message like /timeout request to/",
        "LogGroupIdentifiers": ["/aws/ecs/your-app"],
        "QueryARN": "arn:aws:logs:ap-northeast-1:123456789012:scheduled-query:e026155f-ae01-4d43-bde5-024d4ea39f45",
        "ScheduledQueryRoleARN": "arn:aws:iam::123456789012:role/log-alarm-query-role",
        "ScheduleConfiguration": {
          "ScheduleExpression": "rate(5 minutes)",
          "StartTimeOffset": 360,
          "EndTimeOffset": 60
        },
        "AggregationExpression": "count(*)"
      },
      "QueryResultsToEvaluate": 1,
      "QueryResultsToAlarm": 1,
      "Threshold": 20.0,
      "ComparisonOperator": "GreaterThanThreshold",
      "TreatMissingData": "notBreaching",
      "ActionLogLineCount": 10,
      "ActionLogLineRoleArn": "arn:aws:iam::123456789012:role/log-alarm-query-role"
    }
  ]
}

The describe-alarms response returns these under a LogAlarms key, separate from the conventional MetricAlarms.

Verification: Alarm Firing and Notification Check

When the scheduled query runs, a Logs Insights query is executed against the target log group. In this verification, 34 timeout logs that actually exist in the target log group matched. Since this exceeded the threshold of 20, the alarm transitioned to ALARM.

StateReason: "Threshold Crossed: 1 out of the last 1 query results [34.0 (30/06/26 11:43:54)]
was greater than the threshold (20.0)
(minimum 1 datapoint for OK -> ALARM transition)."

SNS Notification Contents

When the ALARM fires, the SNS message includes the following fields that the conventional MetricAlarm does not have.

{
  "AlarmName": "timeout-log-alarm",
  "NewStateValue": "ALARM",
  "NewStateReason": "Threshold Crossed: 1 out of the last 1 query results [34.0 (30/06/26 11:43:54)] was greater than the threshold (20.0) ...",
  "LogGroups": ["/aws/ecs/your-app"],
  "QueryString": "filter @message like /timeout request to/",
  "AggregationExpression": "count(*)",
  "QueryExecutionId": "627920a2-af1e-4d79-a866-04488dbebd55"
}

The Trigger field found in conventional MetricAlarm messages is not included, and Log Alarm-specific fields are added instead.

Including Log Lines with ActionLogLineCount

The log lines included in the email notification are as follows.

Log Lines:
2026-06-30 11:41:59.111 timeout request to https://example.cloudfront.net/spaces/.../entries?...
2026-06-30 11:38:44.108 timeout request to https://example.io/api/stats/user-30.json.gz
2026-06-30 11:38:44.008 timeout request to https://example.io/api/stats/user-29.json.gz
2026-06-30 11:38:43.908 timeout request to https://example.io/api/stats/user-28.json.gz
2026-06-30 11:38:43.808 timeout request to https://example.io/api/stats/user-27.json.gz
2026-06-30 11:38:43.708 timeout request to https://example.io/api/stats/user-26.json.gz
2026-06-30 11:38:43.608 timeout request to https://example.io/api/stats/user-25.json.gz
2026-06-30 11:38:43.508 timeout request to https://example.io/api/stats/user-24.json.gz
2026-06-30 11:38:43.408 timeout request to https://example.io/api/stats/user-23.json.gz
2026-06-30 11:38:43.308 timeout request to https://example.io/api/stats/user-22.json.gz

The email notification also includes a direct link to the Logs Insights query results.

View query results in Logs Insights:
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:logs-insights$3FqueryId$3D627920a2-af1e-4d79-a866-04488dbebd55

Verification: EventBridge Integration

State changes of a Log Based Alarm can also be captured with EventBridge. The event pattern is CloudWatch Alarm State Change, the same as a regular CloudWatch Alarm.

aws events put-rule \
  --name "log-alarm-state-change-capture" \
  --event-pattern '{
    "source": ["aws.cloudwatch"],
    "detail-type": ["CloudWatch Alarm State Change"],
    "resources": ["arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:timeout-log-alarm"]
  }' \
  --state ENABLED \
  --region ap-northeast-1

The structure of the captured event is as follows.

EventBridge event (OK → ALARM transition)
{
  "version": "0",
  "id": "a644916b-0f3e-7687-4a60-3e0b6b639f81",
  "detail-type": "CloudWatch Alarm State Change",
  "source": "aws.cloudwatch",
  "time": "2026-06-30T12:13:00Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:timeout-log-alarm"
  ],
  "detail": {
    "alarmName": "timeout-log-alarm",
    "state": {
      "value": "ALARM",
      "reason": "Threshold Crossed: 1 out of the last 1 query results [34.0] was greater than the threshold (20.0)...",
      "timestamp": "2026-06-30T12:13:00.795+0000"
    },
    "previousState": {
      "value": "OK",
      "reason": "...",
      "timestamp": "2026-06-30T12:12:51.478+0000"
    },
    "configuration": {
      "logGroupIdentifiers": ["/aws/ecs/your-app"],
      "queryString": "filter @message like /timeout request to/",
      "aggregationExpression": "count(*)",
      "scheduledQueryRoleARN": "arn:aws:iam::123456789012:role/log-alarm-query-role",
      "actionLogLineRoleArn": "arn:aws:iam::123456789012:role/log-alarm-query-role",
      "actionLogLineCount": 10,
      "schedule": {
        "expression": "rate(5 minutes)",
        "startTimeOffset": 360,
        "endTimeOffset": 60
      },
      "threshold": 20.0,
      "comparisonOperator": "GreaterThanThreshold",
      "treatMissingData": "notBreaching",
      "queryResultsToEvaluate": 1,
      "queryResultsToAlarm": 1
    }
  }
}

detail.configuration contains Log Alarm-specific fields (logGroupIdentifiers, queryString, aggregationExpression, schedule, etc.).

Advanced: Combining with Logs Insights Functions

The AggregationExpression of a Log Based Alarm can reference numeric fields defined in the QueryString using fields or parse, with sum(), avg(), min(), and max(). When numerically aggregating a value extracted as a string with parse, convert it with toNumber(). Combined with the new Logs Insights functions added in May–June 2026 (if, toNumber, isPrivateIP, etc.), you can create alarms based not only on simple counts but also on ratios and weighted scores.

https://dev.classmethod.jp/articles/cloudwatch-logs-insights-new-commands-functions-2026/

https://dev.classmethod.jp/articles/cloudwatch-logs-insights-new-commands-functions-2026-june/

Ratio Alarm: Fire When the Timeout Ratio Exceeds the Threshold

Use the if function to flag matching lines as 1 and all others as 0, then calculate the ratio with AVG.

aws cloudwatch put-log-alarm \
  --alarm-name "timeout-ratio-alarm" \
  --alarm-description "Timeout ratio exceeds 50% of all log lines" \
  --scheduled-query-configuration '{
    "QueryString": "fields if(@message like /timeout request to/, 1, 0) as is_timeout",
    "LogGroupIdentifiers": ["/aws/ecs/your-app"],
    "ScheduledQueryRoleARN": "arn:aws:iam::123456789012:role/log-alarm-query-role",
    "ScheduleConfiguration": {
      "ScheduleExpression": "rate(5 minutes)",
      "StartTimeOffset": 360,
      "EndTimeOffset": 60
    },
    "AggregationExpression": "avg(is_timeout)"
  }' \
  --actions-enabled \
  --query-results-to-evaluate 1 \
  --query-results-to-alarm 1 \
  --threshold 0.5 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "notBreaching" \
  --region ap-northeast-1

The evaluation result of the alarm I actually created was as follows.

StateReason: "Threshold Crossed: 1 out of the last 1 query results [0.03333333333333333 (30/06/26 12:58:25)]
was not greater than the threshold (0.5)
(minimum 1 datapoint for INSUFFICIENT_DATA -> OK transition)."

Of the query result rows in the evaluation period, the timeout ratio was calculated at about 3.3%, which is below the 50% threshold, so the state is OK. This confirmed that avg(is_timeout) averages the is_timeout field (0 or 1) of each query result row and evaluates it as a ratio.

Weighted Score: Alarm Based on Severity

The case function (added in May 2026) enables multi-branch scoring. It is more readable than nesting if.

aws cloudwatch put-log-alarm \
  --alarm-name "severity-score-alarm" \
  --alarm-description "Weighted severity score exceeds threshold" \
  --scheduled-query-configuration '{
    "QueryString": "fields case(@message like /timeout/, 2, @message like /error/, 1, 0) as severity_score",
    "LogGroupIdentifiers": ["/aws/ecs/your-app"],
    "ScheduledQueryRoleARN": "arn:aws:iam::123456789012:role/log-alarm-query-role",
    "ScheduleConfiguration": {
      "ScheduleExpression": "rate(5 minutes)",
      "StartTimeOffset": 360,
      "EndTimeOffset": 60
    },
    "AggregationExpression": "avg(severity_score)"
  }' \
  --actions-enabled \
  --query-results-to-evaluate 1 \
  --query-results-to-alarm 1 \
  --threshold 1.0 \
  --comparison-operator "GreaterThanThreshold" \
  --treat-missing-data "notBreaching" \
  --region ap-northeast-1

With the syntax case(condition1, value1, condition2, value2, ..., default value), you can specify up to 10 branches.

Other Combination Patterns

Use case QueryString AggregationExpression
Average response time parse @message /took (?<ms>\d+)ms/ | fields toNumber(ms) as rt avg(rt)
Ratio of slow requests fields if(toNumber(response_time) > 1000, 1, 0) as is_slow avg(is_slow)
Ratio of external IP access fields if(isPrivateIP(src_ip), 0, 1) as is_external avg(is_external)
Detecting huge log lines fields if(strlen(@message) > 10000, 1, 0) as is_oversized sum(is_oversized)

The key point is to define fields with fields in the QueryString rather than using stats. Result fields aggregated with stats cannot be referenced from AggregationExpression.

Notes

AggregationExpression Constraints

Specify an aggregation function directly in AggregationExpression. The available functions are count(*), sum(fieldName), avg(fieldName), min(fieldName), and max(fieldName).

Fields defined with fields or parse can be referenced, but fields aggregated with stats in the QueryString cannot be referenced.

# NG: Include stats in QueryString and reference the field in AggregationExpression
QueryString: "filter @message like /error/ | stats count(*) as errorCount"
AggregationExpression: "errorCount"

# OK: QueryString only filters; aggregation is done in AggregationExpression
QueryString: "filter @message like /error/"
AggregationExpression: "count(*)"

# OK: Aggregate the field defined with fields in AggregationExpression
QueryString: "fields if(@message like /error/, 1, 0) as is_error"
AggregationExpression: "avg(is_error)"

AWS Chatbot Not Supported (as of 2026-06-30)

Because the SNS message of Log Based Alarm has a different format from the conventional MetricAlarm (as described above, it has no Trigger field and is composed of Log Alarm-specific fields), AWS Chatbot may not support it. In my verification, the SNS Publish succeeded, but the notification did not appear in Slack.

If you want to send notifications to Slack via Chatbot, try a setup where EventBridge → Lambda transforms the message into a custom notification format and publishes it to SNS.

https://dev.classmethod.jp/articles/sns-amazon-q-developer-aws-chatbot/

Resource-Based Policy for EventBridge Targets

When specifying CloudWatch Logs as an EventBridge target, the log group needs a resource-based policy. Allow logs:CreateLogStream and logs:PutLogEvents for events.amazonaws.com.

Summary

Log Based Alarm greatly simplifies monitoring configuration for CloudWatch Logs. Without creating a metric filter, you can combine filtering and field processing via Logs Insights queries with aggregation via AggregationExpression to treat log search results directly as alarm conditions.

In this verification, I confirmed that by using functions such as if / case / toNumber / isPrivateIP, you can express not only simple count thresholds but also ratio- and weighted-value-based alarms such as timeout rate, severity score, and average response time. Setups that previously combined metric filters, custom metrics, and MetricAlarms, or used Lambda for condition evaluation, can now be achieved with fewer settings.

On the notification side, it is convenient that ActionLogLineCount lets you include matched log lines and that a direct link to the Logs Insights query results is included. EventBridge integration also worked with the same event pattern as conventional alarms.

At present there are limitations with direct notification to AWS Chatbot, and there are points to watch out for in how to use AggregationExpression, but it looks like a strong option when you want to simply create alarms based on log contents.

Share this article

AWSのお困り事はクラスメソッドへ