I Tested the 23 New Commands and Functions Added to CloudWatch Logs Insights (June 2026 Edition)

I Tested the 23 New Commands and Functions Added to CloudWatch Logs Insights (June 2026 Edition)

I tested 23 new commands and functions added to CloudWatch Logs Insights in June 2026, including hash, IP checks, type conversion, CSV/XML parsing, histogram and multi-stage stats, with observed behavior and caveats.
2026.06.10

This page has been translated by machine translation. View original

Introduction

On June 8, 2026, 23 new commands and functions were added to Amazon CloudWatch Logs Insights.

https://aws.amazon.com/jp/about-aws/whats-new/2026/06/amazon-cloudwatch-logs-insights-new/

The previous article introduced the 13 announced on May 21, but now, in less than a month, there is another major addition.

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

Announcement date Number added Main categories
5/21 13 String manipulation, encoding/decoding, logfmt parsing, coordinate calculation
6/8 23 Hash, IP classification, type conversion, time-series analysis, CSV/XML parsing, command extensions

Comparing the trends in the features added in May and June, the direction of the query language becomes visible.

May (13) June (23)
Character Log viewing and formatting tool Log analysis platform
Main task "Read and search" "Aggregate and classify"
What it replaced Visual inspection, manual decoding Post-processing with Athena/pandas/Splunk
Typical use cases Initial investigation during incidents Security audits, traffic analysis, SLO aggregation

While May's set was about "making logs easier to read," June's set can be described as "getting answers from logs."

The list of newly added features is as follows.

Category Items
Hash functions md5, sha256
String functions strcontains (case-insensitive support), split
Conditional logic if
Conversion functions toNumber, toInt, toLong, toDouble
IP functions ipv4ToNumber, isPrivateIP, isPublicIP, isReservedIP
Analytics functions rate, count_over_time, sum_over_time, offset, histogram
Parse functions parse CSV, parse XML, parse multi, values, addtotals
Others limit any N, up to 10 stages of the stats command
  • The official announcement states "23 new query commands and functions." The list above expands parse CSV/XML/multi and others individually, so the item count may differ. The official count groups syntax extensions and the like together, and this article uses the official "23" as is.

In this article, we will actually run these and check the results.

Test environment

  • Region: us-east-1
  • Log group: /test/insights-new-2026-06
  • Test data: JSON format, CSV format, XML format, time-series data. Fictitious user IDs, service names, and IP addresses are used (IPs consist of RFC 5737 TEST-NET, RFC 1918 private ranges, and well-known addresses such as 8.8.8.8)

Hash functions (md5, sha256)

md5 and sha256 are functions that generate hash values from field values. They can be used when you want to hash and display things like user IDs in logs.

fields user_id, md5(user_id) as md5_hash, sha256(user_id) as sha256_hash
| limit 3
user_id md5_hash sha256_hash
usr_010 2fb6c8adce410db19ed04a7157b1ebd0 34f0365ae65242b4664ad6a1e4fe941c77caf56d7bd5aca88f1e9c6927012207
usr_009 3b24508aecac732b2dbf6d4e4bf9c4c2 8c231f143bc453047665350be67bc92029fe34375ddd45bee71164fcb278315c
usr_008 136ea0f2a0158fcbbd873dead2d60963 a8d79cc679fa29d08b483641f5d4b01faea2e81aa44edc7684fc2b936f32437c

md5 returned a 128-bit string (32 hex characters), and sha256 returned a 256-bit string (64 hex characters).

String functions (strcontains, split)

split

split splits a string into an array using the specified delimiter.

fields tags, split(tags, ',') as tag_array
| limit 3
tags tag_array
prod,warning,us-east-1 ["prod","warning","us-east-1"]
prod,normal,ap-northeast-1 ["prod","normal","ap-northeast-1"]
prod,critical,ap-northeast-1 ["prod","critical","ap-northeast-1"]

The comma-separated tag strings were expanded into array format (["prod","warning","us-east-1"]).

strcontains

strcontains determines whether a string contains a specific substring. According to the documentation, specifying true as the third argument enables case-insensitive search.

fields service,
  strcontains(service, 'auth') as has_auth_lower,
  strcontains(service, 'AUTH') as has_AUTH_upper,
  strcontains(service, 'AUTH', true) as has_AUTH_ci
| filter ispresent(service)
| sort service
| limit 5
service has_auth_lower has_AUTH_upper has_AUTH_ci
api-gateway 0 0 0
auth-service 1 0 0
cdn-edge 0 0 0
data-pipeline 0 0 0
geo-service 0 0 0

The basic behavior (first and second arguments only) is normal. strcontains(service, 'auth') returns 1 for auth-service.

