Testing 25 New Commands and Functions Added to CloudWatch Logs Insights (July 2026 Edition)
This page has been translated by machine translation. View original
Introduction
On July 15, 2026, 25 new commands and functions were added to CloudWatch Logs Insights.
This is the third installment, following the May release (13 items) and the June release (23 items), bringing the three-month total to 61. This time the focus is on analysis-oriented features such as row-level sequence operations, sessionization, outlier detection, and CIDR lookup.
Here is the list of added features.
| Category | Function/Command | Purpose |
|---|---|---|
| Type conversion | hexToAscii | Convert a hex string to ASCII |
| Type conversion | hexToDec | Convert a hex string to a decimal number |
| Type conversion | decToHex | Convert a decimal number to a hex string |
| Date/time | parseDate | Parse a date string into epoch milliseconds |
| Date/time | formatDate | Format a timestamp in strftime format |
| Date/time | queryStartTime | Return the query window start time |
| Date/time | queryEndTime | Return the query window end time |
| Date/time | queryTimeRange | Return the query window width in milliseconds |
| Row sequence | autoregress | Generate previous-row values as lag fields |
| Row sequence | accum | Calculate the cumulative sum of a numeric field |
| Row sequence | filldown | Forward-fill missing values with the last non-null value |
| Row sequence | fillmissing | Insert a specified value into empty time bins |
| Session | sessionize | Sessionize by identity + inactivity gap |
| Statistics | variance | Calculate population variance |
| Statistics | topk | Return the top k most frequent values |
| Statistics | countFrequent | Return approximate counts of field values in descending order |
| String | messageSize | Return the byte length of a field |
| JSON | jsonArraySize | Return the number of elements in a JSON array string |
| JSON | jsonArrayContains | Determine whether a JSON array contains a specified value |
| Condition | isNumeric | Determine whether a field value is numeric |
| Query composition | where | Alias of the filter command |
| Analysis | outlier | IQR-based outlier detection |
| Time comparison | logcompare | Compare the current and past time windows |
| Query composition | appendcols | Join subquery columns by row position |
| Lookup | cidrlookup | Match an IP against a lookup table by CIDR range |
Comparing the additions over the three months, the direction of the features differs from month to month.
| Period | Items added | Trend |
|---|---|---|
| May 2026 | 13 | Reading and formatting (parse extensions, display, string functions) |
| June 2026 | 23 | Aggregation and judgment (conditional branching, statistical functions, IP checks) |
| July 2026 | 25 | Analysis, sessionization, and joins (row operations, outlier, lookup) |
In this article, I actually ran all 25 of these against the log group /test/insights-new-2026-07 (60 test data entries) in the us-east-1 region. I summarize the behavior I was able to confirm and practical points to note. The verification date is 2026-07-17.
Verification
Type conversion and encoding
These are the three functions hexToAscii, hexToDec, and decToHex, which convert between hexadecimal and ASCII strings or decimal numbers.
filter ispresent(hex_ascii)
| display hex_ascii, hexToAscii(hex_ascii) as ascii_text, hex_num, hexToDec(hex_num) as dec_val, decimal, decToHex(decimal) as hex_out
| hex_ascii | ascii_text | hex_num | dec_val | decimal | hex_out |
|---|---|---|---|---|---|
| 436c6f7564576174636820496e736967687473 | CloudWatch Insights | 7fff | 32767 | 32767 | 0x7fff |
| 576f726c64 | World | 1a | 26 | 26 | 0x1a |
| 48656c6c6f | Hello | ff | 255 | 255 | 0xff |
hexToAscii converts a hex string into an ASCII string. "CloudWatch Insights" was restored from a 38-digit (19-byte) input, so it handles variable-length input. hexToDec accepts lowercase hex without the 0x prefix as input and returns a decimal number. The reverse, decToHex, returned lowercase hex with the 0x prefix in its output.
Date/time functions
parseDate and formatDate, which convert between date strings and timestamps, were added, along with queryStartTime, queryEndTime, and queryTimeRange, which reference the times of the query window itself. I verify all five functions together.
Here are the verification results for parseDate and formatDate.
filter ispresent(date_str)
| display event, date_str, parseDate(date_str, "yyyy-MM-dd", "UTC") as parsed, formatDate(@timestamp, "%Y-%m-%d %H:%M:%S", "UTC") as fmt
| event | date_str | parsed | fmt |
|---|---|---|---|
| rollback | 2026-07-01 | 2026-07-01 00:00:00.000 | 2026-07-16 18:09:23 |
| release | 2026-07-10 | 2026-07-10 00:00:00.000 | 2026-07-16 18:09:18 |
| deploy | 2026-07-15 | 2026-07-15 00:00:00.000 | 2026-07-16 18:09:13 |
parseDate takes a Java DateTimeFormatter pattern as the second argument and specifies the time zone in the third argument. The return value is epoch milliseconds, but it is rendered in timestamp format on the Logs Insights console. formatDate uses a strftime-style format string, and the official documentation states that it can also be called with the alias strftime.
Next are the three functions that return the times of the query window.
fields queryStartTime() as qst, queryEndTime() as qet, queryTimeRange() as qtr | limit 1
| qst | qet | qtr |
|---|---|---|
| 2026-07-16 16:13:38.000 | 2026-07-16 18:14:38.999 | 7260999 |
queryTimeRange returns the width of the query window in milliseconds. For a window of about two hours, 7260999 ms was returned, and it can be used within a query as a reference value for aggregation and normalization.
Row sequences and null handling
autoregress, accum, and filldown were added as operations that depend on row ordering, and fillmissing was added as an operation that fills gaps in time bins. For the first three commands, you need to specify sort @timestamp asc earlier in the pipeline (behavior confirmed during verification).
autoregress expands previous-row values into new columns as lag fields.
filter ispresent(metric) | sort @timestamp asc | fields @timestamp, value | autoregress value p=1-2
| @timestamp | value | value_p1 | value_p2 |
|---|---|---|---|
| 2026-07-16 18:09:43.682 | 10 | ||
| 2026-07-16 18:09:48.682 | 12 | 10 | |
| 2026-07-16 18:09:53.682 | 11 | 12 | 10 |
| 2026-07-16 18:09:58.682 | 13 | 11 | 12 |
| 2026-07-16 18:10:03.682 | 12 | 13 | 11 |
Specifying p=1-2 automatically generates value_p1, holding the value from one row before, and value_p2, holding the value from two rows before. The leading rows are empty because there are not enough preceding rows for the lag. Since previous-row values become available, it can also be used to create derived columns, such as taking a difference with value - value_p1.
accum calculates the cumulative sum of a numeric field.
filter ispresent(metric) | sort @timestamp asc | fields @timestamp, value | accum value AS running_total
| @timestamp | value | running_total |
|---|---|---|
| 2026-07-16 18:09:43.682 | 10 | 10 |
| 2026-07-16 18:09:48.682 | 12 | 22 |
| 2026-07-16 18:09:53.682 | 11 | 33 |
| 2026-07-16 18:09:58.682 | 13 | 46 |
| 2026-07-16 18:10:03.682 | 12 | 58 |
filldown forward-fills missing values with the last non-null value.
filter ispresent(sensor) | sort @timestamp asc | fields @timestamp, sensor, region, reading | filldown region
| @timestamp | sensor | region | reading |
|---|---|---|---|
| 2026-07-16 18:11:23.682 | temp-1 | us-east-1 | 22.5 |
| 2026-07-16 18:11:28.682 | temp-1 | us-east-1 | 23.1 |
| 2026-07-16 18:11:33.682 | temp-1 | us-east-1 | 22.8 |
| 2026-07-16 18:11:38.682 | temp-1 | eu-west-1 | 19.2 |
| 2026-07-16 18:11:43.682 | temp-1 | eu-west-1 | 19.5 |
In the original data, region was missing in rows 2, 3, and 5, but filldown filled them with the last non-null value. This is useful for sensor data and event logs where context information is recorded in only some of the records.
fillmissing is used after stats ... by bin() and inserts a specified value into time bins that have no data.
filter metric = "cpu_usage" | stats avg(value) as avg_val by bin(10s) | fillmissing with 0 for avg_val
| bin(10s) | avg_val |
|---|---|
| 2026-07-16 18:27:50.000 | 0 |
| 2026-07-16 18:27:40.000 | 0 |
| 2026-07-16 18:27:30.000 | 0 |
All three displayed rows are bins with no data, and fillmissing inserted 0. In this verification data, no bins with actual data were included in the displayed range, so everything is 0.
Sessionization
sessionize is a command that splits a sequence of events into sessions based on an identity field and an inactivity gap (maxspan).
filter ispresent(userId) | sort @timestamp asc | fields @timestamp, userId, action, deviceId | sessionize userId, deviceId maxspan 20s
| @timestamp | userId | action | deviceId | session_id |
|---|---|---|---|---|
| 2026-07-16 18:11:48.682 | user-A | login | mobile | user-A\x1fmobile-1 |
| 2026-07-16 18:11:53.682 | user-A | view_page | mobile | user-A\x1fmobile-1 |
| 2026-07-16 18:11:58.682 | user-B | login | desktop | user-B\x1fdesktop-1 |
| 2026-07-16 18:12:03.682 | user-A | click | mobile | user-A\x1fmobile-1 |
| 2026-07-16 18:12:08.682 | user-B | view_page | desktop | user-B\x1fdesktop-1 |
| 2026-07-16 18:12:13.682 | user-B | logout | desktop | user-B\x1fdesktop-1 |
| 2026-07-16 18:12:18.682 | user-A | login | mobile | user-A\x1fmobile-1 |
| 2026-07-16 18:12:23.682 | user-A | purchase | mobile | user-A\x1fmobile-1 |
Using the combination of userId and deviceId as the identity, events occurring consecutively within a maxspan of 20s were grouped into the same session. The session_id field is generated automatically. In this verification, it was displayed in a form like user-A\x1fmobile-1, where the values of the identifier fields are joined with the control character \x1f (Unit Separator) and a sequence number is appended at the end. Since this format is not stated in the official documentation, it may be implementation-dependent. According to the official documentation, the default value of maxspan is 30 minutes. Session analysis of user behavior can now be done with Logs Insights alone.
Statistics commands
On the statistics side, variance (population variance), topk (top-k frequent values), and countFrequent (approximate frequency count) have been added.
I checked variance alongside the existing stddev and avg.
filter ispresent(response_time) | stats variance(response_time) as v, stddev(response_time) as sd, avg(response_time) as avg_rt
| v | sd | avg_rt |
|---|---|---|
| 4788776.5156 | 2188.3273 | 1017.4667 |
variance returns the population variance. Squaring the stddev value almost exactly matches variance (2188.3273² ≒ 4788776), which was consistent with the standard deviation.
topk returns the top k frequent values and their occurrence counts.
filter ispresent(status) | stats topk(3, status)
| status | count |
|---|---|
| 200 | 10 |
| 403 | 2 |
| 500 | 2 |
The official documentation states that it cannot be used together with a by clause.
countFrequent calculates approximate counts for each combination of field values and returns them in descending order.
filter ispresent(method) | countFrequent method, status
| method | status | _approxcount |
|---|---|---|
| GET | 200 | 5 |
| POST | 200 | 5 |
| GET | 500 | 2 |
| GET | 403 | 2 |
| POST | 401 | 1 |
The _approxcount field is generated automatically.
String functions
messageSize is a function that returns the byte length of a field.
fields messageSize(@message) as sz, @message | sort sz desc | limit 5
| sz | @message (excerpt) |
|---|---|
| 147 | {"level": "INFO", "service": "payment-service"...} |
| 146 | {"level": "INFO", "service": "payment-service"...} |
| 145 | {"level": "INFO", "service": "payment-service"...} |
JSON functions
jsonArraySize and jsonArrayContains are functions that operate on JSON array strings.
filter ispresent(role_list)
| display user, role_list, jsonArraySize(role_list) as role_count, jsonArrayContains(role_list, "admin") as is_admin
| user | role_list | role_count | is_admin |
|---|---|---|---|
| charlie | ["admin","viewer"] | 2 | 1 |
| bob | ["viewer"] | 1 | 0 |
| alice | ["admin","editor","viewer"] | 3 | 1 |
jsonArraySize returned the number of elements in the array, and jsonArrayContains returned 1 if the specified value was included and 0 if not. These functions cannot be used on arrays that have been expanded by automatic parsing into forms like roles.0; they must be applied to fields that are kept as strings.
Conditional validation
isNumeric determines whether a field value can be interpreted as a number.
filter ispresent(field_a)
| display field_a, isNumeric(field_a) as a_num, field_b, isNumeric(field_b) as b_num
| field_a | a_num | field_b | b_num |
|---|---|---|---|
| 0 | 1 | 99.9 | 1 |
| not_number | 0 | 100 | 1 |
| 42 | 1 | hello | 0 |
Query composition (where)
where is an alias of the filter command ("grammar alias for the filter command" in the official documentation), aimed at users familiar with SQL.
where response_time > 1000 | display service, response_time
| service | response_time |
|---|---|
| notification | 1100 |
| notification | 2045 |
| api-gateway | 8901 |
| auth-service | 1523 |
Data analysis (outlier)
outlier is a statistical outlier detection command based on the IQR (interquartile range). The official documentation describes it as "Detects statistical outliers based on the interquartile range (IQR)".
filter metric = "cpu_usage" | sort @timestamp asc | fields @timestamp, value | outlier mark=true param=1.5 value
The command was accepted and matched=20, but I observed that the output was limited to a single record. The cause is unconfirmed. For practical use, I recommend re-verifying with a larger dataset. According to the official documentation, specifying mark=true adds a boolean marker field indicating the outlier determination result. In this verification environment, this field was displayed as _is_outlier. The available options are action=remove/transform, mark=true, and param (IQR coefficient).
logcompare
logcompare is a command that compares the current time window with a past window.
I verified it with a two-hour query window and timeshift 1h. The error "Diff doesn't support overlap between two time ranges we compare" occurs. If timeshift is smaller than the width of the query window, the time ranges being compared overlap, resulting in this error. This is behavior confirmed during verification, and I could not find this constraint stated in the official documentation I referred to. For it to work correctly, you need to specify a timeshift larger than the query window.
appendcols
appendcols is a command that joins the result columns of a subquery to the main query by row position.
filter ispresent(service)
| stats count(*) as cnt by service
| appendcols ( SOURCE '/test/insights-new-2026-07' | filter ispresent(response_time) | stats avg(response_time) as avg_rt by service )
| service | cnt | avg_rt |
|---|---|---|
| notification | 3 | 1070.6667 |
| search-service | 3 | 212.6667 |
| api-gateway | 3 | 3145 |
| payment-service | 3 | 84.3333 |
| auth-service | 3 | 574.6667 |
The SOURCE clause is required in the subquery. Omitting SOURCE causes a MalformedQueryException. The join is done by positional row matching rather than key matching. In this verification, the results of the main query and the subquery were returned in the same order, and the corresponding values were joined correctly. For practical use, I recommend specifying sort in both the main query and the subquery to explicitly align the row order.
cidrlookup
cidrlookup is a command that matches an IP address against a lookup table by CIDR range and retrieves the corresponding values.
You need to create a lookup table in advance. You can create it from the CloudWatch console via Settings → Logs tab → Lookup tables → Create lookup table. There is also a method of uploading a CSV directly with the CreateLookupTable API.
The CSV used this time is as follows. The IP addresses shown are dummy values for verification, including documentation addresses defined in RFC 5737.
cidr,region,owner,network_type
198.51.100.0/24,us-west-2,external,public
10.0.0.0/8,us-east-1,internal,private
172.16.0.0/12,ap-northeast-1,internal,private
192.168.0.0/16,eu-west-1,internal,private
203.0.113.0/24,documentation,example,reserved
filter ispresent(ip)
| fields @timestamp, ip, service
| cidrlookup net_table ip AS cidr OUTPUT region, owner, network_type
| display ip, region, owner, network_type
| ip | region | owner | network_type |
|---|---|---|---|
| 198.51.100.10 | us-west-2 | external | public |
| 10.0.2.101 | us-east-1 | internal | private |
| 192.168.1.11 | eu-west-1 | internal | private |
| 172.16.0.6 | ap-northeast-1 | internal | private |
| 203.0.113.50 | documentation | example | reserved |
AS cidr specifies the CIDR column name in the lookup table, and OUTPUT lists the columns to retrieve. To display the results, I had to explicitly specify the OUTPUT columns in display.
Summary
The 25 commands and functions added in July 2026 further broaden the range of log analysis you can do with CloudWatch Logs Insights.
In particular, time-series data processing with autoregress, accum, filldown, and fillmissing, sessionization of user behavior with sessionize, and outlier detection with outlier look useful for incident investigation and trend analysis. In addition, logcompare enables comparison with past time periods, cidrlookup enables attaching attributes to IP addresses, and appendcols enables analysis that places multiple aggregation results side by side.
These are things that previously might have required forwarding logs to a database or an external analytics service for processing. With this feature addition, some of that can now be run within Logs Insights queries. For example, it can be used for log investigations like the following.
- Check differences from previous metric values and cumulative values
- Fill in missing time-series data and context information
- Sessionize a series of operations per user or device
- Extract outliers in response time or processing volume
- Compare current log trends with past time periods
- Attach information such as network and management categories to IP addresses
Some features require caution when used, such as commands that depend on row position or time range, but this update increases the situations in which aggregation and processing during an investigation can be completed entirely within Logs Insights.
Test data structure and ingestion example (excerpt)
This shows the log group used for verification and how the test data was ingested. The following is an excerpt of the setup and is not complete code that generates all 60 entries used in the main text. I used the us-east-1 region.
Create the log group and log stream.
aws logs create-log-group --log-group-name /test/insights-new-2026-07 --region us-east-1
aws logs create-log-stream --log-group-name /test/insights-new-2026-07 --log-stream-name test-stream --region us-east-1
The following is an excerpt of the code that generated the 60 JSON logs, covering the time-series metrics and the hex conversion data.
import json, time
now = int(time.time() * 1000)
events = []
# Example: time-series metrics (20 entries)
for i, v in enumerate([10, 12, 11, 13, 12]): # excerpt
events.append({
"timestamp": now + i * 5000,
"message": json.dumps({"metric": "cpu_usage", "value": v})
})
# Example: for hex conversion
events.append({
"timestamp": now,
"message": json.dumps({
"hex_ascii": "436c6f7564576174636820496e736967687473",
"hex_num": "7fff", "decimal": 32767
})
})
# After that, add logs for dates, JSON arrays, sessions, and CIDR in the same way
with open("events.json", "w") as f:
json.dump(events, f)
Ingest the generated logs with put-log-events.
aws logs put-log-events \
--log-group-name /test/insights-new-2026-07 \
--log-stream-name test-stream \
--log-events file://events.json \
--region us-east-1
