# graylog.dadl -- Graylog REST API for ToolMesh # Log management, search, streams, pipelines, inputs, alerts, and cluster administration # # Domain Notes for LLM consumers: # - Graylog is a centralized log management platform. The REST API is served at /api/ on port 9000 by default. # - Authentication: HTTP Basic Auth using access tokens. Username = token string, password = literal "token". # Create tokens via System > Users and Teams > Edit Tokens in the UI, or via the API. # - CSRF protection: all POST/PUT/DELETE requests MUST include header "X-Requested-By: ToolMesh" (any non-empty value). # - No API versioning -- all endpoints are directly under /api/ with no /v1/ prefix. # - Search query syntax: Lucene-based. Examples: "source:myserver AND level:3", "http_response_code:[500 TO 599]". # - Time ranges come in three flavors and follow the Views/Search shape: # - Absolute: { type: "absolute", from: "yyyy-MM-ddTHH:mm:ss.SSSZ", to: "..." } # - Relative: { type: "relative", range: 3600 } (seconds from now) # - Keyword: { type: "keyword", keyword: "last 5 minutes" } (Natty parser, OPEN-ended forms only) # - Aggregations (terms, histogram, stats, pivot, source x level matrix) live exclusively # on the Views/Search API in Graylog 6+. Use the composites aggregate_by_field, # aggregate_pivot, histogram_grouped, field_stats -- the legacy # /search/universal/{relative,absolute,keyword}/{terms,histogram,stats,fieldhistogram} # endpoints were removed in Graylog 6.0 and are no longer surfaced here. # - Plain message retrieval is still available via search_relative (legacy, working in 6.x) # and search_keyword (open-ended Natty phrases). For absolute windows or projected fields # use views_search_sync / search_messages_compact. # - search_keyword / export_search_keyword REQUIRE a "timezone" query parameter. It is # undocumented in Graylog's own API browser (no @Parameter annotation) but carries # @NotEmpty, so omitting it fails with HTTP 400 "KeywordSearchResource.searchKeyword.arg2 # must not be empty". Both tools default it to UTC so it is always sent. # - CSV export is available via export_search_relative, export_search_keyword and # views_search_messages. These endpoints only produce text/csv, so each declares an # `Accept` header parameter (default text/csv) that overrides the backend-wide # Accept: application/json -- without it Graylog answers HTTP 406. Their responses are # streamed to the ToolMesh file broker, so the tool result is a time-limited DOWNLOAD # URL rather than inline rows: treat exported log data accordingly and prefer the # inline JSON tools when the result set is small. An empty result set yields no file # at all (size_bytes 0, url null), and views_search_messages can return a backend # error as text INSIDE the CSV with HTTP 200 -- verify the first line is the header row. # - Stream scoping differs per endpoint and is easy to get wrong: # - legacy /search/universal/* (search_relative, search_keyword, both exports): # ONLY filter="streams:STREAM_ID" works. Graylog 6.1 has no `streams` query param # there; newer server code adds one, but on 6.1 an unknown query param is dropped # silently, so it would return UNSCOPED results with no error. Not surfaced here. # - /views/search/sync: no top-level `streams` (HTTP 400) -- use query.filter. # - /views/search/messages: a top-level `streams` array IS honored. # - Endpoints that do NOT exist in Graylog 6.x and are therefore not surfaced, all of # them verified against a live 6.1.16 and the server sources: /count/total, # /filters/blacklist/*, the legacy alerts API (/streams/alerts, /streams/{id}/alerts # -- superseded by events), GET on /system/inputs/{id}/staticfields (POST/DELETE only), # and the single-item paths /search/saved/{id} and /dashboards/{id}. The last two are # the common trap: the LIST endpoints /search/saved and /dashboards do exist, but a # single dashboard or saved search is fetched as a View via get_view (/views/{id}). # - An unknown /api path does NOT 404 -- Graylog serves the web UI, so the tool gets # HTTP 200 with the SPA HTML. Treat an HTML body as "this endpoint does not exist". # - There is NO /sources endpoint in Graylog 6.x -- the class backing it does not exist # in the server codebase, and /api/sources falls through to the web UI, which answers # with the SPA shell (HTML, HTTP 200). It is therefore not surfaced as a tool; an # earlier revision blamed a base_url without /api, which was a misdiagnosis. For # "which sources are sending, and how much", use aggregate_by_field(field: "source"). # - Search results are limited to 10,000 by default (Elasticsearch result window). # - Pagination uses offset/limit pattern. Response includes "total" for total count. # - Stream filtering in search: use filter param "streams:STREAM_ID" to scope queries. # - IDs are MongoDB ObjectIDs (24-char hex strings) for most resources. # - Pipeline rules use Graylog's own rule language (not Lucene). Parse/validate via dedicated endpoints. # - Sidecar = agent-based log collector management (Filebeat, Winlogbeat, NXLog configs). # - No built-in rate limiting on the REST API. # - Index sets control retention, rotation, and sharding. Each stream maps to one index set. # - The "Default Stream" (id: "000000000000000000000001") cannot be deleted. spec: "https://dadl.ai/spec/dadl-spec-v0.1.md" credits: - "Dunkel Cloud GmbH -- maintainer" source_name: "Graylog REST API" source_url: "https://go2docs.graylog.org/current/setting_up_graylog/rest_api.html" date: "2026-08-04" backend: name: graylog type: rest version: "1.1" # base_url intentionally omitted -- self-hosted, provided via backends.yaml description: "Graylog REST API -- log search (Views/Search + legacy universal), streams, pipelines, inputs, alerts, events, dashboards, users, roles, sidecars, index management, and cluster administration. Targets Graylog 6.x." auth: type: basic username_credential: graylog_token password_credential: graylog_token_password # literal value "token" -- set in credential store defaults: headers: Accept: application/json Content-Type: application/json X-Requested-By: ToolMesh pagination: strategy: offset request: cursor_param: offset limit_param: limit limit_default: 50 behavior: expose max_pages: 10 errors: format: json message_path: "$.message" code_path: "$.type" retry_on: [429, 502, 503, 504] retry_strategy: max_retries: 3 backoff: exponential initial_delay: 1s terminal: [400, 401, 403, 404] response: max_items: 500 allow_jq_override: true coverage: endpoints: 216 total_endpoints: 320 percentage: 68 focus: "Views/Search API (sync, messages, pivot/histogram/stats via composites), legacy relative + keyword search, CSV export, views, streams, stream rules, pipelines, pipeline rules, inputs, extractors, events, notifications, dashboards, users, roles, tokens, index sets, indices, grok patterns, lookup tables, sidecars, sidecar configurations, collectors, content packs, outputs, metrics, system info, cluster, processing control, loggers, forensic incident_window composite" missing: "enterprise-only plugins (archives, audit log, reports, license, data lake), Views CRUD (saved views, dashboard widgets), Views write CRUD (create/update/delete -- POST/PUT/DELETE /views), legacy alerts API (removed in 6.x, superseded by events), message decorators, debug events, codec types, retention/rotation strategy config, LDAP config, async Views/Search (poll-based), absolute-range legacy search (covered by views_search_sync)" last_reviewed: "2026-08-04" setup: credential_steps: - "1. Log into your Graylog web interface (default: http://:9000)" - "2. Navigate to System > Users and Teams" - "3. Click your username, then 'Edit Tokens'" - "4. Enter a token name (e.g., 'toolmesh') and click 'Create Token'" - "5. Copy the generated token -- it is shown only once" - "6. Set CREDENTIAL_GRAYLOG_TOKEN to the token string" - "7. Set CREDENTIAL_GRAYLOG_TOKEN_PASSWORD to the literal string 'token'" env_var: CREDENTIAL_GRAYLOG_TOKEN, CREDENTIAL_GRAYLOG_TOKEN_PASSWORD backends_yaml: | - name: graylog transport: rest dadl: graylog.dadl url: "https://graylog.example.com/api" required_scopes: - "reader" optional_scopes: - "admin" docs_url: "https://go2docs.graylog.org/current/setting_up_graylog/rest_api.html" notes: > Graylog uses HTTP Basic Auth with access tokens. The username is the token string, the password is the literal word "token". Tokens have configurable TTL (default 30 days). Self-hosted: adjust the url in backends.yaml to match your Graylog server address. tools: # ────────────────────────────────────────────── # Search -- Relative Time Range # ────────────────────────────────────────────── search_relative: method: GET path: /search/universal/relative access: read description: > Search messages with a relative time range (seconds from now). Query uses Lucene syntax. Returns messages with id, timestamp, source, and all fields. Default limit 150, max 10000. Scope to a stream with filter "streams:STREAM_ID" -- that is the only stream scoping this endpoint honors on Graylog 6.1. params: query: { type: string, in: query, required: true, description: "Lucene query string, e.g. 'source:myserver AND level:3'" } range: { type: integer, in: query, required: true, description: "Relative timeframe in seconds from now (e.g. 3600 = last hour)" } limit: { type: integer, in: query, default: 150, description: "Max messages to return (max 10000)" } offset: { type: integer, in: query, default: 0, description: "Pagination offset" } filter: { type: string, in: query, description: "Stream filter, e.g. 'streams:000000000000000000000001'" } fields: { type: string, in: query, description: "Comma-separated list of fields to return" } sort: { type: string, in: query, description: "Sort field and order, e.g. 'timestamp:desc'" } decorate: { type: boolean, in: query, default: true, description: "Run decorators on results" } response: result_path: "$" pagination: none export_search_relative: method: GET path: /search/universal/relative/export access: read description: > CSV export of a relative-time-range search. Same query semantics as search_relative, but returns text/csv streamed to the file broker -- the tool result is a download URL, not inline rows. "fields" is REQUIRED here (unlike search_relative) and defines the column order. Use for bulk pulls; for inline results use search_relative or search_messages_compact. Leave the Accept parameter at its default -- it is what selects CSV. params: query: { type: string, in: query, required: true, description: "Lucene query string" } range: { type: integer, in: query, required: true, description: "Relative timeframe in seconds from now (0 = all time)" } fields: { type: string, in: query, required: true, description: "REQUIRED. Comma-separated list of fields, in output column order" } limit: { type: integer, in: query, description: "Max messages to export (omit for no limit)" } offset: { type: integer, in: query, default: 0, description: "Offset" } batch_size: { type: integer, in: query, default: 500, description: "Backend scroll batch size (Graylog default 500)" } filter: { type: string, in: query, description: "Stream filter, e.g. 'streams:000000000000000000000001'" } Accept: { type: string, in: header, default: "text/csv", description: "Response media type -- the endpoint only produces text/csv; overriding this yields HTTP 406" } response: binary: true content_type: text/csv result_path: "$" pagination: none # ────────────────────────────────────────────── # Search -- Keyword Time Range # ────────────────────────────────────────────── search_keyword: method: GET path: /search/universal/keyword access: read description: > Search messages with a Natty-parsed open-ended time range like "last 5 minutes", "last 7 hours", "yesterday", "last week". The keyword is parsed in the "timezone" time zone (default UTC) -- override it to make phrases like "yesterday" mean yesterday in local time. WARNING: closed/relative phrases like "7 hours ago" are rejected with HTTP 400. For closed absolute windows or aggregation use the views_search_sync / aggregate_pivot composites instead. params: query: { type: string, in: query, required: true } keyword: { type: string, in: query, required: true, description: "Open-ended Natty phrase, e.g. 'last 5 minutes', 'yesterday'" } timezone: { type: string, in: query, default: "UTC", description: "IANA time zone the keyword is parsed in, e.g. 'Europe/Berlin'. Sent on every call -- Graylog rejects an empty value with HTTP 400 (arg2 must not be empty)." } limit: { type: integer, in: query, default: 150 } offset: { type: integer, in: query, default: 0 } filter: { type: string, in: query } fields: { type: string, in: query } sort: { type: string, in: query } decorate: { type: boolean, in: query, default: true } response: result_path: "$" pagination: none export_search_keyword: method: GET path: /search/universal/keyword/export access: read description: > CSV export of a keyword-time-range search. Same keyword grammar and timezone handling as search_keyword, but returns text/csv streamed to the file broker -- the tool result is a download URL, not inline rows. "fields" is REQUIRED here and defines the column order. Leave the Accept parameter at its default -- it is what selects CSV. params: query: { type: string, in: query, required: true, description: "Lucene query string" } keyword: { type: string, in: query, required: true, description: "Open-ended Natty phrase, e.g. 'last 5 minutes', 'yesterday'" } timezone: { type: string, in: query, default: "UTC", description: "IANA time zone the keyword is parsed in. Required by Graylog -- an empty value returns HTTP 400." } fields: { type: string, in: query, required: true, description: "REQUIRED. Comma-separated list of fields, in output column order" } limit: { type: integer, in: query, description: "Max messages to export (omit for no limit)" } offset: { type: integer, in: query, default: 0, description: "Offset" } batch_size: { type: integer, in: query, default: 500, description: "Backend scroll batch size (Graylog default 500)" } filter: { type: string, in: query, description: "Stream filter, e.g. 'streams:000000000000000000000001'" } Accept: { type: string, in: header, default: "text/csv", description: "Response media type -- the endpoint only produces text/csv; overriding this yields HTTP 406" } response: binary: true content_type: text/csv result_path: "$" pagination: none # ────────────────────────────────────────────── # Views / Search API (Graylog 4+ canonical aggregation) # ────────────────────────────────────────────── views_search_sync: method: POST path: /views/search/sync access: read description: > Execute a Views/Search query synchronously and return results inline. Body shape: { id?, queries: [...], parameters: [], skip_no_streams_check? }. NOTE: there is no top-level "streams" field -- Graylog rejects it with HTTP 400. Scope to streams via query.filter (e.g. { type: "or", filters: [{ type: "stream", id: STREAM_ID }] }) on the individual query object. Each query has id, query.query_string, timerange (absolute / relative / keyword), optional filter, and search_types[] (pivot, messages, events, ...). For everyday forensic work prefer the higher-level composites: aggregate_by_field, aggregate_pivot, histogram_grouped, field_stats, search_messages_compact, incident_window. Server-side timeout via ?timeout=, default 60. params: queries: { type: array, in: body, required: true, description: "Array of query objects" } parameters: { type: array, in: body, default: [], description: "Declared parameters (usually [])" } skip_no_streams_check: { type: boolean, in: body, description: "Skip the safety check that requires at least one stream filter (admin-only)" } timeout: { type: integer, in: query, default: 60, description: "Server-side timeout in seconds" } response: result_path: "$" pagination: none views_search_messages: method: POST path: /views/search/messages access: read description: > Standalone CSV message export with free-form parameters -- no saved Search needed. This is NOT the views_search_sync body shape. Body fields: timerange (required, the usual absolute / relative / keyword object), query_string (required, { "type": "elasticsearch", "query_string": "level:3" }), streams (array of stream IDs -- allowed here, unlike views_search_sync, and actually honored), fields_in_order (array, MUST be non-empty, defines CSV column order), chunk_size, time_zone, limit. IMPORTANT: limit is only enforced at CHUNK boundaries -- with the default chunk_size of 1000, limit 3 still returns 1000 rows. To cap the output set chunk_size to the row count you want. time_zone defaults to the API user's configured time zone (NOT UTC) -- pass it explicitly for stable timestamps. Returns text/csv streamed to the file broker -- the tool result is a download URL, not inline rows. Preferred over export_search_relative / export_search_keyword when you need an absolute window, exact field projection or real stream scoping. Leave the Accept parameter at its default -- it selects CSV. params: timerange: { type: object, in: body, required: true, description: "Time range object, e.g. { type: 'relative', range: 3600 }" } query_string: { type: object, in: body, required: true, description: "{ type: 'elasticsearch', query_string: 'level:3' } -- use query_string '' or '*' to match everything" } streams: { type: array, in: body, description: "Stream IDs to scope the export to. An unknown ID yields an error string inside the CSV body with HTTP 200." } fields_in_order: { type: array, in: body, default: ["timestamp", "source", "message"], description: "Non-empty list of fields in CSV column order" } chunk_size: { type: integer, in: body, default: 1000, description: "Backend export chunk size -- also the effective granularity of `limit`" } time_zone: { type: string, in: body, description: "IANA time zone for rendering timestamps, e.g. 'Europe/Berlin'. Defaults to the API user's time zone, not UTC." } limit: { type: integer, in: body, description: "Max messages to export (positive; rounded up to whole chunks -- see chunk_size). Omit for no limit." } Accept: { type: string, in: header, default: "text/csv", description: "Response media type -- the endpoint only produces text/csv; overriding this yields HTTP 406" } response: binary: true content_type: text/csv result_path: "$" pagination: none # ────────────────────────────────────────────── # Messages # ────────────────────────────────────────────── get_message: method: GET path: /messages/{index}/{messageId} access: read description: "Retrieve a single message by its ID and index name. Returns all message fields." params: index: { type: string, in: path, required: true, description: "Index name the message is stored in" } messageId: { type: string, in: path, required: true, description: "Message ID" } response: result_path: "$" pagination: none parse_message: method: POST path: /messages/parse access: read description: "Parse a raw message string using Graylog's message parsing pipeline. Useful for testing extractors." params: message: { type: string, in: body, required: true } codec: { type: string, in: body, required: true, description: "Codec to use, e.g. 'syslog'" } remote_address: { type: string, in: body } response: result_path: "$" pagination: none analyze_message_field: method: GET path: /messages/{index}/analyze access: read description: "Analyze a message field value to show how Elasticsearch tokenizes it." params: index: { type: string, in: path, required: true } string: { type: string, in: query, required: true, description: "Field value to analyze" } field: { type: string, in: query, required: true, description: "Field name" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Saved Searches # ────────────────────────────────────────────── list_saved_searches: method: GET path: /search/saved access: read description: "List all saved searches." response: result_path: "$.searches" # ────────────────────────────────────────────── # Streams # ────────────────────────────────────────────── list_streams: method: GET path: /streams access: read description: > List all streams. Returns stream id, title, description, rules, matching_type, disabled status, and index_set_id. The Default Stream ID is "000000000000000000000001". response: result_path: "$.streams" get_stream: method: GET path: /streams/{streamId} access: read description: "Get a single stream by ID with its configuration and rules." params: streamId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_stream: method: POST path: /streams access: write description: > Create a new stream. matching_type: AND (all rules must match) or OR (any rule matches). The stream is created paused -- call resume_stream to start routing messages. params: title: { type: string, in: body, required: true } description: { type: string, in: body } matching_type: { type: string, in: body, default: "AND", description: "AND or OR" } rules: { type: array, in: body, description: "Array of stream rule objects" } index_set_id: { type: string, in: body, description: "Index set ID for this stream" } remove_matches_from_default_stream: { type: boolean, in: body, default: false } response: result_path: "$" pagination: none update_stream: method: PUT path: /streams/{streamId} access: write description: "Update a stream's title, description, matching type, or index set." params: streamId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } matching_type: { type: string, in: body } index_set_id: { type: string, in: body } remove_matches_from_default_stream: { type: boolean, in: body } response: result_path: "$" pagination: none delete_stream: method: DELETE path: /streams/{streamId} access: dangerous description: "Delete a stream. The Default Stream cannot be deleted." params: streamId: { type: string, in: path, required: true } pagination: none clone_stream: method: POST path: /streams/{streamId}/clone access: write description: "Clone a stream including all its rules." params: streamId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } response: result_path: "$" pagination: none pause_stream: method: POST path: /streams/{streamId}/pause access: write description: "Pause a stream. Messages will no longer be routed to this stream." params: streamId: { type: string, in: path, required: true } pagination: none resume_stream: method: POST path: /streams/{streamId}/resume access: write description: "Resume a paused stream. Messages will start routing to this stream again." params: streamId: { type: string, in: path, required: true } pagination: none list_enabled_streams: method: GET path: /streams/enabled access: read description: "List only enabled (non-paused) streams." response: result_path: "$.streams" test_stream_match: method: POST path: /streams/{streamId}/testMatch access: read description: "Test if a message would match the rules of a stream." params: streamId: { type: string, in: path, required: true } message: { type: object, in: body, required: true, description: "Message object with fields to test" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Stream Rules # ────────────────────────────────────────────── list_stream_rules: method: GET path: /streams/{streamId}/rules access: read description: > List all rules for a stream. Rule types: 1=exact match, 2=regex match, 3=greater than, 4=less than, 5=field presence, 6=contains, 7=always match, 8=match input. params: streamId: { type: string, in: path, required: true } response: result_path: "$.stream_rules" get_stream_rule: method: GET path: /streams/{streamId}/rules/{ruleId} access: read description: "Get a single stream rule." params: streamId: { type: string, in: path, required: true } ruleId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_stream_rule: method: POST path: /streams/{streamId}/rules access: write description: > Create a stream rule. type is an integer: 1=exact, 2=regex, 3=greater_than, 4=less_than, 5=field_presence, 6=contains, 7=always_match, 8=match_input. params: streamId: { type: string, in: path, required: true } field: { type: string, in: body, required: true, description: "Message field to match against" } type: { type: integer, in: body, required: true, description: "Rule type (1-8)" } value: { type: string, in: body, required: true, description: "Value to compare" } inverted: { type: boolean, in: body, default: false, description: "Invert the rule match" } description: { type: string, in: body } response: result_path: "$" pagination: none update_stream_rule: method: PUT path: /streams/{streamId}/rules/{ruleId} access: write description: "Update a stream rule." params: streamId: { type: string, in: path, required: true } ruleId: { type: string, in: path, required: true } field: { type: string, in: body, required: true } type: { type: integer, in: body, required: true } value: { type: string, in: body, required: true } inverted: { type: boolean, in: body, default: false } description: { type: string, in: body } response: result_path: "$" pagination: none delete_stream_rule: method: DELETE path: /streams/{streamId}/rules/{ruleId} access: dangerous description: "Delete a stream rule." params: streamId: { type: string, in: path, required: true } ruleId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Stream Outputs # ────────────────────────────────────────────── list_stream_outputs: method: GET path: /streams/{streamId}/outputs access: read description: "List outputs assigned to a stream." params: streamId: { type: string, in: path, required: true } response: result_path: "$.outputs" add_stream_output: method: POST path: /streams/{streamId}/outputs access: write description: "Add an existing output to a stream." params: streamId: { type: string, in: path, required: true } outputs: { type: array, in: body, required: true, description: "Array of output IDs to add" } pagination: none remove_stream_output: method: DELETE path: /streams/{streamId}/outputs/{outputId} access: write description: "Remove an output from a stream." params: streamId: { type: string, in: path, required: true } outputId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Pipelines # ────────────────────────────────────────────── list_pipelines: method: GET path: /system/pipelines/pipeline access: read description: > List all processing pipelines. Each pipeline has stages with rules. Pipeline source uses Graylog's pipeline rule language. response: result_path: "$" get_pipeline: method: GET path: /system/pipelines/pipeline/{pipelineId} access: read description: "Get a single pipeline by ID, including its source code and stages." params: pipelineId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_pipeline: method: POST path: /system/pipelines/pipeline access: write description: > Create a processing pipeline. The source field contains the pipeline definition in Graylog pipeline language. Validate with parse_pipeline first. params: title: { type: string, in: body, required: true } description: { type: string, in: body } source: { type: string, in: body, required: true, description: "Pipeline source in Graylog pipeline language" } response: result_path: "$" pagination: none update_pipeline: method: PUT path: /system/pipelines/pipeline/{pipelineId} access: write description: "Update a pipeline's source, title, or description." params: pipelineId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } source: { type: string, in: body, required: true } response: result_path: "$" pagination: none delete_pipeline: method: DELETE path: /system/pipelines/pipeline/{pipelineId} access: dangerous description: "Delete a processing pipeline." params: pipelineId: { type: string, in: path, required: true } pagination: none parse_pipeline: method: POST path: /system/pipelines/pipeline/parse access: read description: "Parse and validate pipeline source without creating it. Returns parse errors if invalid." params: source: { type: string, in: body, required: true } response: result_path: "$" pagination: none simulate_pipeline: method: POST path: /system/pipelines/simulate access: read description: "Simulate pipeline processing on a message to see which rules would match and what output would be produced." params: stream_id: { type: string, in: body, required: true } message: { type: object, in: body, required: true, description: "Message fields to simulate with" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Pipeline Rules # ────────────────────────────────────────────── list_pipeline_rules: method: GET path: /system/pipelines/rule access: read description: "List all pipeline rules. Rules are written in Graylog's rule language." response: result_path: "$" get_pipeline_rule: method: GET path: /system/pipelines/rule/{ruleId} access: read description: "Get a single pipeline rule by ID." params: ruleId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_pipeline_rule: method: POST path: /system/pipelines/rule access: write description: "Create a new pipeline rule. Validate with parse_pipeline_rule first." params: title: { type: string, in: body, required: true } description: { type: string, in: body } source: { type: string, in: body, required: true, description: "Rule source in Graylog rule language" } response: result_path: "$" pagination: none update_pipeline_rule: method: PUT path: /system/pipelines/rule/{ruleId} access: write description: "Update a pipeline rule." params: ruleId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } source: { type: string, in: body, required: true } response: result_path: "$" pagination: none delete_pipeline_rule: method: DELETE path: /system/pipelines/rule/{ruleId} access: dangerous description: "Delete a pipeline rule." params: ruleId: { type: string, in: path, required: true } pagination: none parse_pipeline_rule: method: POST path: /system/pipelines/rule/parse access: read description: "Parse and validate a pipeline rule without creating it." params: source: { type: string, in: body, required: true } response: result_path: "$" pagination: none list_pipeline_functions: method: GET path: /system/pipelines/rule/functions access: read description: "List all available functions that can be used in pipeline rules." response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Pipeline Connections (Stream-Pipeline mapping) # ────────────────────────────────────────────── list_pipeline_connections: method: GET path: /system/pipelines/connections access: read description: "List all stream-to-pipeline connections." response: result_path: "$" get_stream_pipeline_connections: method: GET path: /system/pipelines/connections/{streamId} access: read description: "Get pipeline connections for a specific stream." params: streamId: { type: string, in: path, required: true } response: result_path: "$" pagination: none connect_pipeline_to_stream: method: POST path: /system/pipelines/connections/to_stream access: write description: "Connect pipelines to a stream." params: stream_id: { type: string, in: body, required: true } pipeline_ids: { type: array, in: body, required: true, description: "Array of pipeline IDs to connect" } response: result_path: "$" pagination: none connect_stream_to_pipeline: method: POST path: /system/pipelines/connections/to_pipeline access: write description: "Connect streams to a pipeline." params: pipeline_id: { type: string, in: body, required: true } stream_ids: { type: array, in: body, required: true, description: "Array of stream IDs to connect" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Inputs # ────────────────────────────────────────────── list_inputs: method: GET path: /system/inputs access: read description: > List all configured inputs. Each input has a type (e.g. org.graylog2.inputs.syslog.udp.SyslogUDPInput), title, global flag, and configuration. response: result_path: "$.inputs" get_input: method: GET path: /system/inputs/{inputId} access: read description: "Get a single input by ID." params: inputId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_input: method: POST path: /system/inputs access: admin description: > Create a new input. Use list_input_types to see available types and their configuration fields. Set global=true for cluster-wide inputs, false for node-local. params: title: { type: string, in: body, required: true } type: { type: string, in: body, required: true, description: "Input type class name" } global: { type: boolean, in: body, required: true } configuration: { type: object, in: body, required: true, description: "Type-specific configuration" } node: { type: string, in: body, description: "Node ID for non-global inputs" } response: result_path: "$" pagination: none update_input: method: PUT path: /system/inputs/{inputId} access: admin description: "Update an input's title, configuration, or global flag." params: inputId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } type: { type: string, in: body, required: true } global: { type: boolean, in: body, required: true } configuration: { type: object, in: body, required: true } node: { type: string, in: body } response: result_path: "$" pagination: none delete_input: method: DELETE path: /system/inputs/{inputId} access: dangerous description: "Delete an input. This stops ingestion from this source." params: inputId: { type: string, in: path, required: true } pagination: none launch_input: method: POST path: /system/inputs/{inputId}/launch access: admin description: "Start/launch an existing input." params: inputId: { type: string, in: path, required: true } pagination: none stop_input: method: POST path: /system/inputs/{inputId}/stop access: admin description: "Stop a running input." params: inputId: { type: string, in: path, required: true } pagination: none list_input_types: method: GET path: /system/inputs/types access: read description: "List all available input types with their configuration schemas." response: result_path: "$.types" pagination: none get_input_type: method: GET path: /system/inputs/types/{inputType} access: read description: "Get details for a specific input type including all configuration fields." params: inputType: { type: string, in: path, required: true } response: result_path: "$" pagination: none list_input_states: method: GET path: /system/inputstates access: read description: "List input states (running, stopped, failed) for all inputs on this node." response: result_path: "$.states" # ────────────────────────────────────────────── # Input Extractors # ────────────────────────────────────────────── list_extractors: method: GET path: /system/inputs/{inputId}/extractors access: read description: "List all extractors configured on an input." params: inputId: { type: string, in: path, required: true } response: result_path: "$.extractors" get_extractor: method: GET path: /system/inputs/{inputId}/extractors/{extractorId} access: read description: "Get a single extractor." params: inputId: { type: string, in: path, required: true } extractorId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_extractor: method: POST path: /system/inputs/{inputId}/extractors access: write description: > Create an extractor on an input. Types: regex, substring, split_and_index, copy_input, grok, json, lookup_table. params: inputId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } type: { type: string, in: body, required: true, description: "regex, substring, split_and_index, copy_input, grok, json, lookup_table" } source_field: { type: string, in: body, required: true } target_field: { type: string, in: body, required: true } configuration: { type: object, in: body, required: true, description: "Type-specific configuration" } cursor_strategy: { type: string, in: body, required: true, description: "copy or cut" } condition_type: { type: string, in: body, default: "none", description: "none, string, regex" } condition_value: { type: string, in: body } converters: { type: array, in: body, description: "Array of converter configs" } cut_or_copy: { type: string, in: body } order: { type: integer, in: body, default: 0 } response: result_path: "$" pagination: none update_extractor: method: PUT path: /system/inputs/{inputId}/extractors/{extractorId} access: write description: "Update an extractor." params: inputId: { type: string, in: path, required: true } extractorId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } type: { type: string, in: body, required: true } source_field: { type: string, in: body, required: true } target_field: { type: string, in: body, required: true } configuration: { type: object, in: body, required: true } cursor_strategy: { type: string, in: body, required: true } condition_type: { type: string, in: body } condition_value: { type: string, in: body } converters: { type: array, in: body } order: { type: integer, in: body } response: result_path: "$" pagination: none delete_extractor: method: DELETE path: /system/inputs/{inputId}/extractors/{extractorId} access: dangerous description: "Delete an extractor from an input." params: inputId: { type: string, in: path, required: true } extractorId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Input Static Fields # ────────────────────────────────────────────── create_static_field: method: POST path: /system/inputs/{inputId}/staticfields access: write description: "Add a static field to all messages from this input." params: inputId: { type: string, in: path, required: true } key: { type: string, in: body, required: true } value: { type: string, in: body, required: true } pagination: none delete_static_field: method: DELETE path: /system/inputs/{inputId}/staticfields/{key} access: write description: "Remove a static field from an input." params: inputId: { type: string, in: path, required: true } key: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Events & Event Definitions # ────────────────────────────────────────────── list_event_definitions: method: GET path: /events/definitions access: read description: "List all event definitions (alert conditions in Graylog 3.1+)." params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } query: { type: string, in: query, description: "Filter by title" } response: result_path: "$.event_definitions" get_event_definition: method: GET path: /events/definitions/{definitionId} access: read description: "Get a single event definition." params: definitionId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_event_definition: method: POST path: /events/definitions access: write description: > Create an event definition. config contains the condition type and parameters. notification_settings controls which notifications fire. params: title: { type: string, in: body, required: true } description: { type: string, in: body } priority: { type: integer, in: body, default: 2, description: "1=low, 2=normal, 3=high" } config: { type: object, in: body, required: true, description: "Event condition configuration" } field_spec: { type: object, in: body } key_spec: { type: array, in: body } notification_settings: { type: object, in: body } notifications: { type: array, in: body, description: "Array of notification references" } alert: { type: boolean, in: body, default: false } response: result_path: "$" pagination: none update_event_definition: method: PUT path: /events/definitions/{definitionId} access: write description: "Update an event definition." params: definitionId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } priority: { type: integer, in: body } config: { type: object, in: body, required: true } field_spec: { type: object, in: body } key_spec: { type: array, in: body } notification_settings: { type: object, in: body } notifications: { type: array, in: body } alert: { type: boolean, in: body } response: result_path: "$" pagination: none delete_event_definition: method: DELETE path: /events/definitions/{definitionId} access: dangerous description: "Delete an event definition." params: definitionId: { type: string, in: path, required: true } pagination: none execute_event_definition: method: POST path: /events/definitions/{definitionId}/execute access: write description: "Manually trigger an event definition to check for events now." params: definitionId: { type: string, in: path, required: true } pagination: none validate_event_definition: method: POST path: /events/definitions/validate access: read description: "Validate an event definition without creating it." params: title: { type: string, in: body, required: true } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none search_events: method: POST path: /events/search access: read description: > Search for triggered events. Supports filtering by event definition, timerange, and query. Returns events with timestamp, priority, fields, and source. params: query: { type: string, in: body } filter: { type: object, in: body } timerange: { type: object, in: body, description: "Object with type (absolute/relative/keyword) and range params" } page: { type: integer, in: body, default: 1 } per_page: { type: integer, in: body, default: 50 } sort_by: { type: string, in: body, default: "timestamp" } sort_direction: { type: string, in: body, default: "desc" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Event Notifications # ────────────────────────────────────────────── list_event_notifications: method: GET path: /events/notifications access: read description: "List all event notifications (email, HTTP, Slack, etc.)." params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } response: result_path: "$.notifications" get_event_notification: method: GET path: /events/notifications/{notificationId} access: read description: "Get a single event notification." params: notificationId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_event_notification: method: POST path: /events/notifications access: write description: "Create an event notification. Config varies by type (email, http, slack, etc.)." params: title: { type: string, in: body, required: true } description: { type: string, in: body } config: { type: object, in: body, required: true, description: "Notification type-specific configuration" } response: result_path: "$" pagination: none update_event_notification: method: PUT path: /events/notifications/{notificationId} access: write description: "Update an event notification." params: notificationId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none delete_event_notification: method: DELETE path: /events/notifications/{notificationId} access: dangerous description: "Delete an event notification." params: notificationId: { type: string, in: path, required: true } pagination: none test_event_notification: method: POST path: /events/notifications/{notificationId}/test access: write description: "Send a test notification to verify the notification configuration works." params: notificationId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Dashboards (Legacy) # ────────────────────────────────────────────── list_dashboards: method: GET path: /dashboards access: read description: "List all dashboards." response: result_path: "$.dashboards" list_views: method: GET path: /views access: read description: > List saved Views. In Graylog 6 a "dashboard" and a "saved search" are both Views, distinguished by their type field (DASHBOARD / SEARCH). list_dashboards and list_saved_searches are the type-filtered convenience listings; this one returns both. Results are paginated under $.elements. params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } query: { type: string, in: query, description: "Search string over title/summary" } sort: { type: string, in: query, description: "Sort field, e.g. title" } order: { type: string, in: query, description: "asc or desc" } response: result_path: "$.views" get_view: method: GET path: /views/{viewId} access: read description: > Get a single View by ID -- this is how you fetch ONE dashboard or ONE saved search. There is no /dashboards/{id} or /search/saved/{id} in Graylog 6: those paths fall through to the web UI and return HTML. Take the id from list_dashboards / list_saved_searches / list_views. params: viewId: { type: string, in: path, required: true, description: "View ID (the `id` field, not `search_id`)" } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Users # ────────────────────────────────────────────── list_users: method: GET path: /users access: admin description: "List all users including their roles, permissions, and preferences." response: result_path: "$.users" get_user: method: GET path: /users/{username} access: read description: "Get a user by username." params: username: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_user: method: POST path: /users access: admin description: "Create a new user." params: username: { type: string, in: body, required: true } password: { type: string, in: body, required: true } email: { type: string, in: body, required: true } full_name: { type: string, in: body, required: true } permissions: { type: array, in: body } roles: { type: array, in: body, description: "Array of role names" } timezone: { type: string, in: body } session_timeout_ms: { type: integer, in: body } response: result_path: "$" pagination: none update_user: method: PUT path: /users/{username} access: admin description: "Update a user's details." params: username: { type: string, in: path, required: true } email: { type: string, in: body } full_name: { type: string, in: body } permissions: { type: array, in: body } roles: { type: array, in: body } timezone: { type: string, in: body } session_timeout_ms: { type: integer, in: body } response: result_path: "$" pagination: none delete_user: method: DELETE path: /users/{username} access: dangerous description: "Delete a user. Built-in admin user cannot be deleted." params: username: { type: string, in: path, required: true } pagination: none change_user_password: method: PUT path: /users/{username}/password access: admin description: "Change a user's password." params: username: { type: string, in: path, required: true } old_password: { type: string, in: body, required: true } password: { type: string, in: body, required: true } pagination: none update_user_permissions: method: PUT path: /users/{username}/permissions access: admin description: "Update a user's permissions." params: username: { type: string, in: path, required: true } permissions: { type: array, in: body, required: true } pagination: none update_user_preferences: method: PUT path: /users/{username}/preferences access: write description: "Update a user's UI preferences." params: username: { type: string, in: path, required: true } preferences: { type: object, in: body, required: true } pagination: none # ────────────────────────────────────────────── # User Tokens # ────────────────────────────────────────────── list_user_tokens: method: GET path: /users/{userId}/tokens access: admin description: "List all access tokens for a user. Token values are NOT returned -- only name, id, and last_access." params: userId: { type: string, in: path, required: true, description: "User ID (24-char hex from list_users[].id) -- NOT the username. Passing a username returns HTTP 500 'hexString has 24 characters'; the config-defined admin (id local:admin) has no token store." } response: result_path: "$.tokens" create_user_token: method: POST path: /users/{userId}/tokens/{tokenName} access: admin description: > Create a new access token for a user. The token value is returned ONLY in this response. Store it immediately. Used for API auth: username=token_value, password="token". params: userId: { type: string, in: path, required: true, description: "User ID (24-char hex from list_users[].id) -- NOT the username. Passing a username returns HTTP 500 'hexString has 24 characters'; the config-defined admin (id local:admin) has no token store." } tokenName: { type: string, in: path, required: true } response: result_path: "$" pagination: none delete_user_token: method: DELETE path: /users/{userId}/tokens/{tokenId} access: admin description: "Delete/revoke an access token." params: userId: { type: string, in: path, required: true, description: "User ID (24-char hex from list_users[].id) -- NOT the username. Passing a username returns HTTP 500 'hexString has 24 characters'; the config-defined admin (id local:admin) has no token store." } tokenId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Roles # ────────────────────────────────────────────── list_roles: method: GET path: /roles access: admin description: "List all roles with their permissions." response: result_path: "$.roles" get_role: method: GET path: /roles/{rolename} access: admin description: "Get a role by name." params: rolename: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_role: method: POST path: /roles access: admin description: "Create a new role." params: name: { type: string, in: body, required: true } description: { type: string, in: body } permissions: { type: array, in: body, required: true, description: "Array of permission strings" } read_only: { type: boolean, in: body, default: false } response: result_path: "$" pagination: none update_role: method: PUT path: /roles/{rolename} access: admin description: "Update a role." params: rolename: { type: string, in: path, required: true } name: { type: string, in: body, required: true } description: { type: string, in: body } permissions: { type: array, in: body, required: true } read_only: { type: boolean, in: body } response: result_path: "$" pagination: none delete_role: method: DELETE path: /roles/{rolename} access: dangerous description: "Delete a role. Built-in roles (Admin, Reader) cannot be deleted." params: rolename: { type: string, in: path, required: true } pagination: none list_role_members: method: GET path: /roles/{rolename}/members access: admin description: "List all users assigned to a role." params: rolename: { type: string, in: path, required: true } response: result_path: "$.users" add_role_member: method: PUT path: /roles/{rolename}/members/{username} access: admin description: "Add a user to a role." params: rolename: { type: string, in: path, required: true } username: { type: string, in: path, required: true } pagination: none remove_role_member: method: DELETE path: /roles/{rolename}/members/{username} access: admin description: "Remove a user from a role." params: rolename: { type: string, in: path, required: true } username: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Index Sets # ────────────────────────────────────────────── list_index_sets: method: GET path: /system/indices/index_sets access: read description: > List all index sets. Each index set defines retention, rotation, and sharding for a group of indices. Every stream maps to one index set. params: skip: { type: integer, in: query, default: 0 } limit: { type: integer, in: query, default: 50 } stats: { type: boolean, in: query, default: false, description: "Include index statistics" } response: result_path: "$.index_sets" get_index_set: method: GET path: /system/indices/index_sets/{indexSetId} access: read description: "Get a single index set." params: indexSetId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_index_set: method: POST path: /system/indices/index_sets access: admin description: "Create a new index set." params: title: { type: string, in: body, required: true } description: { type: string, in: body } index_prefix: { type: string, in: body, required: true, description: "Index name prefix, must be unique" } shards: { type: integer, in: body, default: 4 } replicas: { type: integer, in: body, default: 0 } rotation_strategy_class: { type: string, in: body, required: true } rotation_strategy: { type: object, in: body, required: true } retention_strategy_class: { type: string, in: body, required: true } retention_strategy: { type: object, in: body, required: true } index_analyzer: { type: string, in: body, default: "standard" } index_optimization_max_num_segments: { type: integer, in: body, default: 1 } index_optimization_disabled: { type: boolean, in: body, default: false } field_type_refresh_interval: { type: integer, in: body, default: 5000 } writable: { type: boolean, in: body, default: true } default: { type: boolean, in: body, default: false } response: result_path: "$" pagination: none update_index_set: method: PUT path: /system/indices/index_sets/{indexSetId} access: admin description: "Update an index set." params: indexSetId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } index_prefix: { type: string, in: body, required: true } shards: { type: integer, in: body } replicas: { type: integer, in: body } rotation_strategy_class: { type: string, in: body, required: true } rotation_strategy: { type: object, in: body, required: true } retention_strategy_class: { type: string, in: body, required: true } retention_strategy: { type: object, in: body, required: true } index_analyzer: { type: string, in: body } index_optimization_max_num_segments: { type: integer, in: body } index_optimization_disabled: { type: boolean, in: body } field_type_refresh_interval: { type: integer, in: body } writable: { type: boolean, in: body } default: { type: boolean, in: body } response: result_path: "$" pagination: none delete_index_set: method: DELETE path: /system/indices/index_sets/{indexSetId} access: dangerous description: "Delete an index set. Optionally delete all associated indices." params: indexSetId: { type: string, in: path, required: true } delete_indices: { type: boolean, in: query, default: false } pagination: none set_default_index_set: method: PUT path: /system/indices/index_sets/{indexSetId}/default access: admin description: "Set an index set as the default for new streams." params: indexSetId: { type: string, in: path, required: true } response: result_path: "$" pagination: none get_index_set_stats: method: GET path: /system/indices/index_sets/stats access: read description: "Get statistics for all index sets (document count, size, segments)." response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Indices # ────────────────────────────────────────────── list_indices: method: GET path: /system/indexer/indices access: read description: "List all Elasticsearch/OpenSearch indices managed by Graylog." response: result_path: "$" list_open_indices: method: GET path: /system/indexer/indices/open access: read description: "List all open (writable/searchable) indices." response: result_path: "$" list_closed_indices: method: GET path: /system/indexer/indices/closed access: read description: "List all closed indices." response: result_path: "$" get_index: method: GET path: /system/indexer/indices/{index} access: read description: "Get details for a specific index." params: index: { type: string, in: path, required: true } response: result_path: "$" pagination: none close_index: method: POST path: /system/indexer/indices/{index}/close access: admin description: "Close an index. Closed indices cannot be searched but retain data." params: index: { type: string, in: path, required: true } pagination: none reopen_index: method: POST path: /system/indexer/indices/{index}/reopen access: admin description: "Reopen a closed index." params: index: { type: string, in: path, required: true } pagination: none delete_index: method: DELETE path: /system/indexer/indices/{index} access: dangerous description: "Delete an index. This permanently removes all data in the index." params: index: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Index Ranges # ────────────────────────────────────────────── list_index_ranges: method: GET path: /system/indices/ranges access: read description: "List all index ranges (time boundaries for each index)." response: result_path: "$.ranges" rebuild_all_index_ranges: method: POST path: /system/indices/ranges/rebuild access: admin description: "Trigger a rebuild of all index ranges. Long-running operation." rebuild_index_range: method: POST path: /system/indices/ranges/{index}/rebuild access: admin description: "Rebuild index range for a specific index." params: index: { type: string, in: path, required: true } # ────────────────────────────────────────────── # Deflector (Active Write Index) # ────────────────────────────────────────────── get_deflector: method: GET path: /system/deflector access: read description: "Get the current deflector (active write index) status." response: result_path: "$" pagination: none cycle_deflector: method: POST path: /system/deflector/cycle access: admin description: "Cycle the deflector -- close the current write index and create a new one." get_deflector_for_index_set: method: GET path: /system/deflector/{indexSetId} access: read description: "Get the deflector for a specific index set." params: indexSetId: { type: string, in: path, required: true } response: result_path: "$" pagination: none cycle_deflector_for_index_set: method: POST path: /system/deflector/{indexSetId}/cycle access: admin description: "Cycle the deflector for a specific index set." params: indexSetId: { type: string, in: path, required: true } # ────────────────────────────────────────────── # Indexer Health & Failures # ────────────────────────────────────────────── get_indexer_overview: method: GET path: /system/indexer/overview access: read description: "Get indexing overview with processing rates and counts." response: result_path: "$" pagination: none get_indexer_cluster_health: method: GET path: /system/indexer/cluster/health access: read description: "Get Elasticsearch/OpenSearch cluster health (green/yellow/red)." response: result_path: "$" pagination: none get_indexer_cluster_name: method: GET path: /system/indexer/cluster/name access: read description: "Get the Elasticsearch/OpenSearch cluster name." response: result_path: "$" pagination: none list_indexer_failures: method: GET path: /system/indexer/failures access: read description: "List indexer failures (messages that could not be indexed)." params: limit: { type: integer, in: query, default: 50 } offset: { type: integer, in: query, default: 0 } response: result_path: "$" count_indexer_failures: method: GET path: /system/indexer/failures/count access: read description: > Total count of indexer failures since a given date. "since" is REQUIRED -- an ISO8601 timestamp; omitting it fails with HTTP 400 "FailuresResource.count.arg0 must not be empty". params: since: { type: string, in: query, required: true, description: "ISO8601 date, e.g. '2026-08-01T00:00:00.000Z'. Counts failures at or after this point." } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Grok Patterns # ────────────────────────────────────────────── list_grok_patterns: method: GET path: /system/grok access: read description: "List all grok patterns used for message extraction." response: result_path: "$.patterns" get_grok_pattern: method: GET path: /system/grok/{patternId} access: read description: "Get a single grok pattern." params: patternId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_grok_pattern: method: POST path: /system/grok access: write description: "Create a new grok pattern." params: name: { type: string, in: body, required: true } pattern: { type: string, in: body, required: true } content_pack: { type: string, in: body } response: result_path: "$" pagination: none update_grok_pattern: method: PUT path: /system/grok/{patternId} access: write description: "Update a grok pattern." params: patternId: { type: string, in: path, required: true } name: { type: string, in: body, required: true } pattern: { type: string, in: body, required: true } response: result_path: "$" pagination: none delete_grok_pattern: method: DELETE path: /system/grok/{patternId} access: dangerous description: "Delete a grok pattern." params: patternId: { type: string, in: path, required: true } pagination: none test_grok_pattern: method: POST path: /system/grok/test access: read description: "Test a grok pattern against a sample string." params: pattern: { type: string, in: body, required: true } string: { type: string, in: body, required: true } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Lookup Tables # ────────────────────────────────────────────── list_lookup_tables: method: GET path: /system/lookup/tables access: read description: "List all lookup tables with their data adapter and cache references." params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } sort: { type: string, in: query } order: { type: string, in: query } query: { type: string, in: query } resolve: { type: boolean, in: query, default: false, description: "Resolve adapter and cache references" } response: result_path: "$.lookup_tables" get_lookup_table: method: GET path: /system/lookup/tables/{idOrName} access: read description: "Get a lookup table by ID or name." params: idOrName: { type: string, in: path, required: true } resolve: { type: boolean, in: query, default: false } response: result_path: "$" pagination: none create_lookup_table: method: POST path: /system/lookup/tables access: write description: "Create a lookup table." params: title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true, description: "Unique identifier (slug)" } cache_id: { type: string, in: body, required: true } data_adapter_id: { type: string, in: body, required: true } default_single_value: { type: string, in: body } default_single_value_type: { type: string, in: body, default: "NULL" } default_multi_value: { type: string, in: body } default_multi_value_type: { type: string, in: body, default: "NULL" } response: result_path: "$" pagination: none update_lookup_table: method: PUT path: /system/lookup/tables/{idOrName} access: write description: "Update a lookup table." params: idOrName: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true } cache_id: { type: string, in: body, required: true } data_adapter_id: { type: string, in: body, required: true } default_single_value: { type: string, in: body } default_single_value_type: { type: string, in: body } default_multi_value: { type: string, in: body } default_multi_value_type: { type: string, in: body } response: result_path: "$" pagination: none delete_lookup_table: method: DELETE path: /system/lookup/tables/{idOrName} access: dangerous description: "Delete a lookup table." params: idOrName: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Lookup Data Adapters # ────────────────────────────────────────────── list_lookup_adapters: method: GET path: /system/lookup/adapters access: read description: "List all lookup data adapters." params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } sort: { type: string, in: query } order: { type: string, in: query } query: { type: string, in: query } response: result_path: "$.data_adapters" get_lookup_adapter: method: GET path: /system/lookup/adapters/{idOrName} access: read description: "Get a lookup data adapter by ID or name." params: idOrName: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_lookup_adapter: method: POST path: /system/lookup/adapters access: write description: "Create a lookup data adapter." params: title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none update_lookup_adapter: method: PUT path: /system/lookup/adapters/{idOrName} access: write description: "Update a lookup data adapter." params: idOrName: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none delete_lookup_adapter: method: DELETE path: /system/lookup/adapters/{idOrName} access: dangerous description: "Delete a lookup data adapter." params: idOrName: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Lookup Caches # ────────────────────────────────────────────── list_lookup_caches: method: GET path: /system/lookup/caches access: read description: "List all lookup caches." params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } sort: { type: string, in: query } order: { type: string, in: query } query: { type: string, in: query } response: result_path: "$.caches" get_lookup_cache: method: GET path: /system/lookup/caches/{idOrName} access: read description: "Get a lookup cache by ID or name." params: idOrName: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_lookup_cache: method: POST path: /system/lookup/caches access: write description: "Create a lookup cache." params: title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none update_lookup_cache: method: PUT path: /system/lookup/caches/{idOrName} access: write description: "Update a lookup cache." params: idOrName: { type: string, in: path, required: true } title: { type: string, in: body, required: true } description: { type: string, in: body } name: { type: string, in: body, required: true } config: { type: object, in: body, required: true } response: result_path: "$" pagination: none delete_lookup_cache: method: DELETE path: /system/lookup/caches/{idOrName} access: dangerous description: "Delete a lookup cache." params: idOrName: { type: string, in: path, required: true } pagination: none list_lookup_adapter_types: method: GET path: /system/lookup/types/adapters access: read description: "List available data adapter types." response: result_path: "$" pagination: none list_lookup_cache_types: method: GET path: /system/lookup/types/caches access: read description: "List available cache types." response: result_path: "$" pagination: none # ────────────────────────────────────────────── # Outputs # ────────────────────────────────────────────── list_outputs: method: GET path: /system/outputs access: read description: "List all configured outputs." response: result_path: "$.outputs" get_output: method: GET path: /system/outputs/{outputId} access: read description: "Get a single output." params: outputId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_output: method: POST path: /system/outputs access: admin description: "Create a new output. Use list_output_types to see available types." params: title: { type: string, in: body, required: true } type: { type: string, in: body, required: true } configuration: { type: object, in: body, required: true } response: result_path: "$" pagination: none update_output: method: PUT path: /system/outputs/{outputId} access: admin description: "Update an output." params: outputId: { type: string, in: path, required: true } title: { type: string, in: body, required: true } type: { type: string, in: body, required: true } configuration: { type: object, in: body, required: true } response: result_path: "$" pagination: none delete_output: method: DELETE path: /system/outputs/{outputId} access: dangerous description: "Delete an output." params: outputId: { type: string, in: path, required: true } pagination: none list_output_types: method: GET path: /system/outputs/available access: read description: "List available output types with their configuration schemas." response: result_path: "$.types" pagination: none # ────────────────────────────────────────────── # Content Packs # ────────────────────────────────────────────── list_content_packs: method: GET path: /system/content_packs access: read description: "List all content packs (bundles of inputs, streams, dashboards, etc.)." response: result_path: "$.content_packs" get_content_pack: method: GET path: /system/content_packs/{contentPackId} access: read description: "Get a content pack with all its revisions." params: contentPackId: { type: string, in: path, required: true } response: result_path: "$" pagination: none upload_content_pack: method: POST path: /system/content_packs access: admin description: "Upload/create a content pack." params: name: { type: string, in: body, required: true } summary: { type: string, in: body } description: { type: string, in: body } vendor: { type: string, in: body } url: { type: string, in: body } entities: { type: array, in: body, required: true } response: result_path: "$" pagination: none delete_content_pack: method: DELETE path: /system/content_packs/{contentPackId} access: dangerous description: "Delete a content pack." params: contentPackId: { type: string, in: path, required: true } pagination: none install_content_pack: method: POST path: /system/content_packs/{contentPackId}/{revision}/installations access: admin description: "Install a specific revision of a content pack." params: contentPackId: { type: string, in: path, required: true } revision: { type: integer, in: path, required: true } comment: { type: string, in: body } response: result_path: "$" pagination: none list_content_pack_installations: method: GET path: /system/content_packs/{contentPackId}/installations access: read description: "List all installations of a content pack." params: contentPackId: { type: string, in: path, required: true } response: result_path: "$.installations" uninstall_content_pack: method: DELETE path: /system/content_packs/{contentPackId}/installations/{installationId} access: admin description: "Uninstall a content pack installation." params: contentPackId: { type: string, in: path, required: true } installationId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Sidecars (Collector Management) # ────────────────────────────────────────────── list_sidecars: method: GET path: /sidecars access: read description: > List all registered sidecars. Sidecars are agents running on hosts that manage log collectors (Filebeat, Winlogbeat, NXLog). params: page: { type: integer, in: query, default: 1 } per_page: { type: integer, in: query, default: 50 } query: { type: string, in: query } sort: { type: string, in: query } order: { type: string, in: query } only_active: { type: boolean, in: query, default: false } response: result_path: "$.sidecars" get_sidecar: method: GET path: /sidecars/{sidecarId} access: read description: "Get a single sidecar by ID." params: sidecarId: { type: string, in: path, required: true } response: result_path: "$" pagination: none sidecar_administration: method: POST path: /sidecar/administration access: read description: > Paginated sidecar listing including collector compatibility -- the data behind the Sidecar Administration page. This is a POST despite being a read: the filter is sent as a body. A body is REQUIRED; an empty request fails with HTTP 400 "administration.arg0 must not be null". Body fields: page, per_page, query (search over node_name, e.g. "status:running"), filters (map, e.g. { "status": "1" }). params: page: { type: integer, in: body, default: 1, description: "1-based page number" } per_page: { type: integer, in: body, default: 50, description: "Page size" } query: { type: string, in: body, default: "", description: "Search query over node_name; status:running|failing|unknown is mapped server-side" } filters: { type: object, in: body, default: {}, description: "Field filters as a flat string map" } response: result_path: "$" pagination: none bulk_sidecar_action: method: PUT path: /sidecar/administration/action access: admin description: > Set collector actions (start / restart / stop) on sidecars in bulk. Returns HTTP 202 on success. Body: { action, collectors: [ { sidecar_id, collector_ids: [...] } ] } -- note the nested per-sidecar list; there is no flat sidecar_ids field. params: action: { type: string, in: body, required: true, description: "Action to apply, e.g. start, restart, stop" } collectors: { type: array, in: body, required: true, description: "[{ sidecar_id: string, collector_ids: [string] }]" } pagination: none # ────────────────────────────────────────────── # Sidecar Collectors # ────────────────────────────────────────────── list_sidecar_collectors: method: GET path: /sidecar/collectors access: read description: "List all collector backends (Filebeat, Winlogbeat, NXLog definitions)." response: result_path: "$.collectors" get_sidecar_collector: method: GET path: /sidecar/collectors/{collectorId} access: read description: "Get a collector backend definition." params: collectorId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_sidecar_collector: method: POST path: /sidecar/collectors access: admin description: "Create a new collector backend definition." params: name: { type: string, in: body, required: true } service_type: { type: string, in: body, required: true, description: "exec or svc" } node_operating_system: { type: string, in: body, required: true, description: "linux, windows, darwin" } executable_path: { type: string, in: body, required: true } execute_parameters: { type: string, in: body } validation_parameters: { type: string, in: body } default_template: { type: string, in: body } response: result_path: "$" pagination: none update_sidecar_collector: method: PUT path: /sidecar/collectors/{collectorId} access: admin description: "Update a collector backend definition." params: collectorId: { type: string, in: path, required: true } name: { type: string, in: body, required: true } service_type: { type: string, in: body, required: true } node_operating_system: { type: string, in: body, required: true } executable_path: { type: string, in: body, required: true } execute_parameters: { type: string, in: body } validation_parameters: { type: string, in: body } default_template: { type: string, in: body } response: result_path: "$" pagination: none delete_sidecar_collector: method: DELETE path: /sidecar/collectors/{collectorId} access: dangerous description: "Delete a collector backend definition." params: collectorId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Sidecar Configurations # ────────────────────────────────────────────── list_sidecar_configurations: method: GET path: /sidecar/configurations access: read description: "List all sidecar configurations (collector config templates)." response: result_path: "$.configurations" get_sidecar_configuration: method: GET path: /sidecar/configurations/{configId} access: read description: "Get a sidecar configuration." params: configId: { type: string, in: path, required: true } response: result_path: "$" pagination: none create_sidecar_configuration: method: POST path: /sidecar/configurations access: write description: "Create a sidecar configuration." params: collector_id: { type: string, in: body, required: true } title: { type: string, in: body, required: true } color: { type: string, in: body, default: "#FFFFFF" } template: { type: string, in: body, required: true, description: "Configuration template (e.g., Filebeat YAML)" } response: result_path: "$" pagination: none update_sidecar_configuration: method: PUT path: /sidecar/configurations/{configId} access: write description: "Update a sidecar configuration." params: configId: { type: string, in: path, required: true } collector_id: { type: string, in: body, required: true } title: { type: string, in: body, required: true } color: { type: string, in: body } template: { type: string, in: body, required: true } response: result_path: "$" pagination: none delete_sidecar_configuration: method: DELETE path: /sidecar/configurations/{configId} access: dangerous description: "Delete a sidecar configuration." params: configId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Blacklist Filters # ────────────────────────────────────────────── # ────────────────────────────────────────────── # System -- General Info # ────────────────────────────────────────────── get_system: method: GET path: /system access: read description: "Get system overview: node ID, hostname, version, lifecycle, LB status, timezone, OS info." response: result_path: "$" pagination: none get_system_stats: method: GET path: /system/stats access: read description: "Get node statistics (JVM memory, OS, process info)." response: result_path: "$" pagination: none get_throughput: method: GET path: /system/throughput access: read description: "Get current message throughput (messages per second)." response: result_path: "$" pagination: none list_message_fields: method: GET path: /system/fields access: read description: "List all known message field names across all indices." response: result_path: "$.fields" pagination: none list_system_permissions: method: GET path: /system/permissions access: admin description: "List all available system permission strings." response: result_path: "$.permissions" pagination: none # ────────────────────────────────────────────── # System -- Processing # ────────────────────────────────────────────── pause_processing: method: POST path: /system/processing/pause access: admin description: "Pause message processing on this node. Messages will queue in the journal." resume_processing: method: POST path: /system/processing/resume access: admin description: "Resume message processing on this node." get_message_processor_config: method: GET path: /system/messageprocessors/config access: admin description: "Get message processor ordering and configuration." response: result_path: "$" pagination: none update_message_processor_config: method: PUT path: /system/messageprocessors/config access: admin description: "Update message processor ordering." params: processor_order: { type: array, in: body, required: true, description: "Ordered array of processor class names" } disabled_processors: { type: array, in: body } response: result_path: "$" pagination: none # ────────────────────────────────────────────── # System -- Journal # ────────────────────────────────────────────── get_journal: method: GET path: /system/journal access: read description: "Get journal (disk buffer) information: size, segments, uncommitted entries." response: result_path: "$" pagination: none # ────────────────────────────────────────────── # System -- Load Balancer # ────────────────────────────────────────────── get_lb_status: method: GET path: /system/lbstatus access: read description: > Load-balancer status of this node as PLAIN TEXT, not JSON: "ALIVE" (HTTP 200), "DEAD" (HTTP 503) or "THROTTLED" (HTTP 429). The endpoint produces text/plain only -- leave the Accept parameter at its default; application/json returns HTTP 406. params: Accept: { type: string, in: header, default: "text/plain", description: "Response media type -- the endpoint only produces text/plain" } response: result_path: "$" pagination: none override_lb_status: method: PUT path: /system/lbstatus/override/{status} access: admin description: "Override the load balancer status. status must be ALIVE or DEAD." params: status: { type: string, in: path, required: true, description: "ALIVE or DEAD" } pagination: none # ────────────────────────────────────────────── # System -- Metrics # ────────────────────────────────────────────── list_metrics: method: GET path: /system/metrics access: read description: "List all internal metrics (JVM, processing, indexing, etc.)." response: result_path: "$.metrics" get_metric: method: GET path: /system/metrics/{metricName} access: read description: "Get a specific metric by its full name." params: metricName: { type: string, in: path, required: true } response: result_path: "$" pagination: none list_metric_names: method: GET path: /system/metrics/names access: read description: "List all metric names." response: result_path: "$.names" pagination: none get_metrics_by_namespace: method: GET path: /system/metrics/namespace/{namespace} access: read description: "Get all metrics in a namespace (e.g. org.graylog2.throughput)." params: namespace: { type: string, in: path, required: true } response: result_path: "$.metrics" get_multiple_metrics: method: POST path: /system/metrics/multiple access: read description: "Get multiple metrics at once by their names." params: metrics: { type: array, in: body, required: true, description: "Array of metric name strings" } response: result_path: "$.metrics" pagination: none # ────────────────────────────────────────────── # System -- Notifications # ────────────────────────────────────────────── list_system_notifications: method: GET path: /system/notifications access: read description: "List system notifications (warnings about configuration issues, index problems, etc.)." response: result_path: "$.notifications" dismiss_system_notification: method: DELETE path: /system/notifications/{notificationType} access: admin description: "Dismiss a system notification." params: notificationType: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # System -- Loggers # ────────────────────────────────────────────── list_loggers: method: GET path: /system/loggers access: admin description: "List all internal loggers and their current log levels." response: result_path: "$.loggers" list_logger_subsystems: method: GET path: /system/loggers/subsystems access: admin description: "List logger subsystems (high-level components with adjustable levels)." response: result_path: "$.subsystems" set_logger_level: method: PUT path: /system/loggers/{loggerName}/level/{level} access: admin description: "Set the log level for a specific logger. Levels: TRACE, DEBUG, INFO, WARN, ERROR." params: loggerName: { type: string, in: path, required: true } level: { type: string, in: path, required: true, description: "TRACE, DEBUG, INFO, WARN, ERROR" } pagination: none set_subsystem_logger_level: method: PUT path: /system/loggers/subsystems/{subsystem}/level/{level} access: admin description: "Set the log level for a logger subsystem." params: subsystem: { type: string, in: path, required: true } level: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # System -- Jobs # ────────────────────────────────────────────── list_system_jobs: method: GET path: /system/jobs access: read description: "List all running and completed system jobs." response: result_path: "$.jobs" get_system_job: method: GET path: /system/jobs/{jobId} access: read description: "Get a system job by ID." params: jobId: { type: string, in: path, required: true } response: result_path: "$" pagination: none cancel_system_job: method: DELETE path: /system/jobs/{jobId} access: admin description: "Cancel a running system job." params: jobId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # System -- Plugins # ────────────────────────────────────────────── list_plugins: method: GET path: /system/plugins access: read description: "List all installed plugins with name, version, author, and URL." response: result_path: "$.plugins" pagination: none # ────────────────────────────────────────────── # System -- Sessions (Authentication) # ────────────────────────────────────────────── create_session: method: POST path: /system/sessions access: read description: > Create a session (login). Returns session_id and valid_until. For session-based auth: username=session_id, password="session". params: username: { type: string, in: body, required: true } password: { type: string, in: body, required: true } host: { type: string, in: body, default: "" } response: result_path: "$" pagination: none terminate_session: method: DELETE path: /system/sessions/{sessionId} access: write description: "Terminate a session (logout)." params: sessionId: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # System -- Cluster Config # ────────────────────────────────────────────── get_cluster_config: method: GET path: /system/cluster_config/{configClass} access: admin description: "Get a cluster-wide configuration by class name." params: configClass: { type: string, in: path, required: true } response: result_path: "$" pagination: none update_cluster_config: method: PUT path: /system/cluster_config/{configClass} access: admin description: "Update a cluster-wide configuration." params: configClass: { type: string, in: path, required: true } pagination: none delete_cluster_config: method: DELETE path: /system/cluster_config/{configClass} access: admin description: "Delete a cluster-wide configuration (reset to default)." params: configClass: { type: string, in: path, required: true } pagination: none # ────────────────────────────────────────────── # Cluster # ────────────────────────────────────────────── get_cluster: method: GET path: /cluster access: read description: "Get system overview of all cluster nodes." response: result_path: "$" pagination: none list_cluster_input_states: method: GET path: /cluster/inputstates access: read description: "Get input states across all cluster nodes." response: result_path: "$" pagination: none list_cluster_jobs: method: GET path: /cluster/jobs access: read description: "List running system jobs across all cluster nodes." response: result_path: "$" get_cluster_metrics: method: POST path: /cluster/metrics/multiple access: read description: "Get multiple metrics across all cluster nodes." params: metrics: { type: array, in: body, required: true } response: result_path: "$" pagination: none get_cluster_node_jvm: method: GET path: /cluster/{nodeId}/jvm access: read description: "Get JVM information for a specific cluster node." params: nodeId: { type: string, in: path, required: true } response: result_path: "$" pagination: none get_cluster_node_journal: method: GET path: /cluster/{nodeId}/journal access: read description: "Get journal info for a specific cluster node." params: nodeId: { type: string, in: path, required: true } response: result_path: "$" pagination: none get_cluster_node_thread_dump: method: GET path: /cluster/{nodeId}/threaddump access: admin description: "Get a thread dump for a specific cluster node." params: nodeId: { type: string, in: path, required: true } response: result_path: "$" pagination: none pause_cluster_node_processing: method: POST path: /cluster/{nodeId}/processing/pause access: admin description: "Pause message processing on a specific cluster node." params: nodeId: { type: string, in: path, required: true } resume_cluster_node_processing: method: POST path: /cluster/{nodeId}/processing/resume access: admin description: "Resume message processing on a specific cluster node." params: nodeId: { type: string, in: path, required: true } # ────────────────────────────────────────────── # Stream Alerts (Legacy) # ────────────────────────────────────────────── composites: aggregate_by_field: description: > Top-N values for a single field within a time window. Returns [{key, count}] sorted desc. Server-side replacement for the deprecated search_relative_terms / search_absolute_terms endpoints (removed in Graylog 6.0). time_range is one of: {type:"absolute", from, to} {type:"relative", range} {type:"keyword", keyword} params: query: { type: string, default: "*" } time_range: { type: object, required: true } field: { type: string, required: true } size: { type: integer, default: 20 } streams: { type: array, default: [] } timeout: 60s depends_on: [views_search_sync] code: | const body = { queries: [{ id: "q", query: { type: "elasticsearch", query_string: params.query || "*" }, timerange: params.time_range, filter: (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null, search_types: [{ id: "st", type: "pivot", row_groups: [{ type: "values", field: params.field, limit: params.size || 20 }], series: [{ type: "count", id: "c" }], rollup: false, }], }], parameters: [], }; const r = await api.views_search_sync(body); const rows = r.results?.q?.search_types?.st?.rows || []; return rows .filter(row => row.source === "leaf") .map(row => ({ key: row.key[0], count: row.values?.[0]?.value || 0 })); aggregate_pivot: description: > Multi-dimensional pivot (e.g. source x level). row_fields and column_fields are arrays of field names. Returns flat array of {row_keys, col_keys, count}. The canonical replacement for the Source x Level forensic matrix workflow. params: query: { type: string, default: "*" } time_range: { type: object, required: true } row_fields: { type: array, required: true } column_fields: { type: array, default: [] } row_limit: { type: integer, default: 50 } col_limit: { type: integer, default: 20 } streams: { type: array, default: [] } timeout: 60s depends_on: [views_search_sync] code: | const mkGroups = (fields, limit) => (fields || []).map(f => ({ type: "values", field: f, limit })); const body = { queries: [{ id: "q", query: { type: "elasticsearch", query_string: params.query || "*" }, timerange: params.time_range, filter: (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null, search_types: [{ id: "st", type: "pivot", row_groups: mkGroups(params.row_fields, params.row_limit || 50), column_groups: mkGroups(params.column_fields, params.col_limit || 20), series: [{ type: "count", id: "c" }], rollup: true, }], }], parameters: [], }; const r = await api.views_search_sync(body); const rows = r.results?.q?.search_types?.st?.rows || []; // Column dimensions do NOT live in row.key -- they are one entry per // row.values[] with source "col-leaf" and key [...colValues, seriesId]. // The single "row-leaf" entry is the rollup total across all columns. const hasCols = (params.column_fields || []).length > 0; const out = []; for (const row of rows) { if (row.source !== "leaf") continue; for (const v of (row.values || [])) { const isCol = v.source === "col-leaf"; if (hasCols !== isCol) continue; out.push({ row_keys: row.key || [], col_keys: (v.key || []).slice(0, -1), count: v.value || 0, }); } } return out; histogram_grouped: description: > Time-series histogram, optionally grouped by a field. interval is a Graylog timeunit: {quantity}{unit} with unit one of s m h d w M y -- e.g. 30s, 1m, 5m, 1h, 1d. That is the form to pass. ISO-8601 ("PT1M", "P1D") is also accepted and converted, because the underlying pivot rejects it with HTTP 400. Returns [{bucket_start, group_key|null, count}]. params: query: { type: string, default: "*" } time_range: { type: object, required: true } interval: { type: string, default: "1m" } group_by: { type: string, default: "" } size: { type: integer, default: 10 } streams: { type: array, default: [] } timeout: 60s depends_on: [views_search_sync] code: | // Graylog wants {quantity}{unit} with unit in [smhdwMy]; accept ISO-8601 too. const toTimeunit = (v) => { const s = String(v || "1m").trim(); const iso = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(s); if (!iso) return s; if (iso[1]) return iso[1] + "d"; if (iso[2]) return iso[2] + "h"; if (iso[3]) return iso[3] + "m"; if (iso[4]) return iso[4] + "s"; return "1m"; }; const rowGroups = [{ type: "time", field: "timestamp", interval: { type: "timeunit", timeunit: toTimeunit(params.interval) }, }]; if (params.group_by) { rowGroups.push({ type: "values", field: params.group_by, limit: params.size || 10 }); } const body = { queries: [{ id: "q", query: { type: "elasticsearch", query_string: params.query || "*" }, timerange: params.time_range, filter: (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null, search_types: [{ id: "st", type: "pivot", row_groups: rowGroups, series: [{ type: "count", id: "c" }], rollup: false, }], }], parameters: [], }; const r = await api.views_search_sync(body); const rows = r.results?.q?.search_types?.st?.rows || []; return rows .filter(row => row.source === "leaf") .map(row => ({ bucket_start: row.key[0], group_key: row.key[1] || null, count: row.values?.[0]?.value || 0, })); field_stats: description: "count, min, max, mean, cardinality for a numeric field. Replaces the removed _stats endpoints." params: query: { type: string, default: "*" } time_range: { type: object, required: true } field: { type: string, required: true } streams: { type: array, default: [] } timeout: 30s depends_on: [views_search_sync] code: | const series = ["count","min","max","avg","card"].map(fn => ({ type: fn, field: params.field, id: fn, })); const body = { queries: [{ id: "q", query: { type: "elasticsearch", query_string: params.query || "*" }, timerange: params.time_range, filter: (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null, search_types: [{ id: "st", type: "pivot", row_groups: [], series, rollup: true }], }], parameters: [], }; const r = await api.views_search_sync(body); const row = (r.results?.q?.search_types?.st?.rows || []).find(x => x.source === "leaf"); const out = {}; for (const v of (row?.values || [])) { out[v.key[v.key.length - 1]] = v.value; } return out; search_messages_compact: description: > Search returning only {ts, src, lvl, msg} per message -- ~80 bytes/msg instead of ~500. Offset/limit pagination (Graylog's Views API does not support search_after on the messages search-type). Pass the next_offset returned by the previous call to fetch the next page. params: query: { type: string, default: "*" } time_range: { type: object, required: true } limit: { type: integer, default: 200 } offset: { type: integer, default: 0 } streams: { type: array, default: [] } timeout: 60s depends_on: [views_search_sync] code: | const limit = params.limit || 200; const offset = params.offset || 0; const body = { queries: [{ id: "q", query: { type: "elasticsearch", query_string: params.query || "*" }, timerange: params.time_range, filter: (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null, search_types: [{ id: "st", type: "messages", limit: limit, offset: offset, fields: ["timestamp", "source", "level", "message"], sort: [{ field: "timestamp", order: "DESC" }], }], }], parameters: [], }; const r = await api.views_search_sync(body); const msgs = r.results?.q?.search_types?.st?.messages || []; const total = r.results?.q?.search_types?.st?.total_results; const nextOffset = offset + msgs.length; return { messages: msgs.map(m => ({ ts: m.message.timestamp, src: m.message.source, lvl: m.message.level, msg: m.message.message, })), next_offset: (msgs.length === limit && (total == null || nextOffset < total)) ? nextOffset : null, total: total, }; incident_window: description: > Forensic snapshot for a service-incident window. Returns {window, pivot_source_level, top_errors, baseline_pivot}. timestamp is ISO-8601, window_minutes default 6 (i.e. +/- 3 min). baseline_pivot covers the same-length window 1 hour earlier for comparison. params: timestamp: { type: string, required: true } window_minutes: { type: integer, default: 6 } streams: { type: array, default: [] } timeout: 90s depends_on: [views_search_sync] code: | const t = new Date(params.timestamp).getTime(); const half = (params.window_minutes || 6) * 30 * 1000; const tr = { type: "absolute", from: new Date(t - half).toISOString(), to: new Date(t + half).toISOString(), }; const baselineTr = { type: "absolute", from: new Date(t - half - 3600 * 1000).toISOString(), to: new Date(t + half - 3600 * 1000).toISOString(), }; const streamFilter = (params.streams && params.streams.length) ? { type: "or", filters: params.streams.map(s => ({ type: "stream", id: s })) } : null; const pivotBody = (timerange) => ({ queries: [{ id: "q", query: { type: "elasticsearch", query_string: "*" }, timerange, filter: streamFilter, search_types: [{ id: "st", type: "pivot", row_groups: [{ type: "values", field: "source", limit: 100 }], column_groups: [{ type: "values", field: "level", limit: 8 }], series: [{ type: "count", id: "c" }], rollup: true, }], }], parameters: [], }); const errorBody = (timerange) => ({ queries: [{ id: "q", query: { type: "elasticsearch", query_string: "level:<=4" }, timerange, filter: streamFilter, search_types: [{ id: "st", type: "pivot", row_groups: [{ type: "values", field: "message", limit: 10 }], series: [{ type: "count", id: "c" }], rollup: false, }], }], parameters: [], }); const flatPivot = (r) => (r.results?.q?.search_types?.st?.rows || []) .filter(row => row.source === "leaf") .map(row => ({ row_keys: row.key.slice(0, 1), col_keys: row.key.slice(1), count: row.values?.[0]?.value || 0, })); const flatTerms = (r) => (r.results?.q?.search_types?.st?.rows || []) .filter(row => row.source === "leaf") .map(row => ({ key: row.key[0], count: row.values?.[0]?.value || 0 })); const [matrixR, errorsR, baselineR] = await Promise.all([ api.views_search_sync(pivotBody(tr)), api.views_search_sync(errorBody(tr)), api.views_search_sync(pivotBody(baselineTr)), ]); return { window: tr, pivot_source_level: flatPivot(matrixR), top_errors: flatTerms(errorsR), baseline_pivot: flatPivot(baselineR), }; hints: search_relative: max_results: "10000 (Elasticsearch default result window)" query_syntax: "Lucene -- supports AND, OR, NOT, wildcards, ranges, field:value" stream_filter: "filter param uses 'streams:STREAM_ID' format" absolute_window: "for absolute timestamp windows on 6.x use timestamp:[\"FROM\" TO \"TO\"] in the query plus a generous range, OR use views_search_sync / aggregate_pivot with timerange.type=absolute" search_keyword: keyword_grammar: "open-ended Natty phrases ONLY: 'last 5 minutes', 'last 7 hours', 'yesterday', 'last week'. '7 hours ago' returns HTTP 400 -- use 'last 7 hours' instead, or views_search_sync for closed ranges." timezone_required: "the 'timezone' query param is mandatory server-side (@NotEmpty, undocumented in the API browser). Omitting it returns HTTP 400 'KeywordSearchResource.searchKeyword.arg2 must not be empty'. Defaults to UTC here; set e.g. 'Europe/Berlin' so 'yesterday' means the local day." export_search_relative: csv_only: "endpoint produces text/csv only -- the Accept header param must stay text/csv, anything else is HTTP 406" fields_required: "'fields' is mandatory (unlike search_relative) and sets the CSV column order" result_is_a_url: "response is streamed to the file broker; the tool returns a temporary download URL, not rows" stream_scoping: "use filter 'streams:STREAM_ID'. Graylog 6.1 has no 'streams' query param on the legacy export -- passing one is silently ignored and you get UNSCOPED results." empty_result: "a search matching nothing returns an empty body and no file -- the tool result then carries size_bytes 0 and url null instead of a download link" export_search_keyword: csv_only: "endpoint produces text/csv only -- the Accept header param must stay text/csv, anything else is HTTP 406" timezone_required: "same undocumented @NotEmpty 'timezone' param as search_keyword -- defaults to UTC here" stream_scoping: "use filter 'streams:STREAM_ID' -- see export_search_relative" views_search_messages: body_shape: "NOT the views_search_sync shape. Body is a MessagesRequest: { timerange, query_string: {type:'elasticsearch', query_string:'...'}, streams: [...], fields_in_order: [...], chunk_size, time_zone, limit }" streams_allowed: "unlike views_search_sync, a top-level 'streams' array IS valid here -- and unlike the legacy export endpoints it is actually honored. This is the only CSV export with working stream scoping." fields_in_order: "must be non-empty (@NotEmpty) and defines the CSV column order" limit_is_chunked: "limit is enforced at chunk boundaries only: chunk_size 1000 (default) + limit 3 returns 1000 rows. Set chunk_size to the number of rows you actually want." time_zone_default: "defaults to the API user's configured time zone, NOT UTC -- legacy exports render Z, this one renders e.g. +02:00 unless time_zone is passed" errors_in_body: "backend errors (e.g. an unknown stream ID) come back as an ElasticsearchException string INSIDE the CSV with HTTP 200 -- check that the first line is the expected header row" views_search_sync: time_range_shapes: | absolute: { type: "absolute", from: "2026-04-25T00:11:00.000Z", to: "2026-04-25T00:17:00.000Z" } relative: { type: "relative", range: 3600 } keyword: { type: "keyword", keyword: "last 5 minutes" } pivot_row_response: "search_types[].rows[].source is one of 'leaf' or 'non-leaf' -- always filter to leaf rows for flat output" stream_filter: "no top-level 'streams' field -- Graylog returns HTTP 400. Scope per query via query.filter: { type: 'or', filters: [{ type: 'stream', id: STREAM_ID }, ...] }" aggregate_pivot: replaces: "deprecated _terms / _histogram / _stats endpoints removed in Graylog 6.0" row_col_keys: "row.key holds ONLY the row dimensions. Column dimensions are NOT in row.key -- each column cell is a separate row.values[] entry with source 'col-leaf' and key [...colValues, seriesId]; the one entry with source 'row-leaf' is the rollup total across all columns." histogram_grouped: interval_format: "pass the Graylog timeunit {quantity}{unit}, unit in [s m h d w M y] -- 30s, 1m, 5m, 1h, 1d. ISO-8601 is accepted at the composite and converted; it is the raw pivot row_group that rejects PT1M with HTTP 400 'Time unit must be {quantity}{unit}'. So the 400 only appears when calling views_search_sync directly." interval_property: "the row_group interval object is { type: 'timeunit', timeunit: '1m' } -- the value key is `timeunit`, not `value` (HTTP 400 'Known properties include: timeunit, type')" create_stream: default_stream_id: "000000000000000000000001 -- cannot be deleted" starts_paused: "newly created streams are paused -- call resume_stream to activate" create_stream_rule: rule_types: "1=exact, 2=regex, 3=greater_than, 4=less_than, 5=field_presence, 6=contains, 7=always_match, 8=match_input" create_pipeline: source_syntax: "Graylog pipeline language, not Lucene. Validate with parse_pipeline first." create_input: requires_type: "call list_input_types first to get available types and their config schemas" create_extractor: types: "regex, substring, split_and_index, copy_input, grok, json, lookup_table" create_event_definition: priority_values: "1=low, 2=normal, 3=high" set_logger_level: levels: "TRACE, DEBUG, INFO, WARN, ERROR" override_lb_status: values: "ALIVE or DEAD" examples: - name: "Search recent errors" description: "Search for error-level messages in the last hour" code: | const results = await api.search_relative({ query: "level:3 OR level:ERROR", range: 3600, limit: 50, sort: "timestamp:desc" }); return results.messages; - name: "Source x Level matrix for an incident window" description: "Reproduce a 6-minute pivot around a service-incident timestamp (replaces removed _terms endpoint)." code: | return await api.aggregate_pivot({ query: "*", time_range: { type: "absolute", from: "2026-04-25T00:11:00.000Z", to: "2026-04-25T00:17:00.000Z", }, row_fields: ["source"], column_fields: ["level"], row_limit: 100, col_limit: 8, }); - name: "One-shot incident forensics" description: "Pivot + top errors + 1h-earlier baseline around a given timestamp." code: | return await api.incident_window({ timestamp: "2026-04-25T00:14:00Z", window_minutes: 6, }); - name: "Compact paginated message drill-down" description: "Offset-paged search returning only ts/src/lvl/msg per message." code: | const tr = { type: "relative", range: 3600 }; const p1 = await api.search_messages_compact({ query: "level:<=4", time_range: tr, limit: 100, }); const p2 = await api.search_messages_compact({ query: "level:<=4", time_range: tr, limit: 100, offset: p1.next_offset, }); return { page1: p1.messages.length, page2: p2.messages.length, total: p1.total }; - name: "Histogram with grouping" description: "30-second buckets of messages grouped by level for the last hour." code: | return await api.histogram_grouped({ query: "*", time_range: { type: "relative", range: 3600 }, interval: "30s", group_by: "level", size: 8, }); - name: "Create stream with rule" description: "Create a stream for nginx logs and add a source-matching rule" code: | const stream = await api.create_stream({ title: "Nginx Access Logs", description: "All messages from nginx servers", matching_type: "OR" }); await api.create_stream_rule({ streamId: stream.stream_id, field: "source", type: 2, value: "^nginx-.*", description: "Match nginx sources" }); await api.resume_stream({ streamId: stream.stream_id }); return stream; - name: "Pipeline with rule" description: "Create a pipeline rule and a pipeline that uses it" code: | const rule = await api.create_pipeline_rule({ title: "Extract HTTP status", source: 'rule "extract http status"\nwhen\n has_field("http_response_code")\nthen\n set_field("http_status_class", substring(to_string($message.http_response_code), 0, 1) + "xx");\nend' }); const pipeline = await api.create_pipeline({ title: "HTTP Enrichment", source: 'pipeline "HTTP Enrichment"\nstage 0 match either\nrule "extract http status"\nend' }); return { rule, pipeline };