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

# Centralized Logging with OpenSearch

> Forward on-premise Gurubase logs to OpenSearch for centralized monitoring, search, analysis, and Slack alerting

<Warning>
  This guide is only applicable to self-hosted (on-premise) Gurubase
  deployments.
</Warning>

## Overview

On-premise Gurubase deployments include [Fluent Bit](https://fluentbit.io/) for log collection. By default, logs from all Gurubase services are written to a local file. You can add an **[OpenSearch](https://opensearch.org/)** output to forward these logs to a centralized OpenSearch cluster for real-time search, dashboards, and alerting.

This guide walks you through setting up a dedicated OpenSearch user with minimal permissions, configuring Fluent Bit to ship logs securely, and setting up Slack alerts for error detection.

## Prerequisites

* A running on-premise Gurubase deployment
* An OpenSearch cluster (self-managed or hosted) accessible from the deployment
* Admin credentials for the OpenSearch cluster (used only during setup)

<Note>
  This guide has been tested with **OpenSearch 3.5.0**. The APIs used (Security,
  ISM, Alerting, Notifications) are available in OpenSearch 2.x and later.
</Note>

## Architecture

```mermaid theme={null}
flowchart LR
    subgraph onprem [On-Premise Gurubase]
        Services[Gurubase Services]
        FluentBit[Fluent Bit]
    end
    subgraph opensearch [OpenSearch Cluster]
        API["API :9200"]
        Dashboard["Dashboards :5601"]
    end
    Services --> FluentBit
    FluentBit -->|"HTTPS :443"| API
    API --> Dashboard
```

Fluent Bit tails Docker container logs from all Gurubase services, enriches them with metadata (container name, version), parses JSON-formatted logs from Python services into structured fields, and forwards them to both a local file and your OpenSearch cluster.

## Step 1: Create a Dedicated OpenSearch User

<Note>
  It's recommended to create a dedicated user with write-only permissions
  instead of using your OpenSearch admin credentials in Fluent Bit configuration
  files.
</Note>

Run the following three commands against your OpenSearch cluster.

<Warning>
  Replace the following placeholder values before running the commands:

  | Placeholder                | Replace with                                                     |
  | -------------------------- | ---------------------------------------------------------------- |
  | `YOUR_ADMIN_PASSWORD`      | Your OpenSearch admin password                                   |
  | `YOUR_OPENSEARCH_HOST`     | Your OpenSearch cluster hostname                                 |
  | `YOUR_OPENSEARCH_PORT`     | Your OpenSearch port (`443` for hosted, `9200` for self-managed) |
  | `CHOOSE_A_STRONG_PASSWORD` | A strong password for the new service account                    |
</Warning>

### Create a Write-Only Role

```bash theme={null}
curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_security/api/roles/gurubase_fluentbit_writer_role' \
  -H 'Content-Type: application/json' \
  -d '{
    "cluster_permissions": [
      "cluster_monitor"
    ],
    "index_permissions": [
      {
        "index_patterns": ["gurubase-onprem-*"],
        "allowed_actions": [
          "indices:data/write/*",
          "indices:admin/create",
          "indices:admin/mapping/put"
        ]
      }
    ]
  }'
```

### Create the Internal User

```bash theme={null}
curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_security/api/internalusers/gurubase_fluentbit_writer' \
  -H 'Content-Type: application/json' \
  -d '{
    "password": "CHOOSE_A_STRONG_PASSWORD",
    "backend_roles": [],
    "attributes": {
      "description": "Gurubase service account for Fluent Bit log ingestion"
    }
  }'
```

### Map the Role to the User

```bash theme={null}
curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_security/api/rolesmapping/gurubase_fluentbit_writer_role' \
  -H 'Content-Type: application/json' \
  -d '{
    "users": ["gurubase_fluentbit_writer"]
  }'
```

<Note>These three commands only need to be run once during initial setup.</Note>

## Step 2: Configure Fluent Bit

Add the following `[OUTPUT]` block to your Fluent Bit configuration file. The existing file output is preserved. Fluent Bit supports multiple outputs and will send each log record to all matching outputs.

<Info>
  The Fluent Bit configuration file is located at
  `~/.gurubase/config/fluent-bit.conf` on the host machine.
</Info>

<Warning>
  Replace `YOUR_OPENSEARCH_HOST` and `CHOOSE_A_STRONG_PASSWORD` below with the
  actual values from Step 1.
</Warning>

```ini theme={null}
[OUTPUT]
    Name              opensearch
    Match             docker.*
    Host              YOUR_OPENSEARCH_HOST
    Port              YOUR_OPENSEARCH_PORT
    HTTP_User         gurubase_fluentbit_writer
    HTTP_Passwd       CHOOSE_A_STRONG_PASSWORD
    Logstash_Format   On
    Logstash_Prefix   gurubase-onprem-logs
    Logstash_DateFormat %Y.%m.%d
    Suppress_Type_Name On
    tls               On
    tls.verify        On
    Trace_Error       On
    Replace_Dots      On
    Buffer_Size       False
```

<Note>
  Set `Port` to your OpenSearch cluster's port. Common values: `443` for
  managed/hosted clusters behind a load balancer, `9200` for self-managed
  clusters. Set `Host` to just the hostname (e.g., `opensearch.example.com`),
  without `https://`.
</Note>

After editing, restart the Fluent Bit container:

```bash theme={null}
docker restart gurubase-fluent-bit
```

## Step 3: Verify the Setup

### Check Fluent Bit Logs

```bash theme={null}
docker logs gurubase-fluent-bit --tail 50 -f
```

Look for the OpenSearch output worker starting without errors. If there are permission issues, you will see `security_exception` errors in the output.

### Check the Index in OpenSearch

```bash theme={null}
curl -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/gurubase-onprem-logs-*/_count'
```

You should see a response with a non-zero document count:

```json theme={null}
{
  "count": 325,
  "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }
}
```

## Step 4: Set Up OpenSearch Dashboards

Once logs are flowing, set up OpenSearch Dashboards for browsing and searching.

<Info>
  OpenSearch Dashboards is typically available at
  `http://YOUR_OPENSEARCH_HOST:5601`. Open it in your browser and log in with
  your OpenSearch admin credentials.
</Info>

<Steps>
  <Step title="Open OpenSearch Dashboards">
    Navigate to `http://YOUR_OPENSEARCH_HOST:5601` in your browser and log in with your admin credentials.
  </Step>

  <Step title="Create an Index Pattern">
    Go to **Management** > **Dashboards Management** > **Index patterns** and click **Create index pattern**. Enter `gurubase-onprem-*` as the pattern and Click **Next step** and select `@timestamp` as the time field and click **Create index pattern**.
  </Step>

  <Step title="Browse Logs in Discover">
    Go to **Discover**, select your new index pattern, and adjust the time range. Python service logs (backend, Celery) have structured fields like `level`, `message`, `traceback`, `logger`, `module`, and `function`. Non-Python services have a plain `log` field.
  </Step>

  <Step title="Filter and Search">
    Use the search bar with KQL syntax to filter logs:

    * `level: ERROR` (all error-level logs from Python services)
    * `traceback: *` (all logs with a stack trace)
    * `container_name: *postgres*` (PostgreSQL logs only)
    * `log: *error*` (plain-text logs containing "error")
    * `container_name: *celery* and level: ERROR` (Celery errors)
  </Step>

  <Step title="Save Searches (Optional)">
    Save useful filter combinations as **Saved Searches**, then add them to a **Dashboard** for an overview panel.
  </Step>
</Steps>

## Step 5: Configure Log Retention

OpenSearch's **Index State Management (ISM)** can automatically delete old log indices to control disk usage. The following policy deletes indices older than 14 days.

<Warning>
  Replace `YOUR_OPENSEARCH_HOST` and `YOUR_ADMIN_PASSWORD` with your actual
  values. Adjust `min_index_age` to your desired retention period (e.g., `7d`,
  `30d`, `60d`).
</Warning>

```bash theme={null}
curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_ism/policies/gurubase-onprem-logs-retention' \
  -H 'Content-Type: application/json' \
  -d '{
    "policy": {
      "description": "Delete gurubase on-premise log indices older than 14 days",
      "default_state": "active",
      "states": [
        {
          "name": "active",
          "actions": [],
          "transitions": [
            {
              "state_name": "delete",
              "conditions": {
                "min_index_age": "14d"
              }
            }
          ]
        },
        {
          "name": "delete",
          "actions": [
            {
              "delete": {}
            }
          ],
          "transitions": []
        }
      ],
      "ism_template": [
        {
          "index_patterns": ["gurubase-onprem-*"],
          "priority": 100
        }
      ]
    }
  }'
```

<Note>
  This command only needs to be run once. The ISM template automatically
  attaches the policy to all future daily indices.
</Note>

To verify the policy was created:

```bash theme={null}
curl -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_ism/policies/gurubase-onprem-logs-retention'
```

## Step 6: Set Up Error Alerting with Slack

OpenSearch's **Alerting** plugin can automatically detect error logs and send notifications to Slack. This section sets up a monitor that checks for error-level logs every 5 minutes and sends a Slack message when errors are detected.

### 6a. Create a Slack Incoming Webhook

<Steps>
  <Step title="Go to the Slack API portal">
    Open [https://api.slack.com/apps](https://api.slack.com/apps) and click
    **Create New App** → **From scratch**. Name it **Gurubase Onprem Alerts**.
  </Step>

  <Step title="Enable Incoming Webhooks">
    In the app settings, navigate to **Features** → **Incoming Webhooks** and
    toggle it **On**.
  </Step>

  <Step title="Add a new webhook">
    Click **Add New Webhook to Workspace**, select the channel you want alerts
    sent to (e.g., `#gurubase-alerts`), and click **Allow**.
  </Step>

  <Step title="Copy the webhook URL">
    Copy the generated webhook URL. It looks like
    `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX`.
    You will need this in the next step.
  </Step>
</Steps>

### 6b. Create a Notification Channel in OpenSearch

Use the OpenSearch Notifications API to register your Slack webhook as a notification channel.

<Warning>
  Replace `YOUR_OPENSEARCH_HOST`, `YOUR_ADMIN_PASSWORD`, and
  `YOUR_SLACK_WEBHOOK_URL` with your actual values.
</Warning>

```bash theme={null}
curl -XPOST -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_notifications/configs/' \
  -H 'Content-Type: application/json' \
  -d '{
    "config": {
      "name": "Gurubase Onprem Alerts - Slack",
      "description": "Slack channel for Gurubase on-premise error log alerts",
      "config_type": "slack",
      "is_enabled": true,
      "slack": {
        "url": "YOUR_SLACK_WEBHOOK_URL"
      }
    }
  }'
```

The response returns a `config_id` that you need for the next step:

```json theme={null}
{
  "config_id": "RETURNED_CONFIG_ID"
}
```

<Note>
  Save the `config_id` from the response. You will use it as the `channel_id`
  when creating the monitor.
</Note>

### 6c. Create an Error Log Monitor

This monitor runs every 5 minutes. It queries all `gurubase-onprem-*` indices for error-level structured logs from Python services (using the `level` and `traceback` fields) as well as plain-text error patterns from non-Python services (using the `log` field) within the last 5-minute window. If any matches are found, it sends a summary to Slack.

<Warning>
  Replace the following placeholder values before running the command:

  | Placeholder               | Replace with                                  |
  | ------------------------- | --------------------------------------------- |
  | `YOUR_OPENSEARCH_HOST`    | Your OpenSearch cluster hostname              |
  | `YOUR_ADMIN_PASSWORD`     | Your OpenSearch admin password                |
  | `NOTIFICATION_CHANNEL_ID` | The `config_id` returned in the previous step |
</Warning>

```bash theme={null}
curl -XPOST -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "monitor",
    "name": "Gurubase Error Log Monitor",
    "monitor_type": "query_level_monitor",
    "enabled": true,
    "schedule": {
      "period": {
        "interval": 5,
        "unit": "MINUTES"
      }
    },
    "inputs": [
      {
        "search": {
          "indices": ["gurubase-onprem-*"],
          "query": {
            "size": 10,
            "_source": ["level", "message", "traceback", "log", "container_name", "@timestamp"],
            "sort": [{"@timestamp": "desc"}],
            "query": {
              "bool": {
                "filter": [
                  {
                    "range": {
                      "@timestamp": {
                        "gte": "{{period_end}}||-5m",
                        "lte": "{{period_end}}",
                        "format": "epoch_millis"
                      }
                    }
                  },
                  {
                    "bool": {
                      "should": [
                        {"term": {"level.keyword": "ERROR"}},
                        {"term": {"level.keyword": "FATAL"}},
                        {"exists": {"field": "traceback"}},
                        {
                          "query_string": {
                            "query": "log:(*error* OR *exception* OR *traceback* OR *fatal*)",
                            "analyze_wildcard": true
                          }
                        }
                      ],
                      "minimum_should_match": 1
                    }
                  }
                ]
              }
            },
            "aggregations": {
              "error_services": {
                "terms": {
                  "field": "container_name.keyword",
                  "size": 10
                }
              }
            }
          }
        }
      }
    ],
    "triggers": [
      {
        "name": "Error logs detected",
        "severity": "1",
        "condition": {
          "script": {
            "source": "ctx.results != null && ctx.results.size() > 0 && ctx.results[0].hits.total.value > 0",
            "lang": "painless"
          }
        },
        "actions": [
          {
            "name": "Notify Slack",
            "destination_id": "NOTIFICATION_CHANNEL_ID",
            "throttle_enabled": true,
            "throttle": {
              "value": 15,
              "unit": "MINUTES"
            },
            "subject_template": {
              "source": "Gurubase Error Alert",
              "lang": "mustache"
            },
            "message_template": {
              "source": "*Monitor:* {{ctx.monitor.name}}\n*Period:* {{ctx.periodStart}} - {{ctx.periodEnd}}\n*Total errors:* {{ctx.results.0.hits.total.value}}\n\n*Recent error logs:*\n{{#ctx.results.0.hits.hits}}\n- `{{_source.container_name}}`: {{_source.message}}{{_source.log}}\n{{/ctx.results.0.hits.hits}}",
              "lang": "mustache"
            }
          }
        ]
      }
    ]
  }'
```

<Info>
  **Throttling** is enabled at 15 minutes. Even if the monitor fires every 5
  minutes, Slack will receive at most one notification per 15 minutes. Adjust
  the `throttle.value` to control notification frequency.
</Info>

### 6d. Test the Monitor

You can trigger a dry run to verify the monitor works without sending an actual notification:

```bash theme={null}
curl -XPOST -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID/_execute?dryrun=true'
```

<Note>
  Replace `MONITOR_ID` with the `_id` returned when you created the monitor. The
  dry run response shows whether the trigger condition would fire based on
  current data.
</Note>

To verify the full notification flow, you can wait for an actual error to occur or temporarily lower the trigger threshold. Once verified, you should see a message in your Slack channel similar to:

> **Monitor:** Gurubase Error Log Monitor
> **Period:** 2026-02-16T12:30:00Z - 2026-02-16T12:35:00Z
> **Total errors:** 15
>
> **Recent error logs:**
>
> * `gurubase-backend`: Internal Server Error: /api/v1/...
> * `gurubase-celery-worker`: Connection refused

### 6e. Manage the Monitor

**List all monitors:**

```bash theme={null}
curl -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/_search' \
  -H 'Content-Type: application/json' \
  -d '{"query": {"match_all": {}}}'
```

**Disable the monitor** (without deleting it):

<Note>
  Replace `MONITOR_ID` with the `_id` returned when you created the monitor.
  This command fetches the current monitor definition, sets `enabled` to
  `false`, and updates it in place, preserving all existing configuration
  including Slack actions. These commands require
  [`jq`](https://jqlang.github.io/jq/) to be installed.
</Note>

```bash theme={null}
MONITOR_JSON=$(curl -s -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID')

echo "$MONITOR_JSON" | jq '.monitor.enabled = false | .monitor' | \
  curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
    'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID' \
    -H 'Content-Type: application/json' \
    -d @-
```

**Re-enable the monitor:**

```bash theme={null}
MONITOR_JSON=$(curl -s -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID')

echo "$MONITOR_JSON" | jq '.monitor.enabled = true | .monitor' | \
  curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
    'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID' \
    -H 'Content-Type: application/json' \
    -d @-
```

**Delete the monitor:**

```bash theme={null}
curl -XDELETE -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID'
```

### Customization Tips

<AccordionGroup>
  <Accordion title="Change error keywords">
    The default query matches structured `level` fields (`ERROR`, `FATAL`), the `traceback` field, and plain-text patterns in the `log` field. To also match warnings from Python services, add a `term` clause for `WARNING`:

    ```json theme={null}
    {"term": {"level.keyword": "WARNING"}}
    ```

    For non-Python service logs, modify the `query_string` in the monitor's input query. For example, to also match warnings:

    ```
    log:(*error* OR *exception* OR *traceback* OR *fatal* OR *warn*)
    ```

    Plain-text patterns must be **lowercase** because OpenSearch's default analyzer lowercases indexed text.
  </Accordion>

  <Accordion title="Filter by specific service">
    To alert only on errors from a specific Gurubase service, add a `term` filter to the `bool.filter` array:

    ```json theme={null}
    {
      "term": {
        "container_name": "gurubase-backend"
      }
    }
    ```
  </Accordion>

  <Accordion title="Change monitoring interval">
    Adjust the `schedule.period.interval` value. For example, to check every minute:

    ```json theme={null}
    {
      "period": {
        "interval": 1,
        "unit": "MINUTES"
      }
    }
    ```

    Remember to also adjust the time range in the query (`gte` value) to match the interval (e.g., `{{period_end}}||-1m`).
  </Accordion>

  <Accordion title="Alert on high error volume only">
    To avoid alerts for occasional errors, raise the trigger threshold. For example, to alert only when there are more than 50 errors in the window:

    ```painless theme={null}
    ctx.results != null && ctx.results.size() > 0 && ctx.results[0].hits.total.value > 50
    ```
  </Accordion>

  <Accordion title="Use OpenSearch Dashboards UI instead">
    You can also create monitors visually through **OpenSearch Dashboards** → **Alerting** → **Monitors** → **Create monitor**. The Dashboards UI provides a form-based editor that is equivalent to the REST API approach documented above.
  </Accordion>
</AccordionGroup>

## Log Record Fields

Gurubase uses **JSON structured logging** for Python services (backend, Celery workers, Celery beat). Each log entry, including full stack traces, is emitted as a single JSON line. Fluent Bit automatically parses these JSON logs into structured OpenSearch fields. Non-Python services (PostgreSQL, Redis, Milvus) continue to emit plain-text logs.

<Info>
  You can pretty-print JSON logs in the terminal with `jq`:

  ```bash theme={null}
  docker logs gurubase-backend --tail 10 | jq
  ```
</Info>

**Python services (backend, Celery):**

| Field            | Description                                     |
| ---------------- | ----------------------------------------------- |
| `@timestamp`     | When the log was indexed                        |
| `timestamp`      | When the log was produced                       |
| `level`          | Log level (`ERROR`, `WARNING`, `INFO`, `DEBUG`) |
| `message`        | The log message                                 |
| `traceback`      | Full stack trace (only present for exceptions)  |
| `logger`         | Python logger name                              |
| `module`         | Python module name                              |
| `function`       | Function name                                   |
| `line`           | Source line number                              |
| `container_name` | Which Gurubase service produced the log         |
| `version`        | The Gurubase deployment version                 |
| `stream`         | Output stream (`stdout` or `stderr`)            |

**Non-Python services (PostgreSQL, Redis, Milvus):**

| Field            | Description                             |
| ---------------- | --------------------------------------- |
| `@timestamp`     | When the log was indexed                |
| `log`            | The plain-text log line                 |
| `container_name` | Which Gurubase service produced the log |
| `version`        | The Gurubase deployment version         |
| `stream`         | Output stream (`stdout` or `stderr`)    |

## Troubleshooting

<AccordionGroup>
  <Accordion title="Permission denied errors (security_exception)">
    If Fluent Bit logs show `no permissions for [indices:data/write/bulk[s]]`, the role may be missing wildcard write permissions. Update the role to use `indices:data/write/*` instead of individual write actions:

    ```bash theme={null}
    curl -XPUT -u 'admin:YOUR_ADMIN_PASSWORD' \
      'https://YOUR_OPENSEARCH_HOST/_plugins/_security/api/roles/gurubase_fluentbit_writer_role' \
      -H 'Content-Type: application/json' \
      -d '{
        "cluster_permissions": ["cluster_monitor"],
        "index_permissions": [{
          "index_patterns": ["gurubase-onprem-*"],
          "allowed_actions": [
            "indices:data/write/*",
            "indices:admin/create",
            "indices:admin/mapping/put"
          ]
        }]
      }'
    ```
  </Accordion>

  <Accordion title="TLS certificate errors">
    If your OpenSearch cluster uses a self-signed certificate, set `tls.verify
          Off` in the Fluent Bit output configuration. For production, it is recommended
    to use a valid certificate and keep verification enabled.
  </Accordion>

  <Accordion title="Connection refused or timeout">
    Ensure the Fluent Bit container can reach your OpenSearch cluster. Check that
    the hostname resolves correctly and the port is accessible. If OpenSearch is
    behind a firewall or VPN, the Gurubase host must have network access.
  </Accordion>

  <Accordion title="No data appearing in OpenSearch">
    1. Check Fluent Bit logs: `docker logs gurubase-fluent-bit --tail 100` 2.
       Verify the container name grep filter matches your deployment prefix 3. Ensure
       the Fluent Bit flush interval (default 5 seconds) has elapsed 4. Confirm the
       index pattern in OpenSearch Dashboards matches the index name
  </Accordion>

  <Accordion title="Slack alerts not being sent">
    1. Verify the notification channel exists: `curl -u
         'admin:YOUR_ADMIN_PASSWORD'
         'https://YOUR_OPENSEARCH_HOST/_plugins/_notifications/configs/NOTIFICATION_CHANNEL_ID'`
    2. Send a test notification: `curl -u 'admin:YOUR_ADMIN_PASSWORD'
         'https://YOUR_OPENSEARCH_HOST/_plugins/_notifications/feature/test/NOTIFICATION_CHANNEL_ID'`
    3. Check if throttling is suppressing notifications. The default throttle is
       15 minutes 4. Verify the Slack webhook URL is still valid in your Slack app
       settings 5. Check monitor execution status: `curl -u
         'admin:YOUR_ADMIN_PASSWORD'
         'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/alerts'`
  </Accordion>

  <Accordion title="Monitor error: no mapping found for message.keyword">
    If the monitor alert shows `IllegalArgumentException[no mapping found for message.keyword in order to collapse on]`, the monitor query is using a `collapse` clause on the `message.keyword` field. Non-Python services (PostgreSQL, Redis, Milvus) don't have a `message` field — they use `log` instead. When an index contains only non-Python service logs, the `message` field mapping doesn't exist, causing the collapse (and the entire query) to fail.

    **Fix:** Remove the `collapse` clause from the monitor's input query. The `size: 10` limit already restricts the number of results returned. Update the monitor via the REST API or through **OpenSearch Dashboards** → **Alerting** → **Monitors** → edit the monitor → remove the collapse from the extraction query.
  </Accordion>
</AccordionGroup>

## Using Elasticsearch Instead of OpenSearch

If you are using [Elasticsearch](https://www.elastic.co/elasticsearch) instead of OpenSearch, the same Fluent Bit setup works with minimal changes.

### Fluent Bit Configuration

Change the plugin name from `opensearch` to `es`. All other parameters remain the same:

```ini theme={null}
[OUTPUT]
    Name              es
    Match             docker.*
    Host              YOUR_ELASTICSEARCH_HOST
    Port              YOUR_ELASTICSEARCH_PORT
    HTTP_User         gurubase_fluentbit_writer
    HTTP_Passwd       CHOOSE_A_STRONG_PASSWORD
    Logstash_Format   On
    Logstash_Prefix   gurubase-onprem-logs
    Logstash_DateFormat %Y.%m.%d
    Suppress_Type_Name On
    tls               On
    tls.verify        On
    Trace_Error       On
    Replace_Dots      On
    Buffer_Size       False
```

<Note>
  `Suppress_Type_Name On` is required for Elasticsearch 8.x+ (just like
  OpenSearch 2.x+). For Elasticsearch 7.x, you can omit it.
</Note>

### User and Role Setup

Elasticsearch uses **X-Pack Security** instead of the OpenSearch Security plugin. The API paths are different:

```bash theme={null}
curl -XPUT -u 'elastic:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_ELASTICSEARCH_HOST/_security/role/gurubase_fluentbit_writer_role' \
  -H 'Content-Type: application/json' \
  -d '{
    "cluster": ["monitor"],
    "indices": [
      {
        "names": ["gurubase-onprem-*"],
        "privileges": ["create_index", "write"]
      }
    ]
  }'
```

```bash theme={null}
curl -XPUT -u 'elastic:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_ELASTICSEARCH_HOST/_security/user/gurubase_fluentbit_writer' \
  -H 'Content-Type: application/json' \
  -d '{
    "password": "CHOOSE_A_STRONG_PASSWORD",
    "roles": ["gurubase_fluentbit_writer_role"]
  }'
```

### Log Retention with ILM

Elasticsearch uses **Index Lifecycle Management (ILM)** instead of ISM. Create a policy that deletes indices after 14 days:

```bash theme={null}
curl -XPUT -u 'elastic:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_ELASTICSEARCH_HOST/_ilm/policy/gurubase-onprem-logs-retention' \
  -H 'Content-Type: application/json' \
  -d '{
    "policy": {
      "phases": {
        "hot": {
          "actions": {}
        },
        "delete": {
          "min_age": "14d",
          "actions": {
            "delete": {}
          }
        }
      }
    }
  }'
```

Then apply the policy to matching indices via an index template:

```bash theme={null}
curl -XPUT -u 'elastic:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_ELASTICSEARCH_HOST/_index_template/gurubase-onprem-logs' \
  -H 'Content-Type: application/json' \
  -d '{
    "index_patterns": ["gurubase-onprem-*"],
    "template": {
      "settings": {
        "index.lifecycle.name": "gurubase-onprem-logs-retention"
      }
    }
  }'
```

### Key Differences Summary

|                        | OpenSearch                      | Elasticsearch                    |
| ---------------------- | ------------------------------- | -------------------------------- |
| Fluent Bit plugin name | `opensearch`                    | `es`                             |
| Security API path      | `/_plugins/_security/api/`      | `/_security/`                    |
| Retention system       | ISM (Index State Management)    | ILM (Index Lifecycle Management) |
| `Suppress_Type_Name`   | Required for 2.x+               | Required for 8.x+                |
| Dashboards port        | `:5601` (OpenSearch Dashboards) | `:5601` (Kibana)                 |

## Security Considerations

* The `gurubase_fluentbit_writer` user has **write-only** access to `gurubase-onprem-*` indices
* No read, delete, or cluster administration permissions are granted
* If the credentials are compromised, an attacker can only append log data. They cannot read existing data or access other indices
* Admin credentials are only used during the one-time setup and are not stored in any configuration file
* For additional security, consider rotating the `gurubase_fluentbit_writer` password periodically

## Next Steps

<CardGroup cols={2}>
  <Card title="Audit Logs" icon="clipboard-list" href="/guides/audit-logs">
    Track user actions and API calls within Gurubase
  </Card>

  <Card title="Self-Hosted Overview" icon="server" href="/self-hosted">
    Learn more about on-premise deployment options
  </Card>
</CardGroup>
