Skip to main content

Notification enrichment

Note

This feature is available for early-access customers. To request access and confirm your organization meets the feature criteria, contact your account representative or Support.

A notification tells a responder that something broke, but not what the data looked like when it did. Notification enrichment runs a DataPrime query the moment the alert triggers and delivers the results alongside the notification.

Why it matters

Without enrichment, a responder reads the notification, opens Explore, and runs the same diagnostic query every time. Enrichment runs that query for them.

For example, an error-rate alert fires on the checkout service. An enrichment query attached to that alert returns the affected customers, and that table arrives inside the notification. The responder knows the blast radius before opening the Case.

What you need

  • An alert that opens a Case. Enrichment applies to Case notifications, not to signal-based alert notifications.
  • A query that returns the context you want responders to see.

Add an enrichment query to an alert

  1. Open the alert and go to the Notification step.

  2. In Enrichment, write the DataPrime query to run when the alert triggers.

    For example, to return error logs:

    source logs | filter $m.severity == ERROR
  3. (Optional) Select View in explorer to run the query against your data and check the results before saving. You can also write the query in Explore and bring it back.

  4. Save the alert.

The query runs when the alert triggers, and the results are captured at that moment rather than when someone opens the notification.

The Case timeframe is applied for you

You do not need a time clause. By default the query runs over the Case's own timeframe, so the rows you get back are the ones that sit inside the window the alert fired on.

A time range set in the query itself wins. If the query specifies its own range, that range is used and the Case timeframe is ignored, which is how you reach outside the alert's window when the rows a responder needs sit before it.

The query runs asynchronously, so results land shortly after the notification itself rather than at the same instant. It runs once, when the Case activates, and the rows attached to the notification are that single result. Re-triggering or updating the Case does not run it again.

The Enrichment query link on the Case is different: it re-evaluates each time you open it, so it opens Explore on current data rather than the rows captured at activation. Expect the two to diverge on an old Case.

A query that has not returned within 5 seconds is cut off, so keep it narrow.

Write a good enrichment query

An enrichment query is not the alert's query. It is the query the responder would have run next.

Answer the responder's first question. Every alert type has one. A threshold alert prompts "show me the events"; a latency alert prompts "which requests were slow"; a security alert prompts "who is doing this". The example gallery is organized around those questions.

Mirror the alert's filter. The enrichment does not inherit the filter from the alert or from an events2metrics rule. You write it yourself. If the alert counts logs at severity ERROR and above, filter the same way, or you will attach evidence that is not what fired.

Aggregate before you dump. Fifty raw rows is a wall of text in Slack. A groupby that ranks error codes or sums impact reads in seconds. Attach raw rows only when the individual rows are the answer.

Target the dataset your logs actually live in. source logs resolves to the default logs dataset. If your queryable logs sit elsewhere, such as the Frequent Search tier, point the query there instead:

source frequentsearch/logs
Note

distinct_count is not supported on the Frequent Search tier. If you query frequentsearch data and need a distinct count, use approx_count_distinct instead.

Order and cap deliberately. End with orderby $m.timestamp desc | limit 50 so the newest rows arrive first and the result fits what a notification delivers.

Return every column at the top level. The attached table is built from the raw query result, which carries no schema. A nested object is not flattened into separate columns the way it is in Explore, so a query that returns { j: { source_ip, username }, attempts } renders the whole j object in one cell.

Project the fields you want to their own columns with choose:

source frequentsearch/logs
| filter $d.text.contains('login_failed')
| extract $d.text into $d.j using jsonobject()
| groupby $d.j.source_ip, $d.j.username aggregate count() as attempts
| orderby attempts desc
| limit 50
| choose $d.j.source_ip as source_ip, $d.j.username as username, attempts

How the table is built

The Explore screen inspects your query to decide which columns to show and in what order, then flattens nested fields for you. Enrichment does not have that: it receives only the query result, as JSON where each key is a column, and renders it as a table on a best-effort basis.

Two consequences are worth knowing before you write a query.

  • Nested fields stay nested. Flatten them yourself, as above.
  • Columns arrive ordered by key name, not in the order your query lists them. A query ending choose source_ip, username, attempts delivers attempts first. This cannot be changed from the query.

The same query can therefore look different in a notification and in Explore. That is expected, and it is why the Enrichment query link on the Case is the place to go when you want the Explore rendering.

Each example pairs an alert type with the question a responder asks in the first ten seconds. None of them carries a time clause, because the Case timeframe is applied automatically.

Show me the events that fired this

For a threshold alert on logs or a metric derived from logs, when the individual events carry the diagnosis: error messages, stack traces, transaction IDs.