However, in the author's environment, I could not confirm the effect of the third argument true (case-insensitive mode). strcontains(service, 'AUTH', true) returned 0 for auth-service, and it appeared to behave in a case-sensitive manner. The argument itself was accepted without a syntax error, but specifying it did not change the result.

Conditional logic (if)

if is a function that returns a value based on a condition. With the syntax if(condition, value if true, value if false), it can be used like a ternary operator.

fields service, response_time,
  if(toNumber(response_time) > 1000, 'slow', 'fast') as speed
| limit 5
service response_time speed
queue-worker 2045 slow
search-service 156 fast
auth-service 8901 slow
notification 67 fast
geo-service 312 fast

Requests with response times exceeding 1000 ms were classified as slow.

As for how to choose between this and the case function added in May, if suits a single-condition binary choice, while case suits multiple branches.

Conversion functions (toNumber, toInt, toLong, toDouble)

Four functions for converting string fields to numeric types were added. Let's check the differences between each type conversion.

Converting integer values

fields response_time,
  toInt(response_time) as rt_int,
  toLong(response_time) as rt_long,
  toDouble(response_time) as rt_double,
  toNumber(response_time) as rt_number
| filter ispresent(response_time)
| limit 5
response_time rt_int rt_long rt_double rt_number
234 234 234 234 234
89 89 89 89 89
1523 1523 1523 1523 1523
5002 5002 5002 5002 5002
not_a_number (null) (null) (null) (null)

For integer values, all four functions gave the same result. When conversion fails (not_a_number), null is returned without an error.

Converting decimal values (type differences are pronounced)

parse @message /latency=(?<lat_val>[\d.]+)/
| display lat_val,
    toInt(lat_val) as lat_int,
    toLong(lat_val) as lat_long,
    toDouble(lat_val) as lat_double,
    toNumber(lat_val) as lat_number
| filter ispresent(lat_val)
lat_val lat_int lat_long lat_double lat_number
890.12 890 890 890.12 890.12
45.7 45 45 45.7 45.7
123.456 123 123 123.456 123.456

Differences appeared with values containing decimals.

  • toInt / toLong: Truncated the fractional part (only positive values were tested; the behavior of floor vs. truncate for negative numbers has not been confirmed)
  • toDouble / toNumber: Retained the fractional part, and gave the same results within the scope of this test
  • Judging from the type names, differences between toInt and toLong may appear with values exceeding the 32-bit integer range (about 2.1 billion), but no difference could be confirmed with this test data

As for usage, you would use toInt/toLong for aggregations where decimal precision is unnecessary, and toDouble/toNumber for calculations where you want to keep the decimals.

IP functions (ipv4ToNumber, isPrivateIP, isPublicIP, isReservedIP)

Four functions were added for numeric conversion and classification of IP addresses. They are useful for analyzing VPC flow logs and ALB access logs.

fields ip,
  ipv4ToNumber(ip) as ip_num,
  isPrivateIP(ip) as is_private,
  isPublicIP(ip) as is_public,
  isReservedIP(ip) as is_reserved
| limit 10
ip ip_num is_private is_public is_reserved
10.255.255.1 184549121 1 0 0
52.194.x.x 885131796 0 1 0
192.0.2.1 3221225985 0 0 1
169.254.169.254 2852039166 0 0 1
100.64.0.1 1681915905 0 0 1
8.8.8.8 134744072 0 1 0
172.16.0.10 2886729738 1 0 0
10.0.0.55 167772215 1 0 0
203.0.113.50 3405803826 0 0 1
192.168.1.100 3232235876 1 0 0
  • Some public IPs have their lower octets masked (the actual test used complete IP addresses).

For the IP addresses prepared this time, we confirmed that the expected classification results were obtained. The classification criteria that can be read from the results are as follows.

  • Private (isPrivateIP = 1): Private address ranges defined in RFC 1918
    • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Reserved (isReservedIP = 1): Address ranges reserved for special purposes
    • 169.254.0.0/16 (link-local), 100.64.0.0/10 (CGN / Shared Address)
    • 192.0.2.0/24 (TEST-NET-1), 203.0.113.0/24 (TEST-NET-3)
  • Public (isPublicIP = 1): Within the range tested, ordinary public IP addresses that are neither Private nor Reserved were judged as 1

Within the range of IPv4 addresses tested, exactly one of the three classifications was 1, so they were mutually exclusive. ipv4ToNumber is a function that converts an IP address into a 32-bit integer. It can be used for filtering by IP range (ipv4ToNumber(ip) >= X and ipv4ToNumber(ip) <= Y).

Analytics functions (rate, count_over_time, sum_over_time, offset, histogram)

Five features related to time-series data analysis were added.

