I Tested the 23 New Commands and Functions Added to CloudWatch Logs Insights (June 2026 Edition)
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.
The previous article introduced the 13 announced on May 21, but now, in less than a month, there is another major addition.
| 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 as1
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.
Reference links
