Skip to main content
This guide is only applicable to self-hosted (on-premise) Gurubase deployments.

Overview

On-premise Gurubase deployments include Fluent Bit for log collection. By default, logs from all Gurubase services are written to a local file. You can add an OpenSearch 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)
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.

Architecture

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

It’s recommended to create a dedicated user with write-only permissions instead of using your OpenSearch admin credentials in Fluent Bit configuration files.
Run the following three commands against your OpenSearch cluster.
Replace the following placeholder values before running the commands:
PlaceholderReplace with
YOUR_ADMIN_PASSWORDYour OpenSearch admin password
YOUR_OPENSEARCH_HOSTYour OpenSearch cluster hostname
YOUR_OPENSEARCH_PORTYour OpenSearch port (443 for hosted, 9200 for self-managed)
CHOOSE_A_STRONG_PASSWORDA strong password for the new service account

Create a Write-Only Role

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

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

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"]
  }'
These three commands only need to be run once during initial setup.

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.
The Fluent Bit configuration file is located at ~/.gurubase/config/fluent-bit.conf on the host machine.
Replace YOUR_OPENSEARCH_HOST and CHOOSE_A_STRONG_PASSWORD below with the actual values from Step 1.
[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
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://.
After editing, restart the Fluent Bit container:
docker restart gurubase-fluent-bit

Step 3: Verify the Setup

Check Fluent Bit Logs

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

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:
{
  "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.
OpenSearch Dashboards is typically available at http://YOUR_OPENSEARCH_HOST:5601. Open it in your browser and log in with your OpenSearch admin credentials.
1

Open OpenSearch Dashboards

Navigate to http://YOUR_OPENSEARCH_HOST:5601 in your browser and log in with your admin credentials.
2

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.
3

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.
4

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)
5

Save Searches (Optional)

Save useful filter combinations as Saved Searches, then add them to a Dashboard for an overview panel.

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.
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).
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
        }
      ]
    }
  }'
This command only needs to be run once. The ISM template automatically attaches the policy to all future daily indices.
To verify the policy was created:
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

1

Go to the Slack API portal

Open https://api.slack.com/apps and click Create New AppFrom scratch. Name it Gurubase Onprem Alerts.
2

Enable Incoming Webhooks

In the app settings, navigate to FeaturesIncoming Webhooks and toggle it On.
3

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.
4

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.

6b. Create a Notification Channel in OpenSearch

Use the OpenSearch Notifications API to register your Slack webhook as a notification channel.
Replace YOUR_OPENSEARCH_HOST, YOUR_ADMIN_PASSWORD, and YOUR_SLACK_WEBHOOK_URL with your actual values.
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:
{
  "config_id": "RETURNED_CONFIG_ID"
}
Save the config_id from the response. You will use it as the channel_id when creating the monitor.

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.
Replace the following placeholder values before running the command:
PlaceholderReplace with
YOUR_OPENSEARCH_HOSTYour OpenSearch cluster hostname
YOUR_ADMIN_PASSWORDYour OpenSearch admin password
NOTIFICATION_CHANNEL_IDThe config_id returned in the previous step
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"
            }
          }
        ]
      }
    ]
  }'
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.

6d. Test the Monitor

You can trigger a dry run to verify the monitor works without sending an actual notification:
curl -XPOST -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID/_execute?dryrun=true'
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.
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:
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):
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 to be installed.
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:
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:
curl -XDELETE -u 'admin:YOUR_ADMIN_PASSWORD' \
  'https://YOUR_OPENSEARCH_HOST/_plugins/_alerting/monitors/MONITOR_ID'

Customization Tips

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:
{"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.
To alert only on errors from a specific Gurubase service, add a term filter to the bool.filter array:
{
  "term": {
    "container_name": "gurubase-backend"
  }
}
Adjust the schedule.period.interval value. For example, to check every minute:
{
  "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).
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:
ctx.results != null && ctx.results.size() > 0 && ctx.results[0].hits.total.value > 50
You can also create monitors visually through OpenSearch DashboardsAlertingMonitorsCreate monitor. The Dashboards UI provides a form-based editor that is equivalent to the REST API approach documented above.

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.
You can pretty-print JSON logs in the terminal with jq:
docker logs gurubase-backend --tail 10 | jq
Python services (backend, Celery):
FieldDescription
@timestampWhen the log was indexed
timestampWhen the log was produced
levelLog level (ERROR, WARNING, INFO, DEBUG)
messageThe log message
tracebackFull stack trace (only present for exceptions)
loggerPython logger name
modulePython module name
functionFunction name
lineSource line number
container_nameWhich Gurubase service produced the log
versionThe Gurubase deployment version
streamOutput stream (stdout or stderr)
Non-Python services (PostgreSQL, Redis, Milvus):
FieldDescription
@timestampWhen the log was indexed
logThe plain-text log line
container_nameWhich Gurubase service produced the log
versionThe Gurubase deployment version
streamOutput stream (stdout or stderr)

Troubleshooting

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:
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"
      ]
    }]
  }'
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.
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.
  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
  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'
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 DashboardsAlertingMonitors → edit the monitor → remove the collapse from the extraction query.

Using Elasticsearch Instead of OpenSearch

If you are using 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:
[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
Suppress_Type_Name On is required for Elasticsearch 8.x+ (just like OpenSearch 2.x+). For Elasticsearch 7.x, you can omit it.

User and Role Setup

Elasticsearch uses X-Pack Security instead of the OpenSearch Security plugin. The API paths are different:
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"]
      }
    ]
  }'
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:
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:
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

OpenSearchElasticsearch
Fluent Bit plugin nameopensearches
Security API path/_plugins/_security/api//_security/
Retention systemISM (Index State Management)ILM (Index Lifecycle Management)
Suppress_Type_NameRequired 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