count_over_time

count_over_time counts the number of records per time range.

filter metric = 'cpu_usage'
| stats count_over_time(*) as cot by bin(2m)
bin(2m) cot
2026-06-09 16:20:00.000 3
2026-06-09 16:18:00.000 4
2026-06-09 16:16:00.000 4
2026-06-09 16:14:00.000 4
2026-06-09 16:12:00.000 4
2026-06-09 16:10:00.000 1

Under this bin aggregation condition, the result was equivalent to count(*).

sum_over_time

sum_over_time calculates the total value per time range.

filter metric = 'cpu_usage'
| stats sum_over_time(value) as sot by bin(2m)
bin(2m) sot
2026-06-09 16:20:00.000 277
2026-06-09 16:18:00.000 447
2026-06-09 16:16:00.000 383
2026-06-09 16:14:00.000 408
2026-06-09 16:12:00.000 384
2026-06-09 16:10:00.000 143

This too gave a result equivalent to sum(value). count_over_time/sum_over_time appear to be names intended for time-series analysis. However, under this bin aggregation condition I could not confirm any difference in behavior from count(*)/sum(), so I cannot say definitively whether they behave identically.

histogram

histogram is used as a grouping function in the by clause, and aggregates by dividing a numeric field into buckets.

filter metric = 'cpu_usage'
| stats count(*) as cnt by histogram(value, 50)
histogram(value, 50) cnt
50 10
100 10

The second argument is the bucket width, and the lower bound of each bucket is shown in the result. Changing the bucket width to 25 reveals a finer distribution.

filter metric = 'cpu_usage'
| stats count(*) as cnt by histogram(value, 25)
histogram(value, 25) cnt
50 2
75 8
100 6
125 4

Note that histogram is a grouping function in the by clause, not an aggregate function of stats. While bin buckets along the time axis, histogram buckets along a numeric axis.

offset

offset is a modifier of bin() that shifts the alignment (start position) of bin boundaries.

filter metric = 'cpu_usage'
| stats count(*) as cnt by bin(5m) offset 5m
bin(5m) cnt
2026-06-09 16:20:00.000 3
2026-06-09 16:15:00.000 10
2026-06-09 16:10:00.000 7

The syntax is to append it after bin, as in by bin(5m) offset 5m. It is a modifier, not a function. Because offset lets you shift the start position of bin boundaries, you can set aggregation intervals that match business hours.

rate

rate is a function that calculates the rate of change of a numeric field within a bin. With the syntax rate(field, period), the second argument specifies a time unit (1s, 1m, 2m, etc.).

filter metric = 'cpu_usage'
| stats rate(value, 1s) as rate_1s, rate(value, 1m) as rate_1m, rate(value, 2m) as rate_2m by bin(5m)
bin(5m) rate_1s rate_1m rate_2m
2026-06-09 18:35:00.000 20 0.3333 0.1667
2026-06-09 18:30:00.000 20 0.3333 0.1667
2026-06-09 18:25:00.000 20 0.3333 0.1667

The test data is a time series whose value increases by 10 every 2 minutes. From the results, the ratio of values by period was rate_1s : rate_1m : rate_2m = 60 : 1 : 0.5. rate returns the total change in the field value within the bin divided by the number of seconds in the period. The shorter the period, the larger the value.

Note that specifying a number (60) as the second argument results in an error. You must specify it with a time unit such as 1m.

Parse syntax (parse CSV, parse XML, parse multi, values, addtotals)

Log parsing capabilities have been greatly extended. Three new parse modes — CSV, XML, and multiple matches — have been added, along with the aggregation helpers values and addtotals.

parse CSV

With the syntax parse @message CSV as alias1, alias2, ..., CSV-format logs are split by column.

filter @logStream = 'test-csv-stream'
| parse @message CSV as ts, lvl, svc, val
| display ts, lvl, svc, val
ts lvl svc val
2026-06-10T00:04:00Z DEBUG cache 10
2026-06-10T00:03:00Z INFO search 50
2026-06-10T00:02:00Z WARN payment 175
2026-06-10T00:01:00Z ERROR auth-service 250

Each comma-separated value was stored in the aliases in order. There is no distinction between header rows and data rows; all rows are subject to parsing.

parse XML

For XML-format logs, fields are extracted using XPath-like path expressions.

filter @message like /<event>/
| parse @message XML '/event/level' as xlevel
| parse @message XML '/event/service' as xsvc
| parse @message XML '/event/code' as xcode
| display xlevel, xsvc, xcode
xlevel xsvc xcode
WARN payment 429
INFO api-gw 200
ERROR auth 401

