> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sazabi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# osquery

> Ship osquery scheduled query results and osqueryd status logs from Linux and macOS hosts to Sazabi through an OpenTelemetry Collector or Fluent Bit.

[All osquery integration capabilities](https://docs.sazabi.com/catalogs/integrations/osquery)

## About

Search [osquery](https://osquery.io) scheduled query results and osqueryd status logs in Sazabi. osqueryd keeps writing to local files with its default `filesystem` logger, and an OpenTelemetry Collector or Fluent Bit on each host parses those files and forwards them to Sazabi over OTLP/HTTPS.

Sazabi receives two streams:

* **Query results**: one record per scheduled query row from `osqueryd.results.log` and `osqueryd.snapshots.log`, covering differential results (`added` and `removed`) and snapshot results. The full osquery result line (`name`, `hostIdentifier`, `columns`, `decorations`, `counter`, `epoch`, `unixTime`) is the log body, timestamped from `unixTime`.
* **Status logs**: osqueryd's own messages from `osqueryd.INFO`, with severity (`INFO`, `WARN`, `ERROR`, `FATAL`), source location, and message parsed out of glog's text format.

With the Collector config, every record also carries these attributes:

| Attribute | Value |
| - | - |
| `osquery.log_type` | `result` or `status` |
| `osquery.query.name` | The scheduled query name (results) |
| `osquery.action` | `added`, `removed`, or `snapshot` (results) |
| `host.id` | osquery's `hostIdentifier` (results) |
| `host.name` | The shipper host's hostname |
| `osquery.status.location` | The osquery source file and line (status) |
| `thread.id` | The osqueryd thread ID (status) |

This setup covers Linux and macOS. Windows hosts write to different paths and the Windows Event Log, which it doesn't read.

## Prerequisites

* osqueryd on Linux or macOS with the `filesystem` logger plugin (the default) writing to `/var/log/osquery`
* An OpenTelemetry Collector [contrib distribution](https://github.com/open-telemetry/opentelemetry-collector-releases/releases) (`otelcol-contrib`), or Fluent Bit v3.0 or later, on each osquery host
* Read access to `/var/log/osquery` for the shipper. osqueryd writes its logs readable only by root, so run the shipper as root or grant its group read access.
* Your Sazabi intake URL, shown above. Open the log source and copy the **Intake URL** from its setup screen (the public key is embedded in the URL).

## Set up in the dashboard

**Step 1**

### Set osquery logging flags

Add these lines to your osquery flagfile (usually `/etc/osquery/osquery.flags`):

```bash title="osquery.flags" theme={"system"}
--host_identifier=uuid
--logger_plugin=filesystem
--logger_path=/var/log/osquery
--logger_snapshot_event_type=true
```

`--host_identifier=uuid` gives each host a stable ID that becomes `host.id`. `--logger_snapshot_event_type=true` writes one line per snapshot row instead of one large array. If `--logger_plugin` already lists other plugins, keep them and add `filesystem` with a comma.

To cap disk use, also add `--logger_rotate=true`, `--logger_rotate_size=26214400`, and `--logger_rotate_max_files=25`. Both shippers keep reading across rotations.

**Step 2**

### Add the shipper configuration

Use the OpenTelemetry Collector config for per-query and per-host attributes. Replace `<your intake hostname>` with the hostname portion of your intake URL (shown above), without the `https://` scheme or path.

> [tab] OpenTelemetry Collector
>
> Create `/var/lib/otelcol/file_storage` so read offsets survive restarts, then add:
>
> ```yaml title="otelcol-contrib config.yaml" theme={"system"}
> extensions:
>   file_storage:
>     directory: /var/lib/otelcol/file_storage
>
> receivers:
>   filelog/osquery_results:
>     include:
>       - /var/log/osquery/osqueryd.results.log
>       - /var/log/osquery/osqueryd.snapshots.log
>     start_at: end
>     storage: file_storage
>     operators:
>       - type: json_parser
>         parse_to: body
>         timestamp:
>           parse_from: body.unixTime
>           layout_type: epoch
>           layout: s
>       - type: add
>         field: attributes["osquery.log_type"]
>         value: result
>       - type: copy
>         if: body.name != nil
>         from: body.name
>         to: attributes["osquery.query.name"]
>       - type: copy
>         if: body.action != nil
>         from: body.action
>         to: attributes["osquery.action"]
>       - type: copy
>         if: body.hostIdentifier != nil
>         from: body.hostIdentifier
>         to: resource["host.id"]
>   filelog/osquery_status:
>     include:
>       - /var/log/osquery/osqueryd.INFO
>     start_at: end
>     storage: file_storage
>     multiline:
>       line_start_pattern: '^[IWEF]\d{4} '
>     operators:
>       - type: filter
>         expr: 'not (body matches "^[IWEF][0-9]{4} ")'
>       - type: add
>         field: attributes["osquery.log_type"]
>         value: status
>
> processors:
>   resourcedetection/osquery:
>     detectors: [system]
>   resource/osquery:
>     attributes:
>       - key: service.name
>         value: osquery
>         action: upsert
>   transform/osquery_status:
>     error_mode: ignore
>     log_statements:
>       - context: log
>         statements:
>           - 'set(severity_text, "INFO") where attributes["osquery.log_type"] == "status" and IsMatch(body, "^I")'
>           - 'set(severity_number, SEVERITY_NUMBER_INFO) where attributes["osquery.log_type"] == "status" and IsMatch(body, "^I")'
>           - 'set(severity_text, "WARN") where attributes["osquery.log_type"] == "status" and IsMatch(body, "^W")'
>           - 'set(severity_number, SEVERITY_NUMBER_WARN) where attributes["osquery.log_type"] == "status" and IsMatch(body, "^W")'
>           - 'set(severity_text, "ERROR") where attributes["osquery.log_type"] == "status" and IsMatch(body, "^E")'
>           - 'set(severity_number, SEVERITY_NUMBER_ERROR) where attributes["osquery.log_type"] == "status" and IsMatch(body, "^E")'
>           - 'set(severity_text, "FATAL") where attributes["osquery.log_type"] == "status" and IsMatch(body, "^F")'
>           - 'set(severity_number, SEVERITY_NUMBER_FATAL) where attributes["osquery.log_type"] == "status" and IsMatch(body, "^F")'
>           - 'set(attributes["thread.id"], body) where attributes["osquery.log_type"] == "status"'
>           - 'replace_pattern(attributes["thread.id"], "(?s)^[IWEF][0-9]{4} [0-9:.]+ +([0-9]+) .*$", "$$1") where attributes["osquery.log_type"] == "status"'
>           - 'set(attributes["osquery.status.location"], body) where attributes["osquery.log_type"] == "status"'
>           - 'replace_pattern(attributes["osquery.status.location"], "(?s)^[IWEF][0-9]{4} [0-9:.]+ +[0-9]+ ([^ ]+)] .*$", "$$1") where attributes["osquery.log_type"] == "status"'
>           - 'replace_pattern(body, "(?s)^[IWEF][0-9]{4} [0-9:.]+ +[0-9]+ [^ ]+] ", "") where attributes["osquery.log_type"] == "status"'
>   batch: {}
>
> exporters:
>   otlphttp/sazabi:
>     endpoint: "https://<your intake hostname>"
>
> service:
>   extensions: [file_storage]
>   pipelines:
>     logs/osquery:
>       receivers: [filelog/osquery_results, filelog/osquery_status]
>       processors: [resourcedetection/osquery, resource/osquery, transform/osquery_status, batch]
>       exporters: [otlphttp/sazabi]
> ```

> [tab] Fluent Bit
>
> Create `/var/lib/fluent-bit` so read offsets survive restarts, then use this YAML config (Fluent Bit v3.0 or later):
>
> ```yaml title="fluent-bit.yaml" theme={"system"}
> service:
>   flush: 1
>
> parsers:
>   - name: osquery_results
>     format: json
>     time_key: unixTime
>     time_format: "%s"
>     time_keep: on
>
> multiline_parsers:
>   - name: osquery_glog_multiline
>     type: regex
>     flush_timeout: 1000
>     rules:
>       - state: start_state
>         regex: '/^[IWEF]\d{4} /'
>         next_state: cont
>       - state: cont
>         regex: '/^(?![IWEF]\d{4} )/'
>         next_state: cont
>
> pipeline:
>   inputs:
>     - name: tail
>       tag: osquery.results
>       path: /var/log/osquery/osqueryd.results.log,/var/log/osquery/osqueryd.snapshots.log
>       db: /var/lib/fluent-bit/osquery.db
>       parser: osquery_results
>     - name: tail
>       tag: osquery.status
>       path: /var/log/osquery/osqueryd.INFO
>       db: /var/lib/fluent-bit/osquery.db
>       multiline.parser: osquery_glog_multiline
>
>   filters:
>     - name: lua
>       match: osquery.status
>       call: parse_glog
>       code: |
>         local levels = { I = "INFO", W = "WARN", E = "ERROR", F = "FATAL" }
>         function parse_glog(tag, timestamp, record)
>           local line = record["log"]
>           if line == nil then
>             return -1, timestamp, record
>           end
>           local level, thread, location, message = string.match(line, "^([IWEF])%d%d%d%d [%d:.]+%s+(%d+) ([^ ]+)%] (.-)%s*$")
>           if level == nil then
>             return -1, timestamp, record
>           end
>           return 2, timestamp, { severity = levels[level], ["thread.id"] = thread, ["osquery.status.location"] = location, message = message }
>         end
>
>   outputs:
>     - name: opentelemetry
>       match: osquery.results
>       host: <your intake hostname>
>       port: 443
>       logs_uri: /v1/logs
>       tls: on
>       tls.verify: on
>     - name: opentelemetry
>       match: osquery.status
>       host: <your intake hostname>
>       port: 443
>       logs_uri: /v1/logs
>       logs_body_key: $message
>       logs_severity_text_message_key: $severity
>       tls: on
>       tls.verify: on
> ```
>
> The Fluent Bit config sends each parsed result line as the log body but doesn't set the `osquery.*` attributes.

Both configs start at the end of log files that already exist, so earlier results aren't backfilled.

**Step 3**

### Restart osqueryd and the shipper

Restart osqueryd so it picks up the new flags, then restart the Collector or Fluent Bit. Check the shipper's logs for permission, TLS, or DNS errors during startup.

**Step 4**

### Hand off the change

If the flagfile and shipper config are version-controlled or deployed by config management, commit them on a new branch, push it, and open a pull request. Otherwise apply them directly to each host. Keep the intake URL in a secret store or environment variable where your tooling supports it.

## Set up with the CLI

You can also register the osquery log source with the [Sazabi CLI](https://docs.sazabi.com/reference/cli.md) (installed and authenticated — see [CLI reference](https://docs.sazabi.com/reference/cli/log-sources.md)).

Registering the source mints the same intake URL the dashboard shows above (the public key is embedded in its hostname):

```bash theme={"system"}
sazabi log-sources create osquery --mode manual
```

The command prints the intake URL for the new source. Use its hostname in the shipper configuration shown above. Run `sazabi log-sources setup-instructions osquery` to print the setup steps with your intake URL filled in, or `sazabi log-sources get <log-source-id>` to reprint the endpoint.

## Verify

osqueryd logs `Executing scheduled query ...` status messages as soon as it runs its schedule, so status logs appear within a minute of restarting. Query results appear after each scheduled query's `interval` elapses. Open the [Intake page](https://docs.sazabi.com/guides/instrument/intake.md) in the Sazabi dashboard and confirm records from the **osquery** source appear.

## Troubleshooting

**No records at all** — Confirm the shipper can read `/var/log/osquery/osqueryd.results.log`; a permission error is the most common cause. Then check the shipper's logs for TLS, DNS, or `401`/`403` errors and confirm the hostname matches your intake URL exactly.

**Status logs arrive but no query results** — The schedule may not have run yet; check each query's `interval` in `osquery.conf`. Confirm `--logger_plugin` includes `filesystem` and `--logger_path` matches the paths in the shipper config.

**Results have no `osquery.query.name` attribute** — The Fluent Bit config doesn't set attributes. Search the body's `name` field instead, or switch to the Collector config.

**Duplicate results after a restart** — The shipper lost its read offsets. Confirm `/var/lib/otelcol/file_storage` or `/var/lib/fluent-bit` exists and persists across restarts.

**Numbers compare as strings** — osquery writes `columns` values as strings unless osqueryd runs with `--logger_numerics=true`.

## Further reading

> [card] [Intake](https://docs.sazabi.com/guides/instrument/intake.md)
>
> See per-source ingestion volume and health.

> [card] [OpenTelemetry Collector](https://docs.sazabi.com/catalogs/log-sources/send-to-an-endpoint/otel-collector.md)
>
> Send other host and application logs through the same Collector.

> [card] [Log sources](https://docs.sazabi.com/catalogs/log-sources.md)
>
> Browse the full log source catalog.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.