source logs
| filter $m.severity == ERROR || $m.severity == CRITICAL
| choose $m.timestamp, $l.applicationname, $l.subsystemname, $d.message, $d.error_code
| orderby $m.timestamp desc
| limit 50

What kind of failure is it?

Rather than fifty rows, a ranked table of which error codes are hitting which endpoints. This reads in one glance in a chat message.

source logs
| filter $m.severity == ERROR
| groupby $d.error_code, $d.http.path aggregate count() as occurrences
| orderby occurrences desc
| limit 10

How big is the blast radius?

The alert says errors crossed a threshold; the next question is how many customers that touched. A distinct count turns the same logs into impact.

source logs
| filter $m.severity == ERROR
| groupby $l.applicationname aggregate
count() as error_events,
distinct_count($d.transaction_id) as failed_transactions,
distinct_count($d.customer_id) as customers_affected
| orderby failed_transactions desc

Which requests are slow?

For a latency alert, the evidence is the slowest requests, not the error logs.

source logs
| filter $d.duration_ms > 5000
| choose $m.timestamp, $d.http.path, $d.duration_ms, $d.transaction_id, $l.subsystemname
| orderby $d.duration_ms desc
| limit 20

What was the last thing we heard?

When an alert fires because logs stopped, the Case timeframe covers the silence, so a query scoped to it has nothing to return. This is the one example that needs a wider window: set a time range in the query that reaches back past the silence, and the last events before it come back. Show when each subsystem was last heard from, which pins the stop time and often the cause: a shutdown message, a config change, an auth failure.

source logs
| filter $l.applicationname == 'github-audit'
| groupby $l.subsystemname aggregate
max($m.timestamp) as last_seen,
count() as events_in_window
| orderby last_seen asc

What changed right before this?

Most incidents follow a change. If deployments or config changes are ingested as logs, attach them, and the responder sees which version shipped minutes before the spike.

source logs
| filter $l.subsystemname == 'deployments' || $d.event_type == 'config_change'
| choose $m.timestamp, $d.service, $d.version, $d.actor, $d.event_type
| orderby $m.timestamp desc
| limit 20

Who is doing this, and from where?

For a failed-login or suspicious-activity alert, the first question is the offender list, not the individual events.

source logs
| filter $d.event == 'login_failed'
| groupby $d.source_ip, $d.username aggregate count() as attempts
| orderby attempts desc
| limit 10

Did the notifications themselves fail?

Coralogix records every delivery attempt in the notification.deliveries system dataset, so an alert on delivery failures can be enriched from the platform's own delivery history: connector, failure reason, and HTTP status per failed send.

source system/notification.deliveries
| filter outcome.status != 'Success'
| choose outcome.timestamp, destination.connectorInfo.name,
outcome.status, outcome.status_reasons.message,
outcome.https.responseStatusCode
| orderby outcome.timestamp desc
| limit 20

Troubleshooting

The notification says the query returned no rows, but the alert fired on real events.

Work through these in order.

CheckWhat to do
DatasetOpen the Enrichment query link on the Case, which runs the same query over the same window in Explore. If source logs is empty there while your data shows up under another dataset, point the query at the dataset that holds it.
Filter driftCompare the enrichment's filter against the alert's. Editing one without the other is the most common cause.
WindowThe events may sit just outside the Case timeframe. This is most likely on absence alerts, where the window covers the silence. Setting a time range in the query overrides the Case timeframe, which is how you widen the search past it.

Where the results appear

The same result set reaches three places.

WhereWhat you see
The CaseAn enrichment query link. Selecting it opens Explore with the query and its results.
Slack and Microsoft TeamsThe results render as a table, either in the notification itself or as a reply in its thread when the table is too large to sit in the message.
EmailThe results render as a table in the message body, with the same rows attached as a CSV file.

Because the results are stored on the Case, anyone who opens it later sees the same context the first responder saw, even if the underlying data has since changed.

Limitations

  • Enrichment applies to Case notifications only. An alert that does not open a Case has nothing to attach results to.
  • Notifications must be enabled on the alert, with at least one routing label. If you turn notifications off, the enrichment query is dropped without warning.
  • One enrichment query per alert. If responders need both raw rows and a summary, pick the one that answers their first question, or use a groupby that carries counts and examples together.
  • A notification shows at most 50 rows. The CSV attached to an email carries those same rows, not the full result set, so a query that matches more than 50 records is truncated everywhere it is delivered.
  • A large result may not render as a table at all, and arrives as plain text instead.
  • The query is cut off after 5 seconds. A query that scans too much returns nothing rather than partial results.
  • The query cannot reference the values that triggered the alert. An alert grouped by application that fires for one application runs the same enrichment regardless, so if two applications error in the same window, both appear in the results. Mirror the alert's filter by hand and scope the query as tightly as you can.
Last updated on