Insights reference: Documentation for Honeybadger Insights and the BadgerQL query language.
# Insights & Logging
> Dive into your Honeybadger and application events.
You can use [Honeybadger Insights](https://www.honeybadger.io/tour/logging-observability/) to dive into the data collected by Honeybadger and the logs and other events that you send to our [Events API](/api/reporting-events/). We provide a query language (that we lovingly call [BadgerQL](/guides/insights/badgerql/)) that enables quick discovery of what’s happening inside your applications. The Insights UI also lets you chart the results of those queries and add those charts to [dashboards](/guides/dashboards/) that you can share with your team.  ## Querying and visualization [Section titled “Querying and visualization”](#querying-and-visualization) Our [query language](/guides/insights/badgerql/) strives to be minimalist, yet powerful. With it you can specify which fields you want to see, filter the kinds of events that should be returned, perform aggregations and calculations, and more. When you first load the Insights UI, you will see a query box that has a default query to help you get started:
```badgerql
fields @ts, @preview
| sort @ts
```
This query selects a couple of special fields — the timestamp and a preview of the fields that are available in the event — and sorts the results by time, with the most recent results first. Each row of the query is piped through the following row, which allows you to apply filters, formatting functions, and so on. Let’s do a quick walk-through to see how it works, and to see how it can be used to create visualizations of your data. ### Walk-through [Section titled “Walk-through”](#walk-through) Here’s an example of working with some Honeybadger data. First, filter the data to see only the results of [uptime checks](/guides/uptime/):
```badgerql
fields @ts, @preview
| filter event_type::str == "uptime_check"
| sort @ts
```
 You can see that we’ve piped the initial results through `filter`, which accepts a variety of conditions, such as the string comparison shown here. You’ll also notice that we specified the data type of the `event_type` field (`str`) so the query parser can validate the functions and comparisons that you use on the field data. Clicking on the disclosure arrow will show the all the fields that were stored for an event:  Additional disclosure controls appear inside the event detail view when the event has nested objects. Let’s filter on some additional data that is present in these events. We can limit the results to show only the uptime checks that originated from our Virginia location, and we can change the fields that we display so we can see some info about the results of each check:
```badgerql
fields @ts, location::str, response.status_code::int, duration::int
| filter event_type::str == "uptime_check"
| filter location::str == "Virginia"
| sort @ts
```
 Now let’s summarize the data to find the average response duration for all successful checks:
```badgerql
fields duration::int
| filter event_type::str == "uptime_check"
| filter location::str == "Virginia"
| filter response.status_code::int == 200
| stats avg(duration) by bin(1h) as time
| sort time
```
 We use `stats` to perform all kinds of calculations, such as averages, and `by` allows us to specify the grouping for those calculations. Grouping by `bin` gives us time-series data, which makes it easy to create a chart by clicking the Line button.  From there you can experiment with different visualizations, update the query to change the chart (try changing `1h` to `15m`), and add the chart to a custom dashboard. Of course, this functionality isn’t limited to only the data that is generated by Honeybadger. Your error data is also available for querying (`event_type::str == "notice"`), and you can send logs and events to our [API](/api/reporting-events/) to be able to query and chart your own data. ### Natural language queries [Section titled “Natural language queries”](#natural-language-queries) You don’t need to know [BadgerQL](/guides/insights/badgerql/) to query your data. Click the lightbulb icon to the right of the query editor to open the natural language translator panel. Describe what you want to see, then press `⌘+Enter` or click Translate and Honeybadger will write the query for you.  The translator uses your current query as context, so you can build up a query in steps. Start broad, then ask for changes like including the duration, grouping by controller, or narrowing to just 5xx status codes. You can also include other display options in your description. For example, ask for “the last hour” or “as a line chart” and Honeybadger will update the time range or visualization. **Note:** The NL translator uses an LLM, so it may not always get things right. If the translations are not what you expected, please record your feedback via the thumbs, or feel free to [reach out to support](mailto:support@honeybadger.io). ### Streams [Section titled “Streams”](#streams) Streams are the fundamental data sources in Honeybadger Insights. They serve as the starting point for your queries and represent the data you want to analyze. When you create a new Honeybadger project, we automatically set up two streams for you: **Internal stream** The Internal Stream is a dedicated stream that stores all Honeybadger-generated events related to your project. This includes errors, deployments, notifications, uptime checks, and other internal Honeybadger data. You cannot directly send custom events to the Internal Stream, as it is managed by Honeybadger itself. **Default stream** The Default Stream is the primary stream for storing custom events that you send using Honeybadger client libraries or the Honeybadger API. Any event data you explicitly send to Honeybadger will be stored in the Default Stream. #### The stream selector [Section titled “The stream selector”](#the-stream-selector) You can select the active streams from the stream selector at the top of the query editor. This affects the data that Insights returns for your queries.  Removing a stream you don’t need can improve your query response times, because then Insights doesn’t need to scan that data when executing your query. So for example, if you’re just querying your application logs, you can remove the *Internal* stream to get a faster response. ## Working with dashboards [Section titled “Working with dashboards”](#working-with-dashboards) [Dashboards](/guides/dashboards/) allow you to collect different types of charts and query results on a single page. Any query or chart that you generate can be added to a dashboard, which will then be shared with the rest of your team. Each widget on a dashboard includes a link to view the query and raw results behind the widget:  If you change the query or the visualization, you can save those changes back to your dashboard, or add them as a widget to a new dashboard. We provide some [automatic dashboards](/guides/dashboards/#automatic-dashboards) to get you started. For example, when you [add a Heroku drain](/guides/insights/integrations/heroku/) to your app, the [automatic Heroku dashboard](/guides/dashboards/heroku/) will show data like the number of requests grouped by response code that we automatically collect from [Logplex](https://devcenter.heroku.com/articles/logplex). To learn more about dashboards, see the [dashboards guide](/guides/dashboards/). ## Adding data from other sources [Section titled “Adding data from other sources”](#adding-data-from-other-sources) Insights includes all the events that Honeybadger collects, such as error notifications, uptime checks, and check-in reports, but you can send your own event data as well. Our [API](/api/reporting-events/) accepts newline-delimited JSON, where each line is a JSON object that describes an event that you care about. You can send user audit trail events, metrics, or any other data you’d like to query and analyze. The type of data most frequently sent to Insights is application log data. Sending structured logs in a JSON format (like [lograge](https://github.com/roidrage/lograge) produces) allows you to correlate what’s happening in your app with the error data that Honeybadger is already recording for you. See our integration guides to learn how you can easily send log events from sources such as Heroku apps and CloudWatch Logs. [Ruby and Rails apps](/guides/insights/integrations/ruby-and-rails/)Send metrics and events from Ruby and Rails apps to Honeybadger Insights [Elixir/Phoenix apps](/guides/insights/integrations/elixir-phoenix/)Send logs and events from Elixir/Phoenix apps to Honeybadger Insights [JavaScript apps](/guides/insights/integrations/javascript/)Send metrics and events from JavaScript apps to Honeybadger Insights [PHP/Laravel apps](/guides/insights/integrations/php-laravel/)Send metrics and events from PHP/Laravel apps to Honeybadger Insights [OpenTelemetry (Beta)](/guides/insights/integrations/opentelemetry/)Send traces, metrics, and logs via the OpenTelemetry Protocol (OTLP) [CloudWatch Logs](/guides/insights/integrations/cloudwatch-logs/)Stream AWS CloudWatch Logs to Honeybadger Insights [Crunchy Bridge](/guides/insights/integrations/crunchy-bridge/)Send Crunchy Bridge metrics to Honeybadger Insights [Fly.io](/guides/insights/integrations/fly-io/)Send Fly.io app metrics to Honeybadger Insights [Heroku](/guides/insights/integrations/heroku/)Send Heroku app metrics to Honeybadger Insights [Host metrics](/guides/insights/integrations/host-metrics/)Send host metrics to Honeybadger Insights [Log files](/guides/insights/integrations/log-files/)Use Vector to ship your log files to Honeybadger Insights [Netlify](/guides/insights/integrations/netlify/)Send Netlify function logs to Honeybadger Insights [Rsyslog](/guides/insights/integrations/rsyslog/)Forward rsyslog messages to Honeybadger Insights over syslog-TLS [Systemd (journald)](/guides/insights/integrations/systemd/)Ship systemd journal logs to Honeybadger Insights
# Alarms guide
> Learn how to create Honeybadger alarms to monitor your Insights data in real time.
Insights alarms allow you to monitor your data in real time and get notified under the conditions you set. Your Honeybadger data, such as errors, deployments, and uptime checks, are already available to query. To learn how to send your own custom data to Honeybadger, see the [Getting started guide](/guides/insights/). Then, you can create alarms for anything your business needs.  ## Viewing an alarm [Section titled “Viewing an alarm”](#viewing-an-alarm) Alarms combine a [BadgerQL](/guides/insights/badgerql/) query (“count all slow requests in the past five minutes”) with a threshold (“when count is > 2”) and trigger alerts when the query result exceeds the threshold.  In the above chart, the red line is the threshold for the alarm state. This query was in an alarm state for one period in the last hour but recently recovered. ## Creating or updating an alarm [Section titled “Creating or updating an alarm”](#creating-or-updating-an-alarm) ### Query and timing [Section titled “Query and timing”](#query-and-timing) Construct a `query` using [BadgerQL](/guides/insights/badgerql/) to return data you wish to monitor. You may want to use a `filter` function to isolate the relevant data.  Use the `interval` field to specify the time window for the query. The `interval` field is a string that represents the time window for the query. The format is `1d`, `1h`, `1m`, etc. In other words, the `interval` is both the frequency and the time period over which the query is executed. The `lag` field can be used to delay the query execution by a specified time period. The `lag` field is also a string that represents the time delay for the query. The format is `1d`, `1h`, `1m`, etc. This is useful when you want to wait for slow or late data arriving.  ### Result count [Section titled “Result count”](#result-count) Alarms are triggered based on the number of results returned by the query. You can specify the logical comparison operator (`>`, `>=`, `<`, `<=`, `==`, `!=`) and a value count. The alarm will trigger when the number of results meets the condition.  ### Description [Section titled “Description”](#description) It may be helpful to provide a description of the alarm to help you remember its purpose. The description is also delivered as part of the notification when you have integrations setup. Some useful information would be what the alarm is monitoring, what to do when the alarm triggers, and who to contact. ## Integrations [Section titled “Integrations”](#integrations) Integrations is where you can configured how to be notified when an alarm changes state. There are two states per integration that can be configured: `ok` and `alert`. All users can update their personal notification integrations (email, etc.), while users with administrator access to the project can manage the alert settings for all of the project’s integrations. 
# Archive destinations
> Replicate Honeybadger Insights stream data to an S3-compatible bucket you own.
Archive destinations replicate the events flowing into your Honeybadger [Insights streams](/guides/insights/#streams) to an S3-compatible bucket that you own and control. Once a destination is configured and a stream is attached, Honeybadger writes a continuous archive of that stream’s events into your bucket. This is useful for long-term retention beyond your Insights data window or for feeding events into your own data warehouse or lake.  ## What gets archived [Section titled “What gets archived”](#what-gets-archived) Only the events that flow through your Insights streams are replicated: * The custom events your application sends to Insights via the [Events API](/api/reporting-events/) or a Honeybadger client library. * The internal events Honeybadger generates for your project — error notifications, deployments, uptime checks, check-in reports, and so on. Detailed error data such as backtraces, breadcrumbs, and environment variables is **not** included. Archive destinations replicate distilled stream events, not full error payloads. You choose which streams replicate to which destination, so you can archive just your application events, just the internal Honeybadger events, or both. ## Supported providers [Section titled “Supported providers”](#supported-providers) You can point a destination at any of these S3-compatible providers: * Amazon S3 * Cloudflare R2 * Google Cloud Storage (S3-compatible interop endpoint) * Wasabi * Backblaze B2 * DigitalOcean Spaces The endpoint must use HTTPS. AWS S3 buckets are detected from the bucket name and don’t require an endpoint URL — for the others, set the provider’s S3-compatible endpoint URL on the destination. ## Setting up a destination [Section titled “Setting up a destination”](#setting-up-a-destination) Archive destinations are managed at the account level under **Account Settings → Archive Destinations**. ### 1. Create the bucket [Section titled “1. Create the bucket”](#1-create-the-bucket) Create a bucket on your provider of choice. A fresh bucket dedicated to Honeybadger archives is the simplest setup, but you can also add archives to an existing bucket by configuring a prefix on the destination. ### 2. Create credentials [Section titled “2. Create credentials”](#2-create-credentials) Create an access key and secret with permission to write to that bucket. The archiver only needs to upload objects — at minimum: * `s3:PutObject` on the bucket (scoped to your prefix is fine) No read, list, or delete permissions are required. We recommend creating a dedicated IAM user (or equivalent on your provider) scoped to just the archive bucket so the credentials you give Honeybadger can’t reach anything else. For example, on AWS S3 a minimal policy looks like:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
```
If you scope to a prefix, change the resource to `arn:aws:s3:::your-bucket-name/your-prefix/*`. Objects are uploaded with AES-256 server-side encryption (SSE-S3) by default. If your bucket policy enforces a specific encryption type, make sure SSE-S3 is allowed. ### 3. Add the destination in Honeybadger [Section titled “3. Add the destination in Honeybadger”](#3-add-the-destination-in-honeybadger) In **Account Settings → Archive Destinations**, click **New destination** and fill in: * **Name** — a short label that’s unique within your account. * **S3 bucket** — the bucket name. * **Prefix** *(optional)* — a key prefix that all archived objects will be written under. Useful if the bucket is shared with other data. * **Region** *(optional, required for AWS S3)* — e.g. `us-west-2`. * **Endpoint URL** *(optional)* — leave blank for AWS S3. Set this to the provider’s S3-compatible endpoint for R2, GCS, Wasabi, Backblaze, or DigitalOcean Spaces. * **Access key ID** and **Secret access key** — the credentials from step 2. Credentials are encrypted at rest. When editing an existing destination, leave the credential fields blank to keep the stored values; fill them in to rotate. ### 4. Attach streams [Section titled “4. Attach streams”](#4-attach-streams) A destination with no streams attached is **paused** — nothing replicates until you select at least one stream. On the destination form, pick the streams you want to replicate. Each stream can only be attached to one destination at a time. Streams start replicating on the next archive cycle. There’s no backfill — only events ingested after a stream is attached will land in your bucket. ## File structure [Section titled “File structure”](#file-structure) Objects are gzip-compressed JSON Lines (one event per line, served with `Content-Type: application/jsonl`). The key layout is:
```plaintext
[prefix/]insights/{stream_id}/{YYYY}/{MM}/{DD}/{HH}/{unix_timestamp}_{random_hex}.jsonl.gz
```
For example, with prefix `honeybadger`:
```plaintext
honeybadger/insights/abc123/2026/04/30/14/1714485612_a3f80c1d4e2b9876.jsonl.gz
```
The path components: * `prefix/` — the optional prefix you configured on the destination. * `insights/{stream_id}/` — fixed prefix plus the stream ID. * `{YYYY}/{MM}/{DD}/{HH}/` — UTC ingestion hour the events fell into. This is based on when the events were received by Honeybadger, not when the file was written, so a delayed write still lands in the hour it logically belongs to. * `{unix_timestamp}_{random_hex}.jsonl.gz` — a unique object name within the hour. Each object decompresses to JSON Lines: one JSON object per line, one event per line. ## Object frequency [Section titled “Object frequency”](#object-frequency) Honeybadger periodically writes new objects into your bucket for each active stream. The exact cadence isn’t guaranteed and may vary over time, so the important model to keep in mind is: > Concatenating every object under a stream’s prefix gives you that stream’s full event history. No single object contains all of a stream’s events — each object is a fragment. To reconstruct events for a time range, list every object under `insights/{stream_id}/{YYYY}/{MM}/{DD}/{HH}/` for the hours you care about and concatenate their decompressed contents. Tools like AWS Athena, DuckDB, ClickHouse, and most data warehouses can read directories of gzipped JSON Lines files directly without needing to merge them yourself. ## Status, pauses, and errors [Section titled “Status, pauses, and errors”](#status-pauses-and-errors) Each destination is in one of three states: * **Active** — at least one stream is attached and writes are succeeding. * **Paused** — the destination is configured but no streams are attached. Attach a stream to start. * **Errored** — Honeybadger has stopped writing to the bucket. Events for attached streams are dropped until you fix the destination and reactivate it. Honeybadger transparently handles short-term failures on the bucket’s side — we retry and buffer events through network blips, timeouts, and brief outages, so most disruptions recover without you noticing. If a destination keeps failing or hits a problem we can’t recover from on our own (invalid credentials, a missing bucket, a permission change), we move it to the **errored** state and email the account owner with the details. The destination card shows the last error message and when it occurred. To recover, fix the underlying issue and either save the destination with corrected credentials — a successful save reactivates it automatically — or click **Reactivate** if only a transient issue needed clearing. If the underlying problem isn’t actually fixed, the next archive write will flip the destination back to errored. ## Deleting a destination [Section titled “Deleting a destination”](#deleting-a-destination) Deleting a destination immediately stops new writes for any attached streams. Objects that have already been written to your bucket are **not** deleted — they’re yours, and Honeybadger never reads or removes them after upload. If you want to fully clean up, delete the bucket (or the objects under your prefix) yourself once you no longer need the archived data.
# BadgerQL guide
> Learn how to use BadgerQL to query your log events and observability data in Honeybadger Insights.
BadgerQL is the language you use to interact with your data stored in Insights. It was designed to enable you to enrich, shape, and combine your events so you can craft any view of your data. Quick reference docs are also available in the application via the book icon in the top-right corner of the query box.  We also provide inline hints in the query editor that show info from the quick reference docs as you type:  Need a hand crafting BadgerQL queries? The natural language query translator can [translate plain-English descriptions](/guides/insights/#natural-language-queries) into queries, visualizations, and time ranges. ## Example queries and use cases [Section titled “Example queries and use cases”](#example-queries-and-use-cases) Find N+1 queries in your Rails app:
```badgerql
filter event_type::str == "sql.active_record"
| stats count() as queryCt, sum(duration::float) by request_id::str, query::str
| sort queryCt desc
```
 What events are consuming my Insights quota? Be sure to deselect the [Internal Stream](/guides/insights/#streams) so you only see the data you are sending:
```badgerql
stats sum(@size) as size by event_type::str
| sort size
| only toHumanString(size, "bytes"), event_type
```
 See more examples in the [walk-through](/guides/insights/#walk-through) or review the full BadgerQL reference below for more information. ## Parameterized queries [Section titled “Parameterized queries”](#parameterized-queries) Parameterized queries let you swap values into a query at runtime without editing the query itself. You can filter a dashboard to a single host, environment, or customer; share a prefilled URL with a teammate; or reuse the same widget across multiple contexts. Parameters work anywhere you write BadgerQL, including dashboard widgets and the Insights query editor. Use `${name}` to reference a parameter in a query:
```badgerql
filter hostname::str == "${hostname}"
```
Provide a default with `${name:-default}`:
```badgerql
filter env::str == "${env:-production}"
```
Parameter names must start with a letter or underscore, followed by letters, numbers, or underscores. Parameter values can be provided in the URL (e.g., `?hostname=web-01`), allowing you to share query URLs with prefilled values, or by clicking the parameters button (the slider icon in the dashboard toolbar, next to the date picker) to open a popover with a field for each parameter used in the query. ## Functions to enrich, shape, and combine data [Section titled “Functions to enrich, shape, and combine data”](#functions-to-enrich-shape-and-combine-data) Functions are the core of BadgerQL. You can think of your data falling or piping through each function that you specify, getting filtered, aggregated, and so on along the way. The most common functions you will use are [`fields`](#fields) to select fields to view, [`filter`](#filter) to restrict what data appears in the results, and [`stats`](#stats) to do counts, averages, and other analyses. Keep reading to learn about all the functions we offer. ### Combining functions [Section titled “Combining functions”](#combining-functions) While calling a BadgerQL function on its own can produce interesting results, the real power comes when piping functions together via the pipe (`|`) operator:
```badgerql
fields status_code::int, controller::str
| filter startsWith(controller, "Stripe")
| stats count() by status_code
```
Each function builds off the other to create a result showing the distribution of status codes just for Stripe controller requests. Note that BadgerQL does not work like SQL. Each successive function is applied to the result of the previous, so you can only reference fields down the pipeline. For example, if you want to convert a string to a number gathered from a `parse` function, you can pipe into another `fields` function:
```badgerql
parse url::str /id=(?\d+)/
| fields toInt(id) as id
```
### Expand [Section titled “Expand”](#expand) You can use `expand` to turn an event that has a field with array data into multiple events.
```badgerql
expand array_field [as alias][, ...]
```
With data that has a single event like `{"id": 1, "charges": [700, 430, 200]}`, the following query will return three events, with `id` and `charge` fields:
```badgerql
expand charges[*]::int as charge
```
See the [Arrays](#arrays) section for more detail on working with array data. ### Fill [Section titled “Fill”](#fill) Use `fill` to inject events for missing data points.
```badgerql
fill field_expression [as alias] [asc|desc|up|down] [from ...] [to ...] [step ...] [across field [bounded | including [...]]]* [with field[ = expression][, ...]*]
```
Unless specified with `from` or `to`, `fill` will determine the min and max values of the `field_expression`, sort, and produce new events with missing `field_expression` values replaced by the incremented or decremented step value. `field_expression` only allows for `number` or `temporal` types. The resulting optional clause types differ based on the resolved type:
```badgerql
fill number [from number] [to number] [step number]
fill temporal [from temporal] [to temporal] [step interval]
```
Fill works best when referencing an already existing field. Since fill inserts data at a regular interval, you will also get the best results if the field follows the step size of the fill. #### Typical usage [Section titled “Typical usage”](#typical-usage) Take a `stats` call that bins the count of events per hour:
```badgerql
stats count() as ct by bin(1h) as bin
```
You might get sparse results if there is not enough data to fill each bin: | ct | bin | | -- | ----------------------- | | 5 | 2023-04-05 02:00:00.000 | | 10 | 2023-04-05 04:00:00.000 | | 2 | 2023-04-05 06:00:00.000 | With the `fill` function (the step is inferred from `bin()`, so a bare `fill bin` is enough):
```badgerql
stats count() as ct by bin(1h) as bin
| fill bin
```
You can produce a full binned result set: | ct | bin | | -- | ----------------------- | | 5 | 2023-04-05 02:00:00.000 | | 0 | 2023-04-05 03:00:00.000 | | 10 | 2023-04-05 04:00:00.000 | | 0 | 2023-04-05 05:00:00.000 | | 2 | 2023-04-05 06:00:00.000 | #### Automatic step from `bin()` and `bucket()` [Section titled “Automatic step from bin() and bucket()”](#automatic-step-from-bin-and-bucket) If the fill field comes from a `bin()` or `bucket()`, you don’t need to repeat the step. `fill` picks it up automatically. `bin(1h)` gives you a 1-hour step, `bucket(x, 100)` gives you a 100-wide step, and the bounded form `bucket(x, 0, 1000, 20)` gives you from, to, and step all at once. You can still pass an explicit `step`, `from`, or `to` to override.
```badgerql
stats count() as ct by bucket(duration::int, 0, 2000, 20) as ms
| fill ms
| sort ms asc
```
The bucket width is `2000 / 20 = 100`, so `fill` inserts a row for every 100-wide slot that had no matching events: | ct | ms | | -- | --- | | 12 | 0 | | 0 | 100 | | 0 | 200 | | 45 | 300 | | 30 | 400 | | 0 | 500 | | … | … | #### Filling across dimensions [Section titled “Filling across dimensions”](#filling-across-dimensions) Add `across ` to fill every combination of the fill field and a grouping dimension. This is useful for stacked charts, heatmaps, or any per-category series where you want explicit zeros instead of missing rows.
```badgerql
stats count() as count by bin(1h) as t, status::str
| fill t across status
```
Every combination of time bin and status gets a row, with 0 for missing cells: | count | t | status | | ----- | ----------------------- | ------ | | 8 | 2023-04-05 02:00:00.000 | 200 | | 0 | 2023-04-05 02:00:00.000 | 500 | | 0 | 2023-04-05 03:00:00.000 | 200 | | 3 | 2023-04-05 03:00:00.000 | 500 | You can chain multiple `across` clauses:
```badgerql
stats count() as count by bin(1h) as t, status::str, region::str
| fill t across status across region
```
Counting aggregates (`count`, `sum`, `unique` and their `*If` variants) default to 0 on filled cells. Everything else defaults to null. Use `with field = value` to pick a different default. ##### `bounded` [Section titled “bounded”](#bounded) Without `bounded`, `across` fills every category across the entire range of the fill field. Say `temp` reported from 02:00–04:00 and `humidity` only reported at 05:00. Plain `across` would create rows for both sensors across the full 02:00–05:00 range. `across field bounded` limits each category to its own observed range instead:
```badgerql
stats count() as count by bin(1h) as t, sensor::str
| fill t across sensor bounded
```
| count | t | sensor | | ----- | ----------------------- | -------- | | 5 | 2023-04-05 02:00:00.000 | temp | | 0 | 2023-04-05 03:00:00.000 | temp | | 3 | 2023-04-05 04:00:00.000 | temp | | 7 | 2023-04-05 05:00:00.000 | humidity | No rows for `humidity` at 02:00–04:00, and no rows for `temp` at 05:00. You can’t combine `bounded` with `including` on the same dimension, or with explicit `from`/`to`. ##### `including` [Section titled “including”](#including) `across field including [...]` ensures specific values show up in the result even if they’re missing from the data. The pinned values are added on top of whatever the query discovers, so you won’t lose any existing categories. All values in the array must be the same type.
```badgerql
stats count() as count by bin(1h) as t, op::str
| fill t across op including ["create", "delete"]
```
The `"create"` and `"delete"` values appear even if the data only contains `"update"` events: | count | t | op | | ----- | ----------------------- | ------ | | 0 | 2023-04-05 02:00:00.000 | create | | 0 | 2023-04-05 02:00:00.000 | delete | | 4 | 2023-04-05 02:00:00.000 | update | | 1 | 2023-04-05 03:00:00.000 | create | | 0 | 2023-04-05 03:00:00.000 | delete | | 0 | 2023-04-05 03:00:00.000 | update | This is also useful for keeping chart legends stable. If a category has zero events across the entire query range, `across` alone won’t include it. `including` pins those categories into the result so they always appear. #### Fill order [Section titled “Fill order”](#fill-order) By default, the fill function sorts the `field_expression` in ascending order before injecting fill events. You can change this by providing an order direction after the `field_expression`:
```badgerql
fill @ts desc step -1h
```
When filling in descending order, `from` must be greater than `to` and `step` must be a negative value. #### `@fill` internal field [Section titled “@fill internal field”](#fill-internal-field) Filled events have an additional internal `@fill` field added to the results. You can use this field to determine when an event is filled:
```badgerql
fields @fill
| fill duration::int from 100 to 500 step 100
```
| @fill | duration | | ----- | -------- | | true | 200 | | true | 300 | | | 325 | | true | 400 | #### Filling other fields [Section titled “Filling other fields”](#filling-other-fields) Most fields other than `field_expression` will be filled with a null value for injected events. You can control what data is replaced using the `with` clause. Setting the `with` field to the `field_expression` will result in an error. If `with` is given only a field, it will carry over the field value from the previous event:
```badgerql
fields @fill, controller::str
| fill duration to 340 step 10 with controller
```
| @fill | duration | controller | | ----- | -------- | ---------- | | | 300 | login | | true | 310 | login | | | 320 | sign-up | | true | 330 | sign-up | `with` fields can also be set to specific values for filled events:
```badgerql
stats avg(temp::float) as avgTemp by bin(1d) as bin
| fill bin step 1d with avgTemp = 65.0
```
| avgTemp | bin | | ------- | ---------- | | 73.3 | 2023-04-08 | | 65.0 | 2023-04-09 | | 68.9 | 2023-04-10 | | 65.0 | 2023-04-11 | Referencing other fields from previous events is also possible, acting like a `LAST_VALUE()` window function. #### Notes [Section titled “Notes”](#notes) * Having multiple fills is possible by piping together `fill` functions, but take care to ensure you are not injecting too many events. * `from` and `to` values are not inclusive when producing injected results. ### Fields [Section titled “Fields”](#fields) The `fields` function enriches your results by adding extra fields. Any fields that you select or alias can be referenced in later functions, and they will be returned in the final dataset unless rewritten by later functions.
```badgerql
fields expr [as alias][, ...]*
```
Fields can be aliased with the `as` clause, and unsupported characters (like spaces) can be used by using backticks.
```badgerql
fields user_name::str as `User name`
```
Aliased fields can be used in later functions:
```badgerql
fields concat(first_name::str, " ", last_name::str) as full_name
| filter full_name match /^Bob.*/
```
#### Internal fields [Section titled “Internal fields”](#internal-fields) We set the following internal fields for you as the data is ingested: | Name | Type | Description | | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `@id` | `String` | The event ID | | `@ts` | `DateTime` | The reported timestamp if provided as `ts` or `timestamp`; otherwise, the time when the event was received | | `@received_ts` | `DateTime` | The time when the event was received | | `@stream.id` | `String` | The ID of the stream that contains the event. Each project contains at least two streams: the internal Honeybadger stream used for notices, etc., and the stream used for storing events that you send to our API. | | `@stream.name` | `String` | The name of the stream | | `@query.start_at` | `DateTime` | The timestamp of start of the range queried. E.g., when searching back 3 hours (the default), this will be three hours ago | | `@query.end_at` | `DateTime` | The timestamp of end of the range queried. E.g., for the default query, this would be the time when the query was executed, since the default query searches for data up to the time the query was sent. | | `@size` | `Integer` | The size in bytes of the event | | `@fill` | `Boolean` | Whether the result has filled-in values | | `@preview` | `JSON Object` | A preview of the data stored for the event | ### Filter [Section titled “Filter”](#filter) Filter expects a body that results in a boolean expression, and it will exclude events where the expression returns false.
```badgerql
filter boolean_expr [and|or ...]*
```
Multiple piped filter functions will act as AND operations.
```badgerql
filter controller_name::str == "StripeController" and duration::float > 2000
| filter action_name::str == "hook"
```
### Limit [Section titled “Limit”](#limit) Limit the number of results returned by the query.
```badgerql
limit integer [by expr[, ...]*]
```
Caution Limiting can adversely affect piped function results. For example, adding a `limit` before a `stats` call will only gather stats on the limited events:
```badgerql
limit 10
| stats count() by controller::str
```
If you want to restrict the number of returned results, make sure `limit` is at the end of your pipeline:
```badgerql
stats count() by controller::str
| limit 10
```
Include a `by` clause to limit the number of results per group.
```badgerql
limit 10 by user_id::int
```
Pipe into `limit` to restrict the final number of results returned by the query.
```badgerql
limit 5 by user_id::int
| limit 100
```
### Only [Section titled “Only”](#only) Use `only` to restrict which fields are rendered in the results and in which order they will appear.
```badgerql
only expr [as alias][, ...]*
```
For example, if you want to filter on a particular field, but you don’t want that field to appear in the results, you can use `only` to select the fields you want to see:
```badgerql
fields a, b, c
| filter c > 2
| only b, a
```
### Parse [Section titled “Parse”](#parse) Extract fields using regular expressions
```badgerql
parse expr /regex/
```
If your events have data that can be extracted using regular expressions, you can create fields from that data. The following example will extract “redis” from an event that has a field named “addon” that contains the value “redis-fitted-71581” and place it in a new field called “service”. Both the “addon” and “service” fields will appear in the results.
```badgerql
fields addon::str
| parse addon /(?[[:alpha:]]+)/
```
### Sort [Section titled “Sort”](#sort) Order events based on fields.
```badgerql
sort expr [desc|asc][, ...]*
```
Queries without an explicit sort are unordered and non-deterministic. Sort direction can be either `desc` (descending) or `asc` (ascending). By default, fields are sorted in descending order if not specified.
```badgerql
sort day desc, duration asc
```
Sort is useful to order results by time, or when calculating stats:
```badgerql
fields email
| filter action::str == "Logged in"
| stats count() as count by email
| sort count
```
It can make sense to call `sort` multiple times, as sorting after rewriting functions might be necessary. ### Stats [Section titled “Stats”](#stats) Aggregate event fields
```badgerql
stats agg_expr[, ...]* by [expr][, ...]*
```
The workhorse of Insights, `stats` allows you to perform calculations on your data. You can count events, calculate averages, and more.
```badgerql
stats avg(response_time::float)
```
#### Aggregation [Section titled “Aggregation”](#aggregation) Available aggregate functions: | Function | Description | | ------------------------------- | ---------------------------------------------------------------------------------------- | | `count()` | Returns the total count of all results. Can contain an expression that filters the count | | `avg(field)` | Calculates the average (mean) value for a numeric field | | `min(field)`, `max(field)` | Returns the minimum/maximum value for the given field | | `sum(field)` | Calculates the sum of values for a numeric field | | `percentile(percentage, field)` | Returns the value at the specified percentile for the given numeric field | | `unique(field)` | Returns the number of unique values for the specified field | | `first(field)`, `last(field)` | Returns the first/last value of the specified field for the whole aggregate | | `apdex(field, threshold)` | Calculates an Apdex (Application Performance Index) score between 0 and 1 | Find the number of 500 errors over a time period:
```badgerql
filter status_code::int == 500
| stats count()
```
Find the average response time for a specific endpoint:
```badgerql
filter endpoint::str == "/api/v1/orders"
| stats avg(duration::float)
```
Combine multiple aggregate functions in a single query:
```badgerql
filter environment::str in ["production", "staging"]
| stats count(), percentile(95, duration::float)
```
#### Grouping [Section titled “Grouping”](#grouping) The `by` clause allows you to group the results by one or more fields.
```badgerql
stats avg(response_time::float) by location::str
```
One of the most common use cases for grouping is to create a time series by grouping with `bin()`. #### The `bin()` function [Section titled “The bin() function”](#the-bin-function) `bin()` rounds a datetime down to the nearest interval boundary, which lets you group events into time buckets (e.g., “all events in this 1-hour window”).
```plaintext
bin([interval[, datetime]])
```
Both arguments are optional: * **`interval`** — the bucket size, written using interval syntax (e.g. `1h`, `30m`, `2d`). If omitted, `bin()` automatically picks a reasonable size based on your selected time range. * **`datetime`** — the field to bin. Defaults to `@ts`. Use this when you want to bin on a field other than the event timestamp.
```badgerql
stats count() by bin(1h) as time
```
```badgerql
stats count() by bin(1h, toDateTime(user.created_at::str)) as time
```
When no alias is given, the result column is named after the call itself (e.g. `bin(1h)`). Always alias `bin()` when you need to reference it in a later function like `sort` or `fill`. **Interval syntax** — an integer followed by a unit abbreviation: | Unit | Abbreviation | Example | | ------- | ------------ | ------- | | Seconds | `s` | `30s` | | Minutes | `m` | `15m` | | Hours | `h` | `1h` | | Days | `d` | `7d` | | Weeks | `w` | `1w` | | Months | `mon` | `1mon` | **Auto-sizing** — when `bin()` is called with no interval argument, the bin size is automatically chosen to produce a reasonable number of buckets for your selected time range.
```badgerql
stats count() by bin() as time, status_code::int
```
You can use any field or expression in the `by` clause:
```badgerql
stats avg(duration::float), max(duration::float)
by bin() as time, concat(controller::str, "#", action::str) as controllerAction
```
### Unique [Section titled “Unique”](#unique) The `unique` function filters out duplicate events based on the field(s) you specify.
```badgerql
unique field[, ...]
```
### Toggling functions [Section titled “Toggling functions”](#toggling-functions) **Hotkey: `CTRL + /`** When exploring data in BadgerQL, you might find it useful to temporarily ignore certain functions while keeping them in the query. To do this, add a bang (`!`) at the beginning of the BadgerQL function. This comments out the function, effectively ignoring it without removing it from the query. This is particularly useful for toggling conditions in statistical analyses. For example, you might want to alternate between including and excluding certain filters:
```badgerql
fields event_type::str, duration::int
| filter event_type == "page_view"
| !filter duration > 100
| stats count() by bin(1d)
```
Note that if a function spans multiple lines, placing a bang (`!`) at the beginning will toggle the entire function, not just the first line:
```badgerql
fields event_type::str, duration::int
| filter event_type == "page_view"
| !filter
duration > 100 and
duration < 200
| stats count() by bin(1d)
```
## Types [Section titled “Types”](#types) In Insights, data is stored and accessed in its typed format. BadgerQL is a strongly typed language, which means it is particular about type consistency. We currently support storing data with these types: | Short | Long | | ------- | ------- | | `str` | String | | `bool` | Boolean | | `float` | Float | | `int` | Integer | ### Type hinting [Section titled “Type hinting”](#type-hinting) Type hinting is key in BadgerQL. You indicate the expected field type using `::` and the short type name. For example, if you know you are sending status codes as integers, you must augment your query to point to the field like:
```badgerql
fields status_code::int
```
This only gives the system a hint for where to look for the event field. It does not coerce the value into another type. If you want to convert types, use one of the [conversion expression functions](#conversion). It’s not required to repeat type hints. If you use a field with a type hint earlier, it carries over:
```badgerql
fields status_code::int
| stats count() by status_code
```
Conflicting type hints or inaccurate hints can result in null values or errors. We also support using these types (either through conversion or as a function result) in queries: | Short | Long | | ------------ | ----------------------------------- | | `datetime` | Datetime | | `date` | Date | | `tzdatetime` | Datetime with timezone | | `interval` | Relative time intervals (e.g. `1h`) | **Note:** you can’t hint these types, as we don’t store data in these formats. ### Union types [Section titled “Union types”](#union-types) You may see `number` and `temporal` appear in function signatures throughout the docs. These are not types you can use directly in queries; they are shorthand for describing which concrete types a function accepts. `number` means the function works with either `int` or `float`, and `temporal` means it works with either `date` or `datetime`. ### Literal values [Section titled “Literal values”](#literal-values) Some function arguments don’t accept field references, only literals (e.g., `1.5`, `"hi"`). This is denoted in the type signature. For instance, `round(duration::float, 0)` is valid with the second argument as a literal integer. `round(duration::float, precision::int)` would produce an error. ## Dates [Section titled “Dates”](#dates) ### Creating dates [Section titled “Creating dates”](#creating-dates) We provide a shorthand for creating datetime literal values by wrapping the date in curly brackets `{}`:
```badgerql
fields {2023-01-01} as baseDate
```
### Casting dates [Section titled “Casting dates”](#casting-dates) There is no way to store native dates in Insights, so if you want to interact with a native `date` or `datetime`, you will need to cast a string column to one of the temporal types:
```badgerql
fields toDateTime(created_at::str) as created_at
| filter created_at > {2023-04-08 12:00:00}
```
### Timezones [Section titled “Timezones”](#timezones) All datetimes are returned in your selected timezone by default. This means that if you input a datetime, it will be automatically converted to match your preferred timezone setting. To adjust datetimes to a specific timezone for a query, use the `toTimezone` function:
```badgerql
fields toTimezone(@ts, "America/Los_Angeles")
```
This will show the timestamp in PST, which will be denoted in the timezone information contained within the field type (`tzdatetime.PST` for this example). ## Arrays [Section titled “Arrays”](#arrays) Insights is primarily designed to work with simple key/value data mappings, however, it does support ingesting and querying array data in your events. To access fields within an array, use bracket notation to specify an index. For example, `user.scopes[0].name::str` is a valid path into your event data. ### Expand function [Section titled “Expand function”](#expand-function) The most flexible tool for working with arrays is the `expand` BadgerQL function. `expand` unwraps array data into individual events, which you can then pipe into any other function. For example, given events containing this data:
```json
{"id": 1, "charges": [700, 430, 200]}
{"id": 2, "charges": [100]}
```
You can expand the charges field using wildcard notation:
```badgerql
expand charges[*]::int as charge
```
This will expand each result to: | id | charge | | -- | ------ | | 1 | 700 | | 1 | 430 | | 1 | 200 | | 2 | 100 | **Note:** Just like looking up a field, the path must reference a set of values. You can’t expand into an object or another array. You can then use `stats` to group events back together after processing:
```badgerql
expand charges[*]::int as charge
| filter charge > 200
| stats sum(charge) as total_cost by id
```
Which will combine the filtered events back with summed charges: | id | total\_cost | | -- | ----------- | | 1 | 1130 | ### Conditional array matching [Section titled “Conditional array matching”](#conditional-array-matching) Sometimes you want to know if a value within an array passes some condition. We have [special expression functions](#arrays) just for this case. For example, to find events with a specific tag:
```badgerql
filter any(tags[*]::str == "funky")
```
The `any` function also works with nested object data within an array:
```badgerql
filter any(events[*].user.email::str like "kwebster%")
```
### Performance implications [Section titled “Performance implications”](#performance-implications) Array support is limited in terms of performance optimizations. Where possible, consider flattening array data into separate events before sending them to Honeybadger. ## Expression functions [Section titled “Expression functions”](#expression-functions) Expression functions can be used in a variety of places, such as filtering data, creating fields, calculating aggregates, etc. They are used to compare fields, perform arithmetic, reformat data, and more. ### Comparison [Section titled “Comparison”](#comparison) The comparison operators work across `number`, `string`, `boolean`, and `datetime` types. `!=` and `<>` are equivalent operators. `between` and `not between` are inclusive on both ends:
```badgerql
filter status_code::int between 200 and 299
```
`either` returns the first non-null value from its arguments — useful as a fallback when a field may be stored under different names:
```badgerql
fields either(name::str, full_name::str, username::str) as name
```
* `!=` Inequality comparison. Also written `<>`. Signature`t = number | string | boolean | datetime``t != t -> boolean` Example
```sql
fields status_code::int != 200
```
* `<` Signature`t = number | string | boolean | datetime``t < t -> boolean` Example
```sql
fields status_code::int < 500
```
* `<=` Signature`t = number | string | boolean | datetime``t <= t -> boolean` Example
```sql
fields status_code::int <= 200
```
* `<>` Inequality comparison. Also written `!=`. Signature`t = number | string | boolean | datetime``t <> t -> boolean` Example
```sql
fields status_code::int <> 200
```
* `==` Signature`t = number | number[] | string | string[] | boolean | boolean[] | datetime | datetime[]``t == t -> boolean` Example
```sql
fields status_code::int == 200
```
* `>` Signature`t = number | string | boolean | datetime``t > t -> boolean` Example
```sql
fields status_code::int > 500
```
* `>=` Signature`t = number | string | boolean | datetime``t >= t -> boolean` Example
```sql
fields status_code::int >= 200
```
* `between` Signature`t = number | string | datetime``t between t and t -> boolean` Example
```sql
filter status_code::int between 200 and 300
```
* `coalesce` Returns the first non-null value. Synonym of `either`. Signature`t = integer | float | string | boolean | datetime``coalesce(t, ...t) -> t` Example
```sql
fields coalesce(name::str, full_name::str, username::str) as name
```
* `either` Returns the first non-null value. Also accepts `coalesce`. Signature`t = integer | float | string | boolean | datetime``either(t, ...t) -> t` Example
```sql
fields either(name::str, full_name::str, username::str) as name
```
* `ilike` Returns true when the search string matches Can use these metacharacters: `%` - Matches an arbitrary amount of characters `_` - Matches single arbitrary character The matcher is case insensitive Signature`string ilike string -> boolean` Example
```sql
filter email::str ilike "%compuserve%"
```
* `in` Return true if field value is contained within the array of literal values. The field type must match value type in the array. Signature`t = number | string | datetime``t in t[] -> boolean` Example
```sql
filter status_code::int in [300, 301, 404]
```
* `isNotNull` Signature`t = number | string | boolean | datetime``isNotNull(t) -> boolean` Example
```sql
filter isNotNull(status_code::int)
```
* `isNull` Signature`t = number | string | boolean | datetime``isNull(t) -> boolean` * `like` Returns true when the search string matches Can use these metacharacters: `%` - Matches an arbitrary amount of characters `_` - Matches single arbitrary character The string matcher is case sensitive Signature`string like string -> boolean` Example
```sql
filter email::str like "%compuserve%"
```
* `match` Returns true when the regex matches The regex uses [re2 regex syntax](https://github.com/google/re2/wiki/Syntax) Signature`string match regex -> boolean` Example
```sql
filter email::str match /.*compuserve.*/
```
* `not between` Signature`t = number | string | datetime``t not between t and t -> boolean` Example
```sql
filter status_code::int not between 300 and 400
```
* `not ilike` Returns true when the search string does not match Can use these metacharacters: `%` - Matches an arbitrary amount of characters `_` - Matches single arbitrary character The matcher is case insensitive Signature`string not ilike string -> boolean` Example
```sql
filter email::str not ilike "%compuserve%"
```
* `not in` Return true if field value is not contained within the array of literal values. The field type must match value type in the array. Signature`t = number | string | datetime``t not in t[] -> boolean` Example
```sql
filter status_code::int not in [300, 301, 404]
```
* `not like` Returns true when the search string does not match Can use these metacharacters: `%` - Matches an arbitrary amount of characters `_` - Matches single arbitrary character The string matcher is case sensitive Signature`string not like string -> boolean` Example
```sql
filter email::str not like "%compuserve%"
```
* `not match` Returns true when the regex does not match The regex uses [re2 regex syntax](https://github.com/google/re2/wiki/Syntax) Signature`string not match regex -> boolean` Example
```sql
filter email::str not match /.*compuserve.*/
```
### Arrays [Section titled “Arrays”](#arrays-1) * `all` Return true if the predicate is true for every element of an expanded array. Returns true on empty arrays (vacuous truth). Signature`all(boolean) -> boolean` Example
```sql
filter all(tags[*]::str != "severe")
```
```sql
filter all(coupon_ids[*]::int not in [123, 456])
```
The predicate must reference at least one expanded array (a field with `[*]`). That tells `all()` which array to iterate over.
```sql
filter all(tags[*]::str != "severe")
```
### Empty arrays `all()` returns `true` on an empty array — there are no elements to violate the predicate. This is mathematically consistent (vacuous truth) but bites people who expect "all" to imply "at least one." If you need both "non-empty" and "all match," combine `all()` with a separate `any()` check. ### Nested object data `[*]` works inside object paths, so you can require a property on every element of an array of objects:
```sql
filter all(events[*].status::str == "ok")
```
### Performance Array operations don't benefit from the same indexing that scalar fields do. If you find yourself querying array data heavily, consider sending the events with the array already unrolled. * `any` Return true if the predicate is true for at least one element of an expanded array. Returns false on empty arrays. Signature`any(boolean) -> boolean` Example
```sql
filter any(tags[*]::str == "severe")
```
```sql
filter any(coupon_ids[*]::int in [123, 456])
```
The predicate must reference at least one expanded array (a field with `[*]`). That tells `any()` which array to iterate over.
```sql
filter any(tags[*]::str == "severe")
```
### Nested object data `[*]` works inside object paths, so you can check fields on each element of an array of objects:
```sql
filter any(events[*].user.email::str like "kwebster%")
```
### Empty arrays `any()` returns `false` on an empty array — there's nothing to match. ### Comparison vs membership predicates The predicate inside `any()` can be anything that returns a boolean — equality, `in`/`not in`, `like`, range checks, or expressions on nested fields:
```sql
filter any(coupon_ids[*]::int in [123, 456])
filter any(prices[*]::float > 100.0)
```
You can't drop the `any()` and write `tags[*]::str in ["severe"]` directly — `in` needs a scalar on its left, and `tags[*]::str` is an array. `any()` is what unrolls the array and feeds each element into the predicate one at a time. ### Performance Array operations don't benefit from the same indexing that scalar fields do. If you find yourself querying array data heavily, consider sending the events with the array already unrolled. ### Array [Section titled “Array”](#array) * `contains` Returns true when the array contains the value. Use for simple array membership without writing `any(arr[*] == value)`. Signature`contains(string[], string) -> boolean``contains(number[], number) -> boolean` Example
```sql
filter contains(tags[*]::str, "severe")
```
* `dedupe` Removes duplicate elements from an array, keeping one copy of each value. Compose with `collect` to gather distinct values per group. Signature`t = string[] | number[] | boolean[] | datetime[]``dedupe(t) -> t` Example
```sql
fields dedupe(tags[*]::str) as tags
```
```sql
stats dedupe(collect(user_id::str)) as users by error_class::str
```
* `reverse` Returns the array with its element order reversed. Signature`t = string[] | number[] | boolean[] | datetime[]``reverse(t) -> t` Example
```sql
fields reverse(sort(scores[*]::int)) as descending_scores
```
* `sort` Returns the array sorted ascending. This is the array function `sort(...)`, not the pipeline stage `| sort ...`; compose with `reverse` for descending order. Signature`t = string[] | number[] | boolean[] | datetime[]``sort(t) -> t` Example
```sql
fields sort(scores[*]::int) as sorted_scores
```
* `subarray` Returns `length` elements of the array starting at `start`; array positions are 1-based, so `1` is the first element. Signature`t = string[] | number[] | boolean[] | datetime[]``subarray(t, integer, integer) -> t` Example
```sql
fields subarray(tags[*]::str, 1, 3) as first_three
```
### Logic [Section titled “Logic”](#logic) `if` is single-branch conditional: if the condition is true it returns the `then` value, otherwise it returns the `else` value. The `else` arm also fires when the condition evaluates to `null`.
```badgerql
fields if(status_code::int >= 500, "error", "ok") as result
```
`cond` is multi-branch: condition/value pairs are evaluated in order and the value from the first matching pair is returned. A final bare value (no preceding condition) acts as the fallback:
```badgerql
fields cond(
status_code::int >= 500, "red",
status_code::int >= 300, "yellow",
"green"
) as severity
```
* `and` Signature`boolean and boolean -> boolean` * `cond` Multiple path conditional branching The `cond()` function allows for evaluating branches (ala. `if` and `else if`) through positional arguments. Each successive pair of arguments acts as an else if, with the first true boolean passing it's result as a return." Signature`t = string | boolean | number | datetime | date``cond(boolean, t, boolean, t, ..., t) -> t` Example
```sql
fields cond(
status_code >= 300, "yellow",
status_code >= 500, "red",
"green"
) as status_code_color
```
* `if` Single path conditional branching Signature`t = string | number | boolean | temporal | interval``if(boolean, t, t) -> t` Example
```sql
fields if(toDayOfWeek(ts) == 2, "taco", "slop") as food_day
```
* `not` Signature`not(boolean) -> boolean` * `or` Signature`boolean or boolean -> boolean` ### Arithmetic [Section titled “Arithmetic”](#arithmetic) The standard operators (`+`, `-`, `*`, `/`, `%`) work on numbers. A few noteworthy behaviors: * Subtracting two `datetime` values returns the difference in **seconds** as an integer: `end_ts::datetime - start_ts::datetime` * Adding an interval to a datetime shifts it forward: `@ts + 1h` * The second argument to `round`, `floor`, and `ceil` is the number of decimal places and must be a **literal integer** — you cannot pass a field reference. `round(duration::float, 2)` is valid; `round(duration::float, precision::int)` is not. - `-` Signature`number - number -> number``datetime - number -> datetime``datetime - interval -> datetime``datetime - datetime -> integer` - `*` Signature`number * number -> number` - `/` Division. Dividing by an interval converts a number of seconds — such as a datetime difference — into that unit: `(finished - started) / 1h` is hours. Signature`t = number | interval``number / t -> float` Example
```sql
fields (toDateTime(finished_at::str) - toDateTime(started_at::str)) / 1h as hours
```
- `%` Signature`number % number -> number` - `+` Signature`number + number -> number``datetime + number -> datetime``datetime + interval -> datetime` - `abs` Signature`abs(number) -> number` - `bucket` Assign a numeric value to a bucket and return that bucket's start value. `bucket(value, width)` uses `width`-sized steps anchored at zero. `bucket(value, min, max, n)` divides `[min, max]` into `n` equal bucket slots; values outside that range return null. Signature`bucket(number, literal number) -> number``bucket(number, literal number, literal number, literal integer) -> number` Example
```sql
stats count() as ct by bucket(duration::int, 250) as ms
```
```sql
stats count() as ct by bucket(duration::int, 0, 5000, 16) as ms
```
The width form is the numeric counterpart of `bin()` for time: `bucket(duration::int, 100)` maps `250` to `200`, the start of its 100-wide bucket. Negative values land on the same grid (`-50` maps to `-100`). The bounded form fixes the range and bucket count instead: the width is `(max - min) / n`, so `bucket(duration::int, 0, 1000, 4)` creates starts at `0`, `250`, `500`, and `750`. `100` maps to `0`, `999` maps to `750`, and a value exactly equal to `max` also maps to the last bucket. Anything outside `[min, max]` returns null; filter the range first if you do not want an out-of-range null group. ### Histograms `bucket()` only assigns rows that already exist. Group by the bucket and count, then `fill` to make empty buckets explicit. The fill grid is inferred from the bucket — its width becomes the step, and the bounded form's min/max become from/to:
```sql
stats count() as ct by bucket(duration::int, 0, 2000, 20) as ms
| fill ms
| sort ms asc
```
### Why explicit parameters The width (or bounds and count) are part of the query, so the bucket grid is stable — the same query yesterday and today produces comparable buckets, and outliers can't warp the ranges. - `ceil` Signature`ceil(number, literal integer) -> float` - `exp` Signature`exp(number) -> float` - `floor` Signature`floor(number, literal integer) -> float` - `intDiv` Divide two numbers and return the integer quotient. Use `/` when you want a floating-point result. Signature`intDiv(number, number) -> integer` - `log` Signature`log(number) -> float` - `log10` Signature`log10(number) -> float` - `log2` Signature`log2(number) -> float` - `pow` Signature`pow(number, number) -> float` - `round` Signature`round(number, literal integer) -> float` - `sign` Returns -1 for negative numbers, 0 for zero, and 1 for positive numbers. Signature`sign(number) -> integer` - `sqrt` Signature`sqrt(number) -> float` - `truncate` Drop digits past the given number of decimal places without rounding. This is different from `floor`, which always rounds down. Signature`truncate(number, literal integer) -> float` ### Conversion [Section titled “Conversion”](#conversion) A few things worth knowing: * `toDateTime` from a string uses best-effort parsing, so it handles a wide variety of date formats (ISO 8601, RFC 2822, etc.) without needing an exact format string. * `toUnix` returns **milliseconds** since the Unix epoch, not seconds. * `toDate` strips the time component from a datetime and returns a date-only value. - `toDate` Signature`t = string | datetime``toDate(t) -> date` - `toDateTime` Signature`t = number | date | string | temporal``toDateTime(t) -> datetime` - `toFloat` Signature`toFloat(any) -> float` - `toInt` Signature`toInt(any) -> integer` - `toString` Signature`toString(any) -> string` - `toUnix` Signature`toUnix(datetime) -> integer` ### Dates [Section titled “Dates”](#dates-1) `now()` returns the current datetime in the query’s configured timezone. `toStartOf` and `toEndOf` are lower-level alternatives to `bin()` when you need the start or end of an interval boundary rather than grouping:
```badgerql
fields toStartOf(1w) as week_start
```
```badgerql
fields toEndOf(1d) as end_of_day
```
`toDayOfWeek` returns 1–7 where 1 = Monday and 7 = Sunday. See also the [Dates](#dates) section above for creating and casting date literals. * `bin` Round a datetime down to the nearest interval boundary. Most often used in `stats ... by bin(...)` to bucket events into a time series. Signature``bin(datetime = `@ts`) -> datetime````bin(interval, datetime = `@ts`) -> datetime`` Example
```sql
fields bin(1w) as beginning_of_week
```
```sql
stats count() by bin(1h, toDateTime(user.created_at::str))
```
### Choosing the interval If you pass an interval, that's the bin size:
```sql
stats count() by bin(1h)
```
If you omit the interval, `bin()` picks a size based on the query's time range — small bins for short ranges, larger bins for longer ones. The exact thresholds aren't fixed, so pass an explicit interval if you need a specific size. ### Choosing the field By default `bin()` operates on the event timestamp (`@ts`). Pass a datetime field as the second argument to bin against something else:
```sql
stats count() by bin(1d, toDateTime(user.created_at::str))
```
### Filling gaps Bins with no matching events don't appear in the result. To produce a continuous series, pipe through `fill` — the step is inferred from the bin:
```sql
stats count() by bin(1h) as t
| fill t
```
* `formatDate` Render a datetime as a string using a format pattern. Defaults to the event timestamp (`@ts`) if no datetime is given. Signature``formatDate(literal string, datetime = `@ts`) -> string`` Example
```sql
fields formatDate("%Y-%m-%d") as day
```
```sql
stats count() by formatDate("%a", @ts) as weekday
```
### Date tokens | | | | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `%j` | day of the year (001-366) | 002 | | `%d` | day of the month, zero-padded (01-31) | 02 | | `%e` | day of the month, space-padded (1-31) | 2 | | | | | | `%V` | ISO 8601 week number (01-53) | 01 | | `%w` | weekday as a integer number with Sunday as 0 (0-6) | 2 | | `%u` | ISO 8601 weekday as number with Monday as 1 (1-7) | 2 | | `%a` | abbreviated weekday name (Mon-Sun) | Mon | | `%W` | full weekday name (Monday-Sunday) | Monday | | | | | | `%m` | month as an integer number (01-12) | 01 | | `%M` | full month name (January-December) | January | | `%b` | abbreviated month name (Jan-Dec) | Jan | | `%Q` | Quarter (1-4) | 1 | | | | | | `%y` | Year, last two digits (00-99) | 18 | | `%Y` | Year | 2018 | | `%C` | year divided by 100 and truncated to integer (00-99) | 20 | | `%g` | two-digit year format, aligned to ISO 8601, abbreviated from four-digit notation | 18 | | `%G` | four-digit year format for ISO week number, calculated from the week-based year defined by the ISO 8601 standard, normally useful only with %V | 2018 | | | | | | `%D` | Short MM/DD/YY date, equivalent to %m/%d/%y | 01/02/18 | | `%F` | short YYYY-MM-DD date, equivalent to %Y-%m-%d | 2018-01-02 | ### Time tokens | | | | | ---- | ------------------------------------------------------- | -------- | | `%s` | second (00-59) | 44 | | `%S` | second (00-59) | 44 | | `%f` | fractional second | 1234560 | | | | | | `%i` | minute (00-59) | 33 | | | | | | `%h` | hour in 12h format (01-12) | 09 | | `%I` | hour in 12h format (01-12) | 10 | | `%H` | hour in 24h format (00-23) | 22 | | `%l` | hour in 12h format (01-12) | 09 | | `%k` | hour in 24h format (00-23) | 22 | | `%r` | 12-hour HH:MM AM/PM time, equivalent to %H:%i %p | 10:30 PM | | `%R` | 24-hour HH:MM time, equivalent to %H:%i | 22:33 | | | | | | `%p` | AM or PM designation | PM | | `%T` | ISO 8601 time format (HH:MM:SS), equivalent to %H:%i:%S | 22:33:44 | | `%z` | Time offset from UTC as +HHMM or -HHMM | -0500 | ### Other tokens | | | | | ---- | ------------------------ | - | | `%n` | new-line character | | | `%t` | horizontal-tab character | | | `%%` | a % sign | % | * `now` Signature`now() -> datetime` * `toDay` Returns the day of month (1-31) for the supplied datetime. Signature`toDay(datetime) -> integer` * `toDayOfWeek` Returns the number of the day in a week (1-7, 1 = monday) for the supplied datetime. Signature`toDayOfWeek(datetime) -> integer` * `toDayOfYear` Returns the day of the year (1-366) from a datetime. Signature`toDayOfYear(datetime) -> integer` * `toEndOf` Signature``toEndOf(interval, datetime = `@ts`) -> datetime`` * `toHour` Returns the 24-hour number (0-23) for the supplied datetime. Signature`toHour(datetime) -> integer` * `toMinute` Returns the minute of the hour (0-59) from a datetime. Signature`toMinute(datetime) -> integer` * `toMonth` Returns the month number (1-12) from a datetime. Signature`toMonth(datetime) -> integer` * `toSecond` Returns the second of the minute (0-59) from a datetime. Signature`toSecond(datetime) -> integer` * `toStartOf` Signature``toStartOf(interval, datetime = `@ts`) -> datetime`` * `toTimezone` Convert datetimes to a specific timezone. **Note:** This does not explicitly embed the timezone into the datetime, but updates the type to reflect the selected timezone (tzdatetime). Signature`toTimezone(datetime, literal string) -> datetime` * `toYear` Signature`toYear(datetime) -> integer` ### URL [Section titled “URL”](#url) * `urlBaseDomain` Extracts the registrable/base domain from a URL's hostname, so subdomains can be grouped together. Signature`urlBaseDomain(string) -> string` * `urlDomain` Extracts the hostname from a URL. Signature`urlDomain(string) -> string` * `urlParameter` Parse out value from valid URL query string Signature`urlParameter(string, literal string) -> string` Example
```sql
fields urlParameter(url::str, "user_id") as user_id_param
```
* `urlPath` Extracts the path from a URL. Example: `/hot/goss.html` The path does not include the query string. Signature`urlPath(string) -> string` * `urlPort` Extracts the explicit port from a URL, or returns 0 when the URL does not include one. Signature`urlPort(string) -> integer` * `urlProtocol` Extracts the URL protocol without `://`, for example `https`. Signature`urlProtocol(string) -> string` * `urlQueryString` Extracts the query string from a URL without the leading `?`, for example `page=2&sort=desc`. Signature`urlQueryString(string) -> string` ### Network [Section titled “Network”](#network) * `inCIDR` Returns true when the IP address falls within the CIDR range. Works for IPv4 and IPv6. The address must be a valid IP string. Malformed strings cause a query error. Signature`inCIDR(string, literal string) -> boolean` Example
```sql
filter inCIDR(client_ip::str, "10.0.0.0/8")
```
### Hashing [Section titled “Hashing”](#hashing) `cityHash64` and `xxHash64` are fast, non-cryptographic hashes for bucketing, sampling, or stable grouping. `MD5` and `SHA256` return hex strings for comparing against pre-hashed identifiers. * `cityHash64` Returns a fast, deterministic 64-bit hash of the value. Not cryptographic; use for bucketing, sampling, or stable grouping. Signature`cityHash64(any) -> integer` * `MD5` Returns the MD5 hash of a string as lowercase hexadecimal text. Useful for comparing against pre-hashed identifiers. Signature`MD5(string) -> string` * `SHA256` Returns the SHA-256 hash of a string as lowercase hexadecimal text. Useful for comparing against pre-hashed identifiers. Signature`SHA256(string) -> string` * `xxHash64` Returns a fast, deterministic 64-bit hash of the value. Not cryptographic; use for bucketing, sampling, or stable grouping. Signature`xxHash64(any) -> integer` ### JSON [Section titled “JSON”](#json) * `isValidJSON` Returns true when the string parses as JSON. Signature`isValidJSON(string) -> boolean` Example
```sql
filter isValidJSON(payload::str)
```
* `json` Extract a scalar value from a JSON string using a JSONPath expression. Returns null if the path doesn't resolve to a scalar — arrays and objects are not valid targets. Signature`json(string, literal string) -> string` Example
```sql
fields json(user_config::str, "$.login_info.last_login") as last_logged_in
```
### Path syntax Paths follow [JSONPath](https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html). Common patterns: | Path | Selects | | ---------------------- | ------------------------------------ | | `$.foo` | the value at key `foo` | | `$.foo.bar` | nested key `bar` under `foo` | | `$.items[0]` | the first element of an array | | `$.items[-1]` | the last element of an array | | `$['key with spaces']` | a key with non-identifier characters | ### Type handling `json()` returns the value as a string. To use it as a number or datetime, cast it with the appropriate conversion function:
```sql
fields toInt(json(payload::str, "$.user.id")) as user_id
```
### When it returns null * The path doesn't resolve (key missing, index out of range) * The path resolves to an object or array — only scalar values come back * The input isn't valid JSON ### Recommendation We support `json()` for ad-hoc digging into payloads, but querying it at scale is slower than querying real fields. If you find yourself reaching for it often on the same paths, send those values as top-level event fields instead. ### String [Section titled “String”](#string) `toHumanString` supports five format types: `"number"` (default), `"bytes"`, `"short"`, `"milliseconds"`, and `"microseconds"` for microsecond-precision duration fields. `startsWith` is a convenience wrapper around `like` — it is case-sensitive and does not accept wildcards in the match string. * `concat` Signature`concat(string, string...) -> string` * `editDistance` Returns the number of single-character edits (insertions, deletions, substitutions) needed to transform one string into the other. Lower values are more similar. Signature`editDistance(string, string) -> integer` Example
```sql
filter editDistance(error_message::str, "connection timed out") < 5
```
* `endsWith` Returns true when the first string ends with the second string. Signature`endsWith(string, string) -> boolean` Example
```sql
filter endsWith(file::str, ".rb")
```
* `length` Returns the number of characters in a string, or the number of elements in an array. Signature`t = string | string[] | number[] | boolean[] | datetime[]``length(t) -> integer` * `lowercase` Signature`lowercase(string) -> string` * `position` Returns the 1-based position of the first occurrence of the search string, or 0 when it is not found. Signature`position(string, string) -> integer` Example
```sql
fields position(message::str, "timeout") as timeout_at
```
* `replace` Replace all matches of a substring or regex pattern with another string. Signature`t = string | regex``replace(string, t, string) -> string` Example
```sql
fields replace(controller::str, /Controller/, "") as controller
```
* `replaceFirst` Replace the first match of a substring or regex pattern with another string. Signature`t = string | regex``replaceFirst(string, t, string) -> string` Example
```sql
fields replaceFirst(controller::str, /Controller/, "") as controller
```
* `similarity` Returns a 0-1 similarity score for two strings: 1 means identical, 0 means no similarity. Easier to threshold than `editDistance` when string lengths vary. Signature`similarity(string, string) -> float` Example
```sql
filter similarity(error_message::str, "connection timed out") > 0.9
```
* `split` Splits a string into an array of substrings around a literal separator. Null input returns an empty array. Signature`split(string, literal string) -> string[]` Example
```sql
fields split(tags::str, ",") as tag_list
```
* `startsWith` Signature`startsWith(string, string) -> boolean` * `substring` Signature`substring(string, integer, integer) -> string` Example
```sql
fields substring(token::str, 1, 3) as token_type
```
* `toHumanString` Transform a number into a human-readable string. Picks units, separators, and rounding based on the format type. Defaults to `"number"` (comma-separated) if no type is given. Signature`toHumanString(number, string = "number") -> string` Example
```sql
fields toHumanString(duration::int, "milliseconds")
```
```sql
fields toHumanString(@size, "bytes")
```
### Format types | Type | Output | Example input → output | | ---------------- | ------------------------- | ---------------------------- | | `"number"` | comma-separated digits | `1234567` → `"1,234,567"` | | `"short"` | rounded shorthand | `1234567` → `"1.23 million"` | | `"bytes"` | rounded binary size | `105906176` → `"101.0 MiB"` | | `"milliseconds"` | duration starting from ms | `1500` → `"1.5s"` | | `"microseconds"` | duration starting from µs | `1500` → `"1.5ms"` | ### Common usage
```sql
stats avg(duration::int) as avg_ms
| fields toHumanString(avg_ms, "milliseconds") as avg
```
```sql
stats sum(@size) as total
| fields toHumanString(total, "bytes") as total_size
```
Mostly useful for charting and table output. For computation, keep the raw number and only format at the end. * `trim` Signature`trim(string) -> string` * `uppercase` Signature`uppercase(string) -> string` ### Aggregate [Section titled “Aggregate”](#aggregate) Aggregate functions are only valid inside a `stats` call. `count()` with no argument counts all events. Passing a boolean expression counts only events where the expression is true. Passing a field name counts only non-null occurrences of that field:
```badgerql
stats count() -- all events
stats count(status_code::int >= 500) -- events with 5xx status
stats count(user_id::str) -- events where user_id is not null
```
`first` and `last` return the first or last value seen within the group. If the data is not sorted before `stats`, the result is non-deterministic. Pipe through `sort` first if order matters. `percentile` is an approximated result. * `apdex` Returns the Application Performance Index (Apdex) score, which measures user satisfaction with response time. Signature`apdex(number, number) -> float` Example
```sql
stats apdex(duration::int, 500) as apdex_score
```
Apdex scores a sample of response times against a target threshold `T`. Each request counts as: * **Satisfied** (1.0) if it completed in `T` or less * **Tolerating** (0.5) if it completed between `T` and `4T` * **Frustrated** (0) if it took longer than `4T` The score is the average — so 1.0 means every request was satisfied, 0 means every request was frustrated.
```sql
stats apdex(duration::int, 500) as score
```
### Picking a threshold `T` should be the response time at which a typical user starts to notice latency. Common starting points: * User-facing web requests: 200–500ms * API endpoints: 100–300ms * Background jobs: depends on the job — pick something tied to user expectations ### Reading the score Rough rule of thumb: | Score | Reading | | ----------- | ------------ | | ≥ 0.94 | Excellent | | 0.85 – 0.94 | Good | | 0.70 – 0.85 | Fair | | 0.50 – 0.70 | Poor | | < 0.50 | Unacceptable | These bands aren't a Honeybadger-specific standard — they come from the Apdex specification. * `apdexIf` Returns the Apdex score computed only over events where the predicate is true. The predicate restricts the whole calculation — satisfied and tolerating counts as well as the total — so the score reads as "the apdex of this slice of events." See `apdex` for how the score itself works. Signature`apdexIf(number, number, boolean) -> float` Example
```sql
stats apdexIf(duration::int, 500, route::str == "/checkout") as checkout_apdex
```
* `avg` Signature`avg(number) -> number` * `avgIf` Average a numeric value across events where the predicate is true. Signature`avgIf(number, boolean) -> number` Example
```sql
stats avgIf(duration::int, route::str == "/checkout") as checkout_avg
```
* `avgWeighted` Returns a weighted average. Values with larger weights count more, which is useful when averaging pre-aggregated rows such as per-route latency weighted by request count. Signature`avgWeighted(number, number) -> float` Example
```sql
stats avgWeighted(avg_latency::float, request_count::int) as typical_latency
```
* `collect` Collects the values from each group into an array. Compose with `dedupe` when you want distinct values. Signature`t = string | integer | float | boolean | datetime``collect(t) -> t[]` Example
```sql
stats dedupe(collect(user_id::str)) as users by error_class::str
```
* `corr` Returns the correlation coefficient between two numeric expressions: -1 is inverse correlation, 0 is no linear correlation, and 1 is direct correlation. Signature`corr(number, number) -> float` Example
```sql
stats corr(memory::float, response_time::float) as memory_vs_latency
```
* `count` Return the total counts of all results. The count can be affected by supplying a boolean expression argument. If given a field, it will implicitly count non-null occurrences. Signature`count() -> integer``count(boolean) -> integer``count(number) -> integer``count(string) -> integer` Example
```sql
stats count()
```
```sql
stats count(status_code::int < 500)
```
* `countIf` Count events where the predicate is true. Signature`countIf(boolean) -> integer` Example
```sql
stats countIf(status_code::int >= 500) as errors
```
* `first` Returns the first encountered value. Results could be random if the source is not sorted. Signature`t = string | number | boolean | datetime``first(t) -> t` Example
```sql
stats first(user_name::str) by error_class::str
```
* `firstIf` Returns the first encountered value among events where the predicate is true. Use `pickMin(value, @ts)` when you need deterministic earliest-by-time semantics. Signature`t = string | number | boolean | datetime``firstIf(t, boolean) -> t` Example
```sql
sort @ts asc | stats firstIf(message::str, level::str == "error") as first_error by host::str
```
* `last` Returns the last encountered value. Results could be random if the source is not sorted. Signature`t = string | number | boolean | datetime``last(t) -> t` Example
```sql
stats last(severity::str) by error_class::str
```
* `lastIf` Returns the last encountered value among events where the predicate is true. Use `pickMax(value, @ts)` when you need deterministic latest-by-time semantics. Signature`t = string | number | boolean | datetime``lastIf(t, boolean) -> t` Example
```sql
sort @ts asc | stats lastIf(message::str, level::str == "error") as last_error by host::str
```
* `max` Signature`t = string | number | datetime``max(t) -> t` * `maxIf` Return the maximum value across events where the predicate is true. Signature`t = string | number | datetime``maxIf(t, boolean) -> t` Example
```sql
stats maxIf(duration::int, status_code::int >= 500) as slowest_error
```
* `median` Returns the median value. Equivalent to `percentile(50, value)` and approximated the same way. Signature`median(number) -> number` * `min` Signature`t = string | number | datetime``min(t) -> t` * `minIf` Return the minimum value across events where the predicate is true. Signature`t = string | number | datetime``minIf(t, boolean) -> t` Example
```sql
stats minIf(duration::int, status_code::int >= 500) as fastest_error
```
* `percentile` Calculate the percentile. This is an approximated result. Signature`percentile(literal number, number) -> number` Example
```sql
stats percentile(90, duration::int)
```
* `percentileIf` Calculate a percentile across events where the predicate is true. This is an approximated result. Signature`percentileIf(literal number, number, boolean) -> number` Example
```sql
stats percentileIf(95, duration::int, status_code::int < 500) as p95_ok
```
* `pickMax` Returns the first argument from the row where the second argument is largest. `pickMax(error_message::str, @ts)` returns the most recent error message in each group. Signature`t = string | integer | float | boolean | datetime``pickMax(t, any) -> t` Example
```sql
stats pickMax(error_message::str, @ts) as latest_error by error_class::str
```
* `pickMin` Returns the first argument from the row where the second argument is smallest. `pickMin(user_id::str, duration::int)` returns the user from the fastest request in each group. Signature`t = string | integer | float | boolean | datetime``pickMin(t, any) -> t` Example
```sql
stats pickMin(user_id::str, duration::int) as fastest_user by controller::str
```
* `rate` Convert an aggregate into a rate by dividing it by the width of the query's `bin()` group. Defaults to a per-second rate; pass an interval to get a rate per minute, per hour, etc. Signature`rate(number) -> float``rate(number, interval) -> float` Example
```sql
stats rate(count()) as rps by bin(1m) as t
```
```sql
stats rate(sum(bytes::int)) as bps by bin() as t
```
```sql
stats rate(count(), 1m) as rpm by bin(1h) as t
```
### Following the bin The divisor is the width of the query's `bin()`. That includes auto-sized `bin()` — when the bin width changes with the query window, the divisor changes with it, and the result keeps the same unit:
```sql
stats rate(count()) as rps by bin() as t
```
### Choosing the interval The default is per second — the universal observability idiom (RPS, BPS, errors/sec). Pass an interval as the second argument for other units; the bin size doesn't have to match:
```sql
stats rate(count(), 1m) as rpm by bin(1h) as t
```
### Composing Rates are plain numbers, so they compose with arithmetic — two rates over the same bin make a unitless ratio:
```sql
stats (rate(countIf(status::int >= 500)) / rate(count())) as error_rate by bin(1m) as t
```
### Restrictions * Requires exactly one `bin()` group in the same stats stage (directly or via a renamed field). * The argument must be an aggregate. Rates are most natural over `count`/`sum`-style aggregates; `rate(min(x))` is computable but rarely what you want. * Month and year bins or intervals are rejected — they have no fixed second count, so use a fixed-period interval like `30d`. * `stddev` Returns the sample standard deviation of the numeric values. Pair with `avg` to see how spread out a metric is. Signature`stddev(number) -> float` * `sum` Signature`sum(number) -> number` * `sumIf` Sum a numeric value across events where the predicate is true. Signature`sumIf(number, boolean) -> number` Example
```sql
stats sumIf(amount::float, status::str == "paid") as paid_total
```
* `unique` Count all unique values Signature`t = string | number | datetime``unique(t) -> integer` Example
```sql
stats unique(concat(controller::str, action::str))
```
* `uniqueIf` Count distinct values among events where the predicate is true. Signature`t = string | number | datetime``uniqueIf(t, boolean) -> integer` Example
```sql
stats uniqueIf(user_id::str, event_type::str == "purchase") as purchasers
```
* `variance` Returns the sample variance of the numeric values. Variance is the square of standard deviation. Signature`variance(number) -> float` ### Grouping [Section titled “Grouping”](#grouping-1) * `top` Select the top N values of a field. By default, values are ranked by frequency. An optional third argument ranks values by an aggregate instead, such as `max`, `sum`, or `avg`. `top()` is context-aware: it caps groups, filters by membership, or returns an array depending on where it appears. Useful for high-cardinality fields like controllers, endpoints, queues, or workers. Signature`t = string | number``top(literal integer, t, any = null) -> t[] | t` Example
```sql
stats count() by top(10, controller::str)
```
```sql
filter controller::str in top(5, controller::str)
```
```sql
stats top(10, controller::str) by env::str
```
### In a stats group Caps the group to the top N values, dropping the rest. By default, "top" means most frequent. The default ranking is approximate.
```sql
stats count() by top(10, controller::str)
```
Pass an `order_by` aggregate as the third argument to rank by something other than frequency. This switches to an exact ranking — slower than the default, but deterministic.
```sql
stats count() by top(10, controller::str, max(duration::float))
```
Combine with `bin()` to chart the top N series over time:
```sql
stats avg(duration::float) by top(10, controller::str), bin()
```
Group-position `top()` may be wrapped in another expression (e.g. `lower(top(5, controller::str))`). The ranking matches the wrapped value so the result lines up with the group key. ### In a filter Tests membership against the top N values. Use `in` to keep matching events or `not in` to exclude them.
```sql
filter controller::str in top(5, controller::str)
```
The check runs against raw events when used in a pre-stats filter, and against grouped results when used in a post-stats filter. In practice, `filter controller::str in top(5, controller::str) | stats count() by bin()` selects the top 5 controllers from the source events first, then charts only those events over time. ### As a stats aggregate Returns the top N values as an array.
```sql
stats top(10, controller::str) by env::str
```
Aggregate-position `top()` does not accept an `order_by` argument. Given an expanded array field, it ranks the array's elements and still returns a flat array. This counts every element across all events, so it does not multiply rows the way `expand` does.
```sql
stats top(3, tags[*]::str) as top_tags by fault_id::int
```
### Restrictions * `n` must be a positive integer literal — not a field reference. * `top()` is not allowed inside an `or` condition. * In a filter, `top()` must be the right-hand side of `in` or `not in`. Other filter shapes (e.g. equality) are rejected.
# Ship your CloudWatch Logs to Honeybadger Insights
> Here's how to ship your logs from CloudWatch Logs to Honeybadger Insights.
Ingesting logs from CloudWatch Logs requires setting up a [Data Firehose](https://aws.amazon.com/firehose/) stream with a [HTTP Endpoint destination](https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html#create-destination-http) that sends events to our API. Once you create [subscription filters](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#FirehoseExample) for the desired log groups, log data from those groups will start flowing into Insights. The easiest way to set this up is to use our [CloudFormation template](https://honeybadger-docs-assets.s3.amazonaws.com/insights-cloudformation-stack.yml) to create a CloudFormation stack in your AWS account. It will prompt you to enter your Honeybadger API key and the name of a log group that you want to connect to Data Firehose. You can quickly [launch this template in your AWS account](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=honeybadger-insights\&templateURL=https://honeybadger-docs-assets.s3.amazonaws.com/insights-cloudformation-stack.yml) and then create additional log group subscriptions for other log groups you wish to monitor. If you set up the Data Firehose stream manually, choose HTTP Endpoint as the destination and use the following URL as the HTTP Endpoint URL in the destination settings:
```plaintext
https://api.honeybadger.io/v1/data-firehose-events?api_key=PROJECT_API_KEY
```
## Setting default fields with a query parameter [Section titled “Setting default fields with a query parameter”](#setting-default-fields-with-a-query-parameter) Events from this endpoint are built from the CloudWatch Logs record, so they always have the same shape: a `ts`, a `message`, and the `logGroup` and `logStream` the record came from. If you want more than that — the environment, the region, the name of the app — you can add a `defaults` query parameter to the HTTP Endpoint URL containing a URL-encoded JSON object, and its fields will be merged into every event the stream delivers:
```plaintext
https://api.honeybadger.io/v1/data-firehose-events?api_key=PROJECT_API_KEY&defaults={"environment":"production","region":"us-east-1"}
```
With that URL, an event that would otherwise be stored as:
```json
{"ts": "2023-08-31T09:19:30.000Z", "logGroup": "/aws/lambda/checkout", "logStream": "2023/08/31/[$LATEST]abc123", "message": "This is a log line"}
```
…is stored as:
```json
{"ts": "2023-08-31T09:19:30.000Z", "logGroup": "/aws/lambda/checkout", "logStream": "2023/08/31/[$LATEST]abc123", "message": "This is a log line", "environment": "production", "region": "us-east-1"}
```
Because the parameter lives on the destination URL, each Data Firehose stream can carry its own metadata — a useful way to tag events by environment or account when you’re shipping logs from more than one place, without running a transformation Lambda to rewrite the payloads. The `defaults` parameter has a few restrictions: * It must be a flat JSON object; values must be strings, numbers, or booleans. * The keys `event_type` and `ts` are reserved and will be ignored. * It’s limited to 16 keys and 2kB (URL-decoded). * Fields we build from the log record always win — a default named `message` or `logGroup` won’t overwrite the real one. An invalid `defaults` parameter never causes the delivery to fail: entries that break the rules above are dropped (an unparseable or oversized parameter is ignored entirely), and the events are ingested without them. Likewise, if merging the defaults would push an event past the 100kB per-event size limit, we drop the defaults for that event rather than the event itself.
# Ship your Crunchy Bridge logs to Honeybadger Insights
> Ship Postgres logs from Crunchy Bridge to Honeybadger Insights.
You can have Crunchy Bridge ship the logs from your Postgres clusters by following their [setup instructions](https://docs.crunchybridge.com/how-to/logging). Use the following values for the logging destination: | Field | Value | | -------- | --------------------------------------------------------------------------------------------------------- | | Host | in.honeybadger.io | | Port | 6514 | | Template | `<$PRI>1 $ISODATE $HOST $PROGRAM $PID ${MSGID:--} [honeybadger@61642 api_key=\"PROJECT_API_KEY\"] $MSG\n` | You can choose to add additional key/value data to the structured data section of the template. E.g., if you want to add an environment field to the payload, you can specify it after the API key in the template:
```plaintext
<$PRI>1 $ISODATE $HOST $PROGRAM $PID ${MSGID:--} [honeybadger@61642 api_key=\"PROJECT_API_KEY\" environment=\"production\"] $MSG\n
```
# Ship your Docker container logs to Honeybadger Insights
> Here's how to use Vector to collect Docker container logs and send them to Honeybadger Insights.
You can use [Vector](https://vector.dev) with its `docker_logs` source to collect logs from your Docker containers and send them to Honeybadger Insights. This example collects logs from all running containers:
```yaml
# Put this in vector.yaml
sources:
docker:
type: "docker_logs"
transforms:
enrich_docker:
type: "remap"
inputs: ["docker"]
source: |
# Try to parse JSON log messages
payload, err = parse_json(string!(.message))
if err == null {
.payload = payload
del(.message)
}
sinks:
honeybadger_events:
type: "http"
inputs: ["enrich_docker"]
uri: "https://api.honeybadger.io/v1/events"
request:
headers:
X-API-Key: "PROJECT_API_KEY"
encoding:
codec: "json"
framing:
method: "newline_delimited"
```
To run Vector with Docker and collect logs from other containers, you need to mount the Docker socket. Here’s a Docker Compose configuration:
```yaml
services:
vector:
image: timberio/vector:latest-alpine
volumes:
- "./vector.yaml:/etc/vector/vector.yaml:ro"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
# Example app container whose logs will be collected
app:
image: your-app:latest
labels:
vector.enable: "true"
```
You can filter which containers Vector collects logs from using labels. Update the source configuration to only collect logs from containers with a specific label:
```yaml
sources:
docker:
type: "docker_logs"
include_labels:
- "vector.enable=true"
```
# Send logs and events from Elixir apps to Honeybadger Insights
> Here's how to integrate your Elixir apps with Honeybadger Insights.
When enabled, Honeybadger [automatically instruments your Elixir/Phoenix application](/lib/elixir/insights/automatic-instrumentation/) to send application events to Honeybadger Insights. This is the easiest way to get started with Insights and logging. To get started, enable Insights in your app configuration:
```elixir
config :honeybadger,
insights_enabled: true
```
See our [automatic instrumentation](/lib/elixir/insights/automatic-instrumentation/) guide to learn more. You can also [add extra context data](/lib/elixir/insights/event-context/) to events, [filter events](/lib/elixir/insights/filtering-events/) to remove PII, and [sample events](/lib/elixir/insights/sampling-events/) to reduce the amount of data sent to Honeybadger. ## Sending custom events [Section titled “Sending custom events”](#sending-custom-events) You can send custom events to Honeybadger Insights with the `Honeybadger.event/1` and `Honeybadger.event/2` functions. For example:
```elixir
Honeybadger.event(%{
event_type: "user_created",
user: user.id
})
Honeybadger.event("project_deleted", %{
project: project.name
})
```
## Sending logs from your infrastructure [Section titled “Sending logs from your infrastructure”](#sending-logs-from-your-infrastructure) Honeybadger isn’t just for errors and application data! You can use our [syslog](/guides/insights/integrations/systemd/), [Vector](/guides/insights/integrations/log-files/), or [PaaS integrations](/guides/insights/#adding-data-from-other-sources) to send additional data from your infrastructure to [Honeybadger Insights](/guides/insights/), where you can query, visualize, and analyze all of your production data in one place.
# Ship your Fly.io logs to Honeybadger Insights
> Here's how to ship your logs from Fly.io to Honeybadger Insights.
Use [Fly.io’s log shipper app](https://github.com/superfly/fly-log-shipper) to ship logs from your apps hosted by Fly.io. First, create a new app config:
```shell
# Make a directory for your log shipper app
mkdir logshipper
cd logshipper
# Create the app but don't deploy just yet
fly launch --no-deploy --image ghcr.io/superfly/fly-log-shipper:latest
# Set some secrets. Setting HONEYBADGER_API_KEY enables the shipping of logs to your Honeybadger project.
fly secrets set ORG=personal # The org you chose when running "fly launch"
fly secrets set ACCESS_TOKEN=$(fly auth token)
fly secrets set HONEYBADGER_API_KEY=PROJECT_API_KEY
```
Edit the generated `fly.toml` file, replacing the `[http_service]` section with this:
```toml
[[services]]
http_checks = []
internal_port = 8686
```
Then deploy the app:
```shell
fly deploy
```
Once that’s done, you should see logs from your apps flowing into Insights. See the [Fly.io docs](https://fly.io/docs/going-to-production/monitoring/exporting-logs/) for more information about using the log shipper app.
# Ship your Heroku logs to Honeybadger Insights
> Here's how to ship your logs from Heroku to Honeybadger Insights.
To get your Heroku logs into Insights, create a new log drain for your Heroku app using an API key displayed on the API keys tab of the project settings page:
```bash
heroku drains:add "https://logplex.honeybadger.io/v1/events?api_key=PROJECT_API_KEY"
```
You can optionally add the `env` parameter to the log drain endpoint. If you do so, each payload recorded from Logplex will have a field named `environment` added to it. You can then add a filter for the desired environment to your queries, like `filter environment::str == 'production'`.
```bash
heroku drains:add https://logplex.honeybadger.io/v1/events?api_key=PROJECT_API_KEY&env=production
```
# Host metrics
> Monitor CPU, memory, and disk usage on your servers with Honeybadger Insights.
Track your infrastructure’s health by sending host metrics to [Honeybadger Insights](/guides/insights/). Monitor CPU usage, memory consumption, and disk space alongside your application errors and logs. ## Using the Honeybadger CLI [Section titled “Using the Honeybadger CLI”](#using-the-honeybadger-cli) The easiest way to collect host metrics is with the [Honeybadger CLI](/resources/cli/). Download a prebuilt binary from the [GitHub releases page](https://github.com/honeybadger-io/cli/releases), or install with Go:
```shell
go install github.com/honeybadger-io/cli@latest
```
See the [CLI installation guide](/resources/cli/#installation) for other options, including Homebrew. Start the metrics agent with your project API key:
```shell
hb agent --api-key PROJECT_API_KEY
```
The agent collects CPU, memory, and disk metrics every 60 seconds and sends them to Insights. You can customize the interval with the `-i, --interval` flag (see the [CLI reference](/resources/cli/#agent) for details). ### Tagging metrics [Section titled “Tagging metrics”](#tagging-metrics) If you’re running the agent on multiple hosts, add tags to identify and group them:
```shell
hb agent --api-key PROJECT_API_KEY \
--tag environment=production \
--tag role=web-1
```
Tags appear as top-level fields on every metric event. You can also override the default hostname with `--tag host=custom-name`, which is useful when hostnames are auto-generated (e.g. IP-based names from cloud providers). Tags can also be set in the configuration file (`~/.honeybadger-cli.yaml`):
```yaml
api_key: PROJECT_API_KEY
agent:
tags:
environment: production
role: web-1
```
CLI flags take precedence over configuration file tags. See the [CLI reference](/resources/cli/#agent) for details and examples of reserved field names that cannot be used as tag keys. Once tagged, you can filter and group metrics in Insights:
```badgerql
fields @ts, host::str, used_percent::float
| filter event_type::str == "report.system.cpu"
| filter environment::str == "production"
| filter role::str == "web-1"
```
## Querying agent metrics in Insights [Section titled “Querying agent metrics in Insights”](#querying-agent-metrics-in-insights) Once metrics are flowing, you can query them in Insights. Each metric type sends a separate event:
```json
{"@id": "ca4dee56-bede-453d-a41e-a6fd93d30eaf", "@stream.id": "3XepYQVyo5to", "@ts": "2026-01-12 22:22:11.000", "total_bytes": 994662584320, "used_bytes": 544694333440, "free_bytes": 449968250880, "used_percent": 54.76, "device": "/dev/disk3s1s1", "event_type": "report.system.disk", "host": "vonnegut.lan", "mountpoint": "/", "fstype": "apfs"}
{"@id": "d76ca037-3bab-4c1c-beb1-a18b9e6ff765", "@stream.id": "3XepYQVyo5to", "@ts": "2026-01-12 22:22:11.000", "total_bytes": 51539607552, "used_bytes": 38632865792, "free_bytes": 164954112, "available_bytes": 12906741760, "used_percent": 74.96, "event_type": "report.system.memory", "host": "vonnegut.lan"}
{"@id": "5b7c4060-1ff3-4d52-90d8-a9d3af17174a", "@stream.id": "3XepYQVyo5to", "@ts": "2026-01-12 22:22:11.000", "num_cpus": 14, "used_percent": 32.85, "load_avg_1": 3.35009765625, "load_avg_5": 3.73046875, "load_avg_15": 3.86083984375, "event_type": "report.system.cpu", "host": "vonnegut.lan"}
```
Here’s an example [BadgerQL](/guides/insights/badgerql/) query to get a snapshot of disk usage:
```badgerql
fields @ts, mountpoint::str, used_percent::float
| filter event_type::str == "report.system.disk"
| sort used_percent desc
| limit 1 by mountpoint::str
```
| @ts `TIME EDT` | mountpoint `STR` | used\_percent `FLOAT` | | ----------------------- | ---------------- | --------------------- | | 2026-01-12 16:15:06.000 | / | 55.01 | | 2026-01-12 16:14:21.000 | /data | 11.91 | ## Using Vector [Section titled “Using Vector”](#using-vector) If you need more flexibility or are already using [Vector](https://vector.dev) in your infrastructure, you can use it to send host metrics to Insights instead. Here’s a sample configuration:
```yaml
# Put this in /etc/vector/vector.yaml
sources:
host:
type: "host_metrics"
sinks:
honeybadger_events:
type: "http"
inputs: ["host"]
uri: "https://api.honeybadger.io/v1/events"
request:
headers:
X-API-Key: "PROJECT_API_KEY"
encoding:
codec: "json"
framing:
method: "newline_delimited"
```
The easiest way to run Vector is via Docker. Here’s a sample [Docker Compose](https://docs.docker.com/compose/) configuration, assuming your Vector configuration is in a file named `vector.yaml`:
```yaml
version: "3.2"
services:
vector:
image: timberio/vector:latest-alpine
volumes:
- "vector.yaml:/etc/vector/vector.yaml:ro"
```
See the [Vector documentation](https://vector.dev/docs/reference/configuration/sources/host_metrics/) for more configuration options. ## Querying Vector’s metrics [Section titled “Querying Vector’s metrics”](#querying-vectors-metrics) Vector’s [metrics](https://vector.dev/docs/reference/configuration/sources/host_metrics/#output-metrics) are structured like this:
```json
{
"@id": "01922983-149f-7a69-b5e1-ddca928d815e",
"@stream.id": "cEhUcrZrnny0",
"@ts": "2025-09-25 14:08:26.048",
"gauge": {
"value": 1.25
},
"tags": {
"collector": "load",
"host": "api-10-0-11-252"
},
"kind": "absolute",
"name": "load15",
"namespace": "host"
}
```
Here’s an example [BadgerQL](/guides/insights/badgerql/) query to get a snapshot of disk usage:
```badgerql
fields @ts, tags.mountpoint::str, round(gauge.value::float * 100, 2) as used_percentage
| filter namespace::str == "host"
| filter name::str == "filesystem_used_ratio"
| filter gauge.value::float > 0.0
| filter tags.filesystem::str not in ["tmpfs", "devtmpfs", "squashfs"]
| sort @ts
| limit 1 by tags.mountpoint
```
| @ts `TIME EDT` | tags.mountpoint `STR` | used\_percentage `FLOAT` | | ----------------------- | --------------------- | ------------------------ | | 2025-09-25 10:45:11.047 | / | 29.77 | | 2025-09-25 10:45:11.047 | /efs | 0 |
# Send logs and events from JavaScript apps to Honeybadger Insights
> Here's how to integrate your JavaScript apps with Honeybadger Insights.
#### Automatic instrumentation [Section titled “Automatic instrumentation”](#automatic-instrumentation) Capture inbound HTTP requests from Express, Fastify, AWS Lambda, and Next.js as `request.handled` events. See [Automatic instrumentation](/lib/javascript/insights/automatic-instrumentation/) for configuration and framework setup. #### Logs [Section titled “Logs”](#logs) Instrument your JavaScript application, either backend or frontend, to send your logs automatically to Honeybadger Insights. More information can be found [here](/lib/javascript/insights/capturing-logs/). #### Events [Section titled “Events”](#events) If you have custom events you’d like to track, use `Honeybadger.event()` to report them to Insights:
```javascript
Honeybadger.event("button_click", {
action: "buy_now",
user_id: 123,
product_id: 456,
});
```
More information about sending events to Insights from your JavaScript apps can be found [here](/lib/javascript/insights/sending-events-to-insights/).
# Use Vector to ship your log files to Honeybadger Insights
> Here's how to use Vector to watch your log files and send the events they record to Honeybadger.
You can use [Vector](https://vector.dev) to watch your existing log files and send the events they record. Here’s a sample configuration that will encode the log lines into the newline-delimited JSON format that our API expects:
```yaml
# Put this in /etc/vector/vector.yaml
sources:
app:
type: "file"
include: ["/home/app/shared/log/*.log"]
sinks:
honeybadger_events:
type: "http"
inputs: ["app"]
uri: "https://api.honeybadger.io/v1/events"
request:
headers:
X-API-Key: "PROJECT_API_KEY"
encoding:
codec: "json"
framing:
method: "newline_delimited"
```
If you are using something like [Lograge](https://github.com/roidrage/lograge) to emit JSON-formatted logs (and you should — it’s awesome), you can have Vector replace the message field with a JSON payload:
```yaml
# Put this in /etc/vector/vector.yaml
sources:
app:
type: "file"
include: ["/home/app/shared/log/*.log"]
transforms:
parse_logs:
type: "remap"
inputs: ["app"]
source: |
payload, err = parse_json(string!(.message))
if err == null {
.payload = payload
del(.message)
}
sinks:
honeybadger_events:
type: "http"
inputs: ["parse_logs"]
uri: "https://api.honeybadger.io/v1/events"
request:
headers:
X-API-Key: "PROJECT_API_KEY"
encoding:
codec: "json"
framing:
method: "newline_delimited"
```
Or if you are using logfmt-style logs, like “controller=pages action=index”, then you can add a transform that parses that into JSON:
```yaml
---
transforms:
parse_logs:
type: "remap"
inputs: ["app"]
source: |
payload, err = parse_key_value(string!(.message))
if err == null {
.payload = payload
del(.message)
}
```
Again, we **highly** recommend structured logging. 😉 By the way, Vector supports a variety of [input sources](https://vector.dev/docs/reference/configuration/sources/), such as Docker logs, Redis metrics, etc., in addition to log files. You can define whatever `sources` and `transforms` make sense for what you want to capture, then use the `sinks` section provided in the examples above to send everything to Insights. The easiest way to run Vector is via Docker. Here’s a sample [Docker Compose](https://docs.docker.com/compose/) configuration you can use, assuming your Vector configuration is in a file named `vector.yaml`:
```yaml
version: "3.2"
services:
vector:
image: timberio/vector:latest-alpine
volumes:
- "vector.yaml:/etc/vector/vector.yaml:ro"
```
## Additional Vector configuration examples [Section titled “Additional Vector configuration examples”](#additional-vector-configuration-examples) ### Nginx logs [Section titled “Nginx logs”](#nginx-logs) You can use regular expressions to extract the fields of an Nginx log to create a JSON structure in a transform:
```yaml
sources:
nginx_logs:
type: "file"
ignore_older: 86400
include:
- "/var/log/nginx/access.log"
read_from: "end"
transforms:
parse_nginx:
type: "remap"
inputs:
- "nginx_logs"
source: |
match, err = parse_regex(.message, r'(?P[^ ]*) - (?P[^ ]*) \[(?P[^\]]*)\] "(?P[^ ]*) ?(?P[^ ]*) ?(?P[^"]*)" (?P[^ ]*) (?P[^ ]*) "(?P[^"]*)" "(?P[^"]*)" (?[0-9\.]+)', true)
if err == null {
.remote_addr = match.remote_addr
.user = match.user
.timestamp = parse_timestamp(match.timestamp, "%d/%b/%Y:%H:%M:%S %z") ?? match.timestamp
.request = match.request
.method = match.method
.url = match.url
.protocol = match.protocol
.status, err = to_int(match.status)
.bytes_sent, err = to_int(match.bytes_sent)
.referer = match.referer
.user_agent = match.user_agent
.duration, err = to_float(match.duration)
del(.message)
} else {
log("Failed to parse log line: " + err, level: "error")
}
```
# Ship your Netlify logs to Honeybadger Insights
> Here's how to ship your logs from Netlify to Honeybadger Insights.
You can use Netlify’s [General HTTP endpoint](https://docs.netlify.com/monitor-sites/log-drains/?monitoring-providers=general#general-http-endpoint) to send your Netlify logs to Insights. Choose NDJSON as the Log Drain Format and enter this URL as the Full URL:
```plaintext
https://api.honeybadger.io/v1/events?api_key=PROJECT_API_KEY
```
# OpenTelemetry Protocol (OTLP)
> Send traces, metrics, and logs to Honeybadger Insights using the OpenTelemetry Protocol.
Honeybadger can ingest OpenTelemetry traces, metrics, and logs directly via the [OpenTelemetry Protocol (OTLP)](https://opentelemetry.io/docs/specs/otlp/). If you’re already using OpenTelemetry to instrument your applications, you can send that data to Honeybadger Insights without changing your instrumentation code—just point your OTLP exporter at our endpoint. ## Getting started [Section titled “Getting started”](#getting-started) The quickest way to send OpenTelemetry data to Honeybadger is by pointing your OTLP exporter at our endpoint:
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeybadger.io
export OTEL_EXPORTER_OTLP_HEADERS=X-API-Key=PROJECT_API_KEY
```
We accept the `http/protobuf` protocol, which is the default for most SDKs. See [Authentication](#authentication) below for other ways to pass your API key. ## Authentication [Section titled “Authentication”](#authentication) Honeybadger accepts your project API key via either the `X-API-Key` header or a standard `Authorization: Bearer` header. Use whichever fits your exporter or collector configuration:
```bash
export OTEL_EXPORTER_OTLP_HEADERS=X-API-Key=PROJECT_API_KEY
```
```bash
export OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20PROJECT_API_KEY
```
Note that `OTEL_EXPORTER_OTLP_HEADERS` requires the space between `Bearer` and your key to be URL-encoded as `%20`. Your API key is available on the API keys tab of your project settings page. ## Using the OpenTelemetry Collector [Section titled “Using the OpenTelemetry Collector”](#using-the-opentelemetry-collector) If you’re using the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), add an `otlphttp` exporter to your configuration:
```yaml
exporters:
otlphttp/honeybadger:
endpoint: https://api.honeybadger.io
headers:
X-API-Key: PROJECT_API_KEY
```
You can also authenticate using a bearer token:
```yaml
exporters:
otlphttp/honeybadger:
endpoint: https://api.honeybadger.io
headers:
Authorization: Bearer PROJECT_API_KEY
```
Then add `otlphttp/honeybadger` to your pipeline exporters. ## Supported signals [Section titled “Supported signals”](#supported-signals) We accept traces, metrics, and logs at the following endpoints: | Signal | Endpoint | | ------- | --------------------------------------- | | Traces | `https://api.honeybadger.io/v1/traces` | | Metrics | `https://api.honeybadger.io/v1/metrics` | | Logs | `https://api.honeybadger.io/v1/logs` | ## Querying your data [Section titled “Querying your data”](#querying-your-data) Once your data is flowing, you can query it in [Insights](/guides/insights/) using [BadgerQL](/guides/insights/badgerql/). OpenTelemetry data appears as events with these types: * **Traces**: `event_type` = `otel.span` * **Metrics**: `event_type` = `otel.metric` * **Logs**: `event_type` = `otel.log` For example, to view recent spans:
```badgerql
fields @ts, span_name::str, duration::float, status.code::str, resource.service.name::str
| filter event_type::str == "otel.span"
| sort @ts
```
# Send logs and events from PHP apps to Honeybadger Insights
> Here's how to integrate your PHP apps with Honeybadger Insights.
#### Logs [Section titled “Logs”](#logs) Instrument your PHP application, either a Lumen, a Laravel or a plain PHP app, to send your logs automatically to Honeybadger Insights. More information can be found [here](/lib/php/insights/capturing-logs/). #### Events [Section titled “Events”](#events) If you are using Laravel or Lumen, enable the automatic events instrumentation :
```php
'events' => [
'enabled' => true,
'automatic' => HoneybadgerLaravel::DEFAULT_EVENTS,
],
```
If you have custom events you’d like to track, use `Honeybadger.event()` to report them to Insights:
```php
Honeybadger.event('button_click', [
'action' => 'buy_now',
'user_id' => 123,
'product_id' => 456
])
```
More information about sending events to Insights from your PHP apps can be found [here](/lib/php/insights/sending-events-to-insights/).
# Send logs and events from Python apps to Honeybadger Insights
> Here's how to integrate your Python apps with Honeybadger Insights.
When enabled, Honeybadger [automatically instruments your Python application](/lib/python/insights/automatic-instrumentation/) to send application events to Honeybadger Insights. This is the easiest way to get started with Insights. To get started, enable Insights in your app configuration:
```python
from honeybadger import honeybadger
honeybadger.configure(insights_enabled=True)
```
Once integrated with our middleware or extensions, Honeybadger instruments the following libraries: * **Django** requests & database queries * **Flask** requests & database queries * **ASGI** requests (FastAPI, Starlette, etc.) * **Celery** tasks * **Oban** workers & maintenance loops See the [automatic instrumentation guide](/lib/python/insights/automatic-instrumentation/) to learn how to configure each integration, and the [Python event reference](/insights/event-types/python/) for every event the package emits, with field schemas and types. You can also [add extra context data](/lib/python/insights/event-context/) to events, [filter events](/lib/python/insights/filtering-events/) to remove PII, and [sample events](/lib/python/insights/sampling-events/) to reduce the amount of data sent to Honeybadger. ## Querying events with BadgerQL [Section titled “Querying events with BadgerQL”](#querying-events-with-badgerql) Once events are flowing into Insights, you can query them with [BadgerQL](/guides/insights/badgerql/). For example, to find your slowest Django views:
```plaintext
filter event_type::str == "django.request"
| stats avg(duration::float) as avg_duration, count() as requests by view::str
| sort avg_duration desc
```
Or to see Oban background-job throughput and p95 duration by worker:
```plaintext
filter event_type::str == "oban.job_finished"
| stats count() as jobs, percentile(95, duration::float) as p95_ms by worker::str
| sort jobs desc
```
The [Python event reference](/insights/event-types/python/) lists the fields available on each event type. ## Sending custom events [Section titled “Sending custom events”](#sending-custom-events) If you have custom events you’d like to track, use `honeybadger.event` to report them to Insights:
```python
from honeybadger import honeybadger
honeybadger.event("user.signup", {"user_id": user.id, "plan": user.plan})
```
More information about sending events to Insights from your Python apps can be found [here](/lib/python/insights/sending-custom-events/). ## Sending logs from your infrastructure [Section titled “Sending logs from your infrastructure”](#sending-logs-from-your-infrastructure) Honeybadger isn’t just for errors and application data! You can use our [syslog](/guides/insights/integrations/systemd/), [Vector](/guides/insights/integrations/log-files/), or [PaaS integrations](/guides/insights/#adding-data-from-other-sources) to send additional data from your infrastructure to [Honeybadger Insights](/guides/insights/), where you can query, visualize, and analyze all of your production data in one place.
# Ship your rsyslog logs to Honeybadger Insights
> Use rsyslog to forward system and application logs to Honeybadger Insights over syslog-TLS.
[rsyslog](https://www.rsyslog.com/) is the default syslog daemon on most Linux distributions. You can configure it to forward logs to Honeybadger Insights over syslog-TLS (RFC 5425), tagging each message with your project’s API key in the structured-data section of the RFC 5424 payload. ## Requirements [Section titled “Requirements”](#requirements) Install the TLS driver package for rsyslog. On Debian and Ubuntu:
```shell
sudo apt-get install rsyslog-gnutls
```
On RHEL, Fedora, and derivatives:
```shell
sudo dnf install rsyslog-gnutls
```
You’ll also need the CA certificate bundle for your system. On Debian/Ubuntu this is `/etc/ssl/certs/ca-certificates.crt`. On RHEL/Fedora it’s `/etc/pki/tls/certs/ca-bundle.crt`. ## Configuration [Section titled “Configuration”](#configuration) /etc/rsyslog.d/60-honeybadger.conf
```plaintext
# Load the TLS network stream driver. Set the CA file to match your OS:
# Debian/Ubuntu: /etc/ssl/certs/ca-certificates.crt
# RHEL/Fedora: /etc/pki/tls/certs/ca-bundle.crt
global(DefaultNetstreamDriver="gtls"
DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca-certificates.crt")
# RFC 5424 template with Honeybadger structured data
template(name="HoneybadgerFormat" type="string"
string="<%PRI%>1 %TIMESTAMP:::date-rfc3339% %HOSTNAME% %APP-NAME% %PROCID% %MSGID% [honeybadger@61642 api_key=\"PROJECT_API_KEY\" event_type=\"rsyslog\"] %msg%\n")
# Forward all logs to Honeybadger over syslog-TLS (RFC 5425)
action(type="omfwd"
Target="in.honeybadger.io"
Port="6514"
Protocol="tcp"
TCP_Framing="octet-counted"
StreamDriver="gtls"
StreamDriverMode="1"
StreamDriverAuthMode="x509/name"
StreamDriverPermittedPeers="*.honeybadger.io"
template="HoneybadgerFormat")
```
Restart rsyslog to pick up the change:
```shell
sudo systemctl restart rsyslog
```
You can add additional key/value pairs to the structured-data section of the template. For example, to tag every event with an environment, replace the `string=` value inside the `template(name="HoneybadgerFormat" ...)` block above with the following:
```plaintext
string="<%PRI%>1 %TIMESTAMP:::date-rfc3339% %HOSTNAME% %APP-NAME% %PROCID% %MSGID% [honeybadger@61642 api_key=\"PROJECT_API_KEY\" event_type=\"rsyslog\" environment=\"production\"] %msg%\n"
```
## Shipping application log files with imfile [Section titled “Shipping application log files with imfile”](#shipping-application-log-files-with-imfile) rsyslog’s [`imfile`](https://www.rsyslog.com/doc/configuration/modules/imfile.html) module can tail arbitrary log files and feed them through the same pipeline, which is handy if your application writes to its own log file instead of stdout. Add the following to the top of `/etc/rsyslog.d/60-honeybadger.conf` (before the `action(...)` block):
```plaintext
# Load the file input module
module(load="imfile" PollingInterval="10")
# Tail your application's log files
input(type="imfile"
File="/var/log/myapp/*.log"
Tag="myapp"
Severity="info"
Facility="local7")
```
Each line written to a matching file will be forwarded to Honeybadger using the `HoneybadgerFormat` template, with `APP-NAME` set to the `Tag` value (`myapp`). Adjust `File`, `Tag`, `Severity`, and `Facility` to match your application. If you’d rather only forward the events captured by `imfile` (and not every other message rsyslog processes), wrap the action in a conditional:
```plaintext
if ($programname == "myapp") then {
action(type="omfwd"
Target="in.honeybadger.io"
Port="6514"
Protocol="tcp"
TCP_Framing="octet-counted"
StreamDriver="gtls"
StreamDriverMode="1"
StreamDriverAuthMode="x509/name"
StreamDriverPermittedPeers="*.honeybadger.io"
template="HoneybadgerFormat")
}
```
## Querying your data [Section titled “Querying your data”](#querying-your-data) Once your data is flowing, you can query it in [Insights](/guides/insights/) using [BadgerQL](/guides/insights/badgerql/). The following query will return events sent via rsyslog:
```badgerql
fields @ts, hostname::str, appname::str, severity::str, message::str
| filter event_type::str == "rsyslog"
| sort @ts
```
## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) If events aren’t showing up in Insights, check rsyslog’s own log for TLS or forwarding errors:
```shell
sudo journalctl -u rsyslog -f
```
A missing or incorrect CA file is the most common cause of connection failures — double-check the `DefaultNetstreamDriverCAFile` path against what’s installed on your system.
# Send metrics and events from Ruby and Rails apps to Honeybadger Insights
> Here's how to integrate your Ruby/Ruby on Rails apps with Honeybadger Insights.
#### Logs [Section titled “Logs”](#logs) Sending your logs to Insights is a quick way to get some visibility into your app. There are two good options: ##### Semantic Logger [Section titled “Semantic Logger”](#semantic-logger) Use the [rails\_semantic\_logger gem](https://github.com/reidmorrison/rails_semantic_logger) and enable the `HoneybadgerInsights` appender by adding `config.semantic_logger.add_appender(appender: :honeybadger_insights)` to `config/application.rb`. Outside of Rails, you can use the same appender with the [semantic\_logger gem](https://github.com/reidmorrison/semantic_logger). Please note that if you are using SolidQueue, you will need to add the following snippet to `config/initializers/solid_queue.rb` to work around a [known issue with Semantic Logger](https://github.com/reidmorrison/rails_semantic_logger/issues/237) that causes SolidQueue/ActiveJob logging to not be sent to Insights:
```yaml
# Re-open appenders after forking the worker, dispatcher, and scheduler processes
SolidQueue.on_worker_start { SemanticLogger.reopen }
SolidQueue.on_dispatcher_start { SemanticLogger.reopen }
SolidQueue.on_scheduler_start { SemanticLogger.reopen }
```
##### Lograge [Section titled “Lograge”](#lograge) Use [Lograge](https://github.com/roidrage/lograge) to emit JSON-formatted output to your log files and Vector to [forward them to Insights](/guides/insights/integrations/log-files/). If you go this route, be sure to disable the log tagging in your Rails environment config (`config/environments/production.rb`) by commenting out the `config.log_tags` line, as that will mess with the JSON output. #### Metrics [Section titled “Metrics”](#metrics) You can get more details about what’s happening in your application by enabling our gem’s automatic instrumentation, which will report information about every SQL query, HTTP request, etc. to Insights. This will consume more Insights quota than the logging approach, but you will get much more data to use for analyzing your app’s performance, and this will populate our ready-made Rails dashboard, which includes charts for request duration, SQL query counts, and more. More information can be found [here](/lib/ruby/insights/collecting-and-reporting-metrics). Alternatively, you can use [Yabeda](https://github.com/yabeda-rb/yabeda) and [our Yabeda integration](https://github.com/honeybadger-io/yabeda-honeybadger_insights) to collect and report metrics in your Ruby and Rails apps. Several default metrics, such as request counts, request duration, etc., will be reported automatically once you’ve added and configured the gems. Alternatively, you can use [Yabeda](https://github.com/yabeda-rb/yabeda) and [our Yabeda integration](https://github.com/honeybadger-io/yabeda-honeybadger_insights) to collect and report metrics in your Ruby and Rails apps. Several default metrics, such as request counts, request duration, etc., will be reported automatically once you’ve added and configured the gems. #### Events [Section titled “Events”](#events) If you have custom events you’d like to track, use `Honeybadger#event` to report those events to Insights: app/controllers/accounts\_controller.rb
```ruby
class AccountsController < ApplicationController
def create
# Account.create(...)
Honeybadger.event("Created account", account_id: account.id, account_name: account.name, plan: account.subscription.name)
end
end
```
More information about sending events to Insights from your Ruby and Rails apps can be found [here](/lib/ruby/insights/sending-events-to-insights).
# Send CI/CD telemetry from RWX to Honeybadger Insights
> Here's how to send CI/CD telemetry from RWX (Mint) to Honeybadger Insights using OpenTelemetry.
[RWX](https://www.rwx.com/) can send CI/CD pipeline telemetry to Honeybadger Insights using [OpenTelemetry](/guides/insights/integrations/opentelemetry/), giving you visibility into pipeline runs, task durations, and failures. ## Configuration [Section titled “Configuration”](#configuration) 1. Go to your [RWX organization observability settings](https://cloud.rwx.com/org/deep_link/manage/mint/observability). 2. Select **Honeybadger** as the OpenTelemetry provider. 3. The endpoint will default to `https://api.honeybadger.io/v1/traces`. 4. Enter your Honeybadger API key, which is available on the API keys tab of your [project settings](/guides/projects/). ## Querying your data [Section titled “Querying your data”](#querying-your-data) Once you’re receiving telemetry, you can query your CI/CD data in [Insights](/guides/insights/) using [BadgerQL](/guides/insights/badgerql/). RWX sends OpenTelemetry spans with [CI/CD semantic convention](https://opentelemetry.io/docs/specs/semconv/cicd/cicd-metrics/) fields. View recent pipeline tasks:
```badgerql
fields @ts, span_name::str, cicd.pipeline.task.run.result::str, duration::int
| filter event_type::str == "otel.span"
| filter resource.service.name::str == "rwx"
| sort @ts desc
```
Find failed tasks:
```badgerql
fields @ts, span_name::str, cicd.pipeline.run.git.repository::str, cicd.pipeline.run.git.branch::str
| filter event_type::str == "otel.span"
| filter resource.service.name::str == "rwx"
| filter cicd.pipeline.task.run.result::str == "failure"
| sort @ts desc
```
Analyze task durations:
```badgerql
fields cicd.pipeline.task.name::str, cicd.pipeline.task.run.timing.runtime.ms::int
| filter event_type::str == "otel.span"
| filter resource.service.name::str == "rwx"
| filter cicd.pipeline.task.name::str != "$run"
| stats avg(cicd.pipeline.task.run.timing.runtime.ms::int), max(cicd.pipeline.task.run.timing.runtime.ms::int) by cicd.pipeline.task.name::str
```
## Learn more [Section titled “Learn more”](#learn-more) * [RWX Honeybadger integration docs](https://www.rwx.com/docs/observability/honeybadger) * [OpenTelemetry CI/CD semantic conventions](https://opentelemetry.io/docs/specs/semconv/cicd/cicd-metrics/) * [Honeybadger OpenTelemetry integration](/guides/insights/integrations/opentelemetry/)
# Use Vector to ship your systemd logs to Honeybadger Insights
> Here's how to use Vector to watch journald and send events to Honeybadger.
[Journald](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html) is the logging system used by [systemd](https://systemd.io), the init system used on most modern Linux distributions. It’s a replacement for syslog and rsyslog, and it captures the logs for just about everything running on a Linux server, including services like web and database servers that are managed by systemd. Any systemd-managed process that sends output to stdout will show that output in journald. You can use [Vector](https://vector.dev) to watch journald and relay the events it captures. Here’s a sample configuration that will encode the journald’s data into the newline-delimited JSON format that our API expects:
```yaml
# Put this in /etc/vector/vector.yaml
sources:
journald:
type: journald
include_matches:
_TRANSPORT:
- stdout
# See the Vector Remap Language reference for more info: https://vrl.dev
transforms:
parse_logs:
type: "remap"
inputs: ["journald"]
source: |
. = {"host": .host, "unit": ._SYSTEMD_USER_UNIT || ._SYSTEMD_UNIT, "message": .message, "timestamp": .timestamp}
structured = parse_json(.message) ?? {}
. = merge!(., structured)
sinks:
honeybadger:
type: "http"
inputs: ["parse_logs"]
uri: "https://api.honeybadger.io/v1/events"
request:
headers:
X-API-Key: "PROJECT_API_KEY"
encoding:
codec: "json"
framing:
method: "newline_delimited"
batch:
max_bytes: 1000000
```
Since journald captures *everything* that happens on your server, and since you probably don’t care about stuff like kernel messages, the `sources` section of the configuration limits what it will pass on to Honeybadger. This configuration will only relay events that were emitted to stdout, like web server logs, Rails application logs, and that sort of thing. If you really want to send everything that gets logged to journald, you can delete the `include_matches` portion of the configuration. See the [Vector documentation](https://vector.dev/docs/reference/configuration/sources/journald/) for more info on filtering the journald input. The `parse_logs` transformation also reduces the amount of data sent to Insights by stripping out things like the process ID, the user running the service, etc. If you decide you want to be able to query that data in Insights, you can remove the transform and change the `honeybadger` sink `inputs` from “parse\_logs” to “journald”. Please see our documentation on ingesting [log files](/guides/insights/integrations/log-files/) for a few more handy transformations you can use in your Vector configuration. ## Quick setup method [Section titled “Quick setup method”](#quick-setup-method) If you’re running a system that uses apt to manage packages, like Debian or Ubuntu, you can use the following command to fetch and run a [script](https://gist.github.com/stympy/9ccb5a809a6731f53b3335fb4e020c2c#file-install_vector-sh) that installs the Vector package, sets up the configuration file, and starts Vector as a service:
```shell
curl -sL https://gist.github.com/stympy/9ccb5a809a6731f53b3335fb4e020c2c/raw/bc5741a4e277ea3a7d6d0f5e70a67e0767aec221/install_vector.sh > install_vector.sh && \
chmod a+x install_vector.sh && \
HONEYBADGER_API_KEY="PROJECT_API_KEY" ./install_vector.sh
```