The syntax is parse @message XML '/element/path' as alias. Within the range I checked, values could be retrieved with simple element paths such as /event/level. Retrieving an entire document object with something like parse @message XML as doc, or accessing it with dot notation, resulted in an error. To extract multiple fields, pipe parse multiple times as shown above.

parse multi

parse multi expands all matches of a regular expression within a single line into separate records. It is powerful for parsing key=value format logs.

filter @message like /^level=/
| parse @message /(?<kname>\w+)=(?<kval>\S+)/ multi
| stats count(*) by kname
kname count(*)
level 3
service 3
latency 3
request_id 3

The source data is 3 lines, each with 4 key=value pairs. Adding multi expanded them into 3 lines × 4 pairs = 12 records, allowing aggregation by key name.

Without multi, only the first match per line is extracted.

parse @message /(?<kname>\w+)=(?<kval>\S+)/
| display kname, kval
kname kval
level WARN
level INFO
level ERROR

With the regex-based parse multi tested this time, extraction worked as expected when using the named capture group (?<name>...). On the other hand, the as alias multi syntax described in the documentation produced a syntax error in the author's environment.

values

values is an aggregate function that returns the distinct values of each group together.

filter ispresent(service) and ispresent(level)
| stats values(service) as services by level
level services
INFO test-convert, api-gateway, geo-service, notification, search-service
WARN payment-service, queue-worker
ERROR auth-service, cdn-edge
DEBUG data-pipeline

In this API result, it was confirmed as a comma-separated string representation. It is useful for listing the unique values within a group.

addtotals

addtotals is a command that adds a column totaling the numeric fields of each row. Note that all numeric fields present in the query, not just the displayed columns, are included in the sum, so the simple sum of the displayed columns may not match the Total value.

filter ispresent(response_time)
| fields toNumber(response_time) as rt, toNumber(response_time) * 2 as rt2
| addtotals
| limit 5
rt rt2 Total
2045 4090 8180
156 312 624
8901 17802 35604
67 134 268
234 468 936

By default, a row total is added with the column name Total. The column name can be customized with addtotals fieldname=RowSum.

Specifying col=true should also add a column totals row, but the response of the get-query-results API did not include a column totals row. It may be displayed only in the console UI.

Others (limit any, up to 10 stages of the stats command)

limit any

limit any is a syntax that returns any N records with no ordering guarantee. Whereas a normal limit returns the first N records in the order of the preceding sort (when unspecified, behavior that appears to be descending timestamp order), limit any may return results faster when ordering is unnecessary.

fields service, level, ip | limit any 2
service level ip
search-service INFO 10.255.255.1
cdn-edge ERROR 52.194.x.x

It is useful when you want to quickly get a sample from a log group with a large volume of logs.

Up to 10 stages of the stats command

The range in which stats can be chained with pipes has been extended, and up to 10 stages can now be used in the Standard log class.

filter ispresent(service)
| stats count(*) as cnt by service, level
| stats sum(cnt) as level_total by level
| stats max(level_total) as max_level_total, min(level_total) as min_level_total
max_level_total min_level_total
5 1

Three stages of stats are connected with pipes. The first stage counts per service × level, the second stage totals per level, and the third stage calculates the maximum and minimum.

According to the documentation, up to 10 stages can be used in the Standard log class, and up to 2 stages in the Infrequent Access log class. Subsequent stats can reference only fields defined in the preceding stage, and sort and limit must be placed after the last stats.

As a practical example, here is a pattern for grasping trends from the aggregation of message character counts by time period.

fields strlen(@message) as msg_len
| stats sum(msg_len) as total_chars by bin(5m)
| stats max(total_chars) as peak, min(total_chars) as lowest, avg(total_chars) as average
peak lowest average
1586 431 935.5

The total message character count is calculated every 5 minutes, and the peak, minimum, and average are obtained from that. If the logs are mostly ASCII, it can also be used to get a rough sense of message size trends. This pattern of "aggregating aggregation results" can now be completed in a single query.

Summary

With 13 added in May and 23 in June, the CloudWatch Logs Insights query language has expanded greatly in a short time.

The features added this time included IP classification, CSV/XML parsing, histogram, and multi-stage stats. It has become easier to go beyond searching logs to perform aggregation, classification, and trend analysis within the query. Within the scope of what I tested, it seems there will be more situations where analysis that previously required post-processing with Athena or external tools can be completed with Logs Insights alone.

On the other hand, in the author's environment at the time of writing, case-insensitive search via the third argument of strcontains did not work as expected.

My impression is that CloudWatch Logs Insights is evolving from a "tool for searching and checking logs" into a query environment that is easier to use for analysis purposes as well. With these additions, the situations where it can be used for daily investigations and ad hoc analysis seem likely to expand further.

https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html

Share this article

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