feat: move index management out of /srch/, add storage size reporting (#8169)

* feat: move index management endpoints out of /srch/, add storage size reporting

- Mount management_service() at /api/indexer (authenticated)
- Add management_service() OSS stub in indexer_oss.rs
- Update OpenAPI: /indexer/delete/{idx_name} and /indexer/storage
- Show disk + S3 storage sizes in IndexerMemorySettings UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add index storage section with refresh button

Move storage sizes into a dedicated "Index storage" section with a
refresh button to reload sizes after clearing an index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add indexer status endpoint with liveness detection and improve settings UI

Add GET /indexer/status endpoint that combines lock-based liveness
detection with storage sizes. Frontend now shows running/stopped
indicators with last-active timestamps for each indexer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update ee ref

* fix

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-03 16:46:10 +00:00
committed by GitHub
parent 7b6f1deeb1
commit ee01acd9a6
5 changed files with 232 additions and 31 deletions
+1 -1
View File
@@ -1 +1 @@
8ffae1f43b31dc8136714fa612d22b6301773e27
9b3339730eb4bb0b564c7c56ac546f33fb3d8905
+93 -3
View File
@@ -16973,9 +16973,9 @@ paths:
description: count of log lines that matched the query per hostname
type: object
/srch/index/delete/{idx_name}:
/indexer/delete/{idx_name}:
delete:
summary: Restart container and delete the index to recreate it.
summary: Clear an index and restart the indexer.
operationId: clearIndex
tags:
- indexSearch
@@ -16990,12 +16990,102 @@ paths:
- ServiceLogIndex
responses:
"200":
description: idx to be deleted and container restarting
description: idx to be deleted and indexer restarting
content:
text/plain:
schema:
type: string
/indexer/storage:
get:
summary: Get index storage sizes (disk and S3).
operationId: getIndexStorageSizes
tags:
- indexSearch
responses:
"200":
description: storage sizes for each index
content:
application/json:
schema:
type: object
properties:
job_index:
type: object
properties:
disk_size_bytes:
type: integer
nullable: true
s3_size_bytes:
type: integer
nullable: true
service_log_index:
type: object
properties:
disk_size_bytes:
type: integer
nullable: true
s3_size_bytes:
type: integer
nullable: true
/indexer/status:
get:
summary: Get indexer status including liveness and storage sizes.
operationId: getIndexerStatus
tags:
- indexSearch
responses:
"200":
description: indexer status for each index
content:
application/json:
schema:
type: object
properties:
job_indexer:
type: object
properties:
is_alive:
type: boolean
last_locked_at:
type: string
format: date-time
nullable: true
owner:
type: string
nullable: true
storage:
type: object
properties:
disk_size_bytes:
type: integer
nullable: true
s3_size_bytes:
type: integer
nullable: true
log_indexer:
type: object
properties:
is_alive:
type: boolean
last_locked_at:
type: string
format: date-time
nullable: true
owner:
type: string
nullable: true
storage:
type: object
properties:
disk_size_bytes:
type: integer
nullable: true
s3_size_bytes:
type: integer
nullable: true
/w/{workspace}/assets/list:
get:
summary: List all assets in the workspace with cursor pagination
+5
View File
@@ -14,3 +14,8 @@ pub fn workspaced_service() -> Router {
pub fn global_service() -> Router {
Router::new()
}
#[cfg(not(feature = "private"))]
pub fn management_service() -> Router {
Router::new()
}
+10 -7
View File
@@ -58,10 +58,7 @@ use windmill_common::db::UserDB;
use windmill_common::worker::CLOUD_HOSTED;
#[allow(unused_imports)]
pub(crate) use windmill_common::BASE_URL;
use windmill_common::{
utils::GIT_VERSION,
INSTANCE_NAME,
};
use windmill_common::{utils::GIT_VERSION, INSTANCE_NAME};
use crate::scim_oss::has_scim_token;
use windmill_common::error::AppError;
@@ -550,6 +547,7 @@ pub async fn run_server(
.nest("/embeddings", embeddings::global_service())
.nest("/ai", ai::global_service())
.nest("/inkeep", inkeep_oss::global_service())
.nest("/indexer", indexer_oss::management_service())
.nest("/mcp/w/:workspace_id/list_tools", mcp_list_tools_service)
.nest("/health/detailed", health::detailed_service())
.route_layer(from_extractor::<ApiAuthed>())
@@ -612,8 +610,10 @@ pub async fn run_server(
if let Some(agent_workers_job_completed_tx) =
agent_workers_job_completed_tx.clone()
{
windmill_api_agent_workers::global_service(agent_workers_job_completed_tx)
.layer(Extension(agent_cache.clone()))
windmill_api_agent_workers::global_service(
agent_workers_job_completed_tx,
)
.layer(Extension(agent_cache.clone()))
} else {
Router::new()
}
@@ -785,7 +785,10 @@ pub async fn run_server(
},
)
// JWKS endpoint for HashiCorp Vault JWT authentication (must be outside /api prefix)
.route("/.well-known/jwks.json", get(windmill_api_settings::get_jwks))
.route(
"/.well-known/jwks.json",
get(windmill_api_settings::get_jwks),
)
.fallback(static_assets::static_handler)
.layer(middleware_stack);
@@ -2,11 +2,14 @@
import { Button } from '$lib/components/common'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { IndexSearchService } from '$lib/gen'
import type { GetIndexerStatusResponse } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { displaySize } from '$lib/utils'
import Tooltip from '../Tooltip.svelte'
import IntegerInput from '../IntegerInput.svelte'
import InputError from '../InputError.svelte'
import Label from '../Label.svelte'
import { Loader2, RefreshCw } from 'lucide-svelte'
import type { Writable } from 'svelte/store'
interface Props {
@@ -19,6 +22,35 @@
let clearJobsIndexModalOpen = $state(false)
let clearServiceLogsIndexModalOpen = $state(false)
let status: GetIndexerStatusResponse | undefined = $state(undefined)
let statusLoading = $state(true)
let statusError = $state(false)
function formatTimeAgo(isoDate: string): string {
const diffMs = Date.now() - new Date(isoDate).getTime()
const diffSecs = Math.floor(diffMs / 1000)
if (diffSecs < 60) return `${diffSecs}s ago`
const diffMins = Math.floor(diffSecs / 60)
if (diffMins < 60) return `${diffMins}m ago`
const diffHours = Math.floor(diffMins / 60)
return `${diffHours}h ago`
}
async function loadStatus() {
statusLoading = true
statusError = false
try {
status = await IndexSearchService.getIndexerStatus()
} catch (e) {
status = undefined
statusError = true
} finally {
statusLoading = false
}
}
loadStatus()
</script>
<div class="space-y-6">
@@ -52,30 +84,101 @@
/>
<InputError error={errors.writer_memory_budget ?? ''} />
</div>
<Label label="Indexer status">
{#snippet action()}
<button
onclick={loadStatus}
disabled={statusLoading}
class="inline-flex items-center text-secondary hover:text-primary disabled:opacity-50"
>
{#if statusLoading}
<Loader2 size={14} class="animate-spin" />
{:else}
<RefreshCw size={14} />
{/if}
</button>
{/snippet}
{#if status}
<div class="flex flex-col gap-2">
{#each [{ label: 'Job indexer', entry: status.job_indexer }, { label: 'Service log indexer', entry: status.log_indexer }] as { label, entry } (label)}
<div class="flex flex-row items-center gap-2 text-xs">
<span
class="inline-block w-2 h-2 rounded-full {entry?.is_alive
? 'bg-green-500'
: 'bg-red-500'}"
></span>
<span class="text-primary font-medium">{label}:</span>
<span class="font-semibold {entry?.is_alive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">
{entry?.is_alive ? 'Running' : 'Stopped'}
</span>
{#if entry?.last_locked_at}
<span class="text-tertiary text-2xs">
Last active: {formatTimeAgo(entry.last_locked_at)}
</span>
{/if}
</div>
{/each}
</div>
{:else if statusError && !statusLoading}
<span class="text-2xs text-secondary">
Could not fetch indexer status. Search may not be enabled in this build.
</span>
{/if}
</Label>
<Label label="Index storage">
{#if status}
<div class="flex flex-col gap-1 text-2xs text-tertiary">
<span>
Jobs index:
{#if status.job_indexer?.storage?.disk_size_bytes != null}
Disk: {displaySize(status.job_indexer.storage.disk_size_bytes) ?? 'N/A'}
{/if}
{#if status.job_indexer?.storage?.s3_size_bytes != null}
{#if status.job_indexer?.storage?.disk_size_bytes != null}&middot;{/if}
S3: {displaySize(status.job_indexer.storage.s3_size_bytes) ?? 'N/A'}
{/if}
</span>
<span>
Service logs index:
{#if status.log_indexer?.storage?.disk_size_bytes != null}
Disk: {displaySize(status.log_indexer.storage.disk_size_bytes) ?? 'N/A'}
{/if}
{#if status.log_indexer?.storage?.s3_size_bytes != null}
{#if status.log_indexer?.storage?.disk_size_bytes != null}&middot;{/if}
S3: {displaySize(status.log_indexer.storage.s3_size_bytes) ?? 'N/A'}
{/if}
</span>
</div>
{/if}
</Label>
<Label label="Clear index">
<span class="text-xs text-secondary"
>This buttons will clear the whole index, and the service will start reindexing from scratch.
>These buttons will clear the whole index, and the service will start reindexing from scratch.
Full text search might be down during this time.</span
>
<div class="flex flex-row gap-2">
<Button
variant="default"
unifiedSize="sm"
on:click={() => {
clearJobsIndexModalOpen = true
}}
>
Clear jobs index
</Button>
<Button
variant="default"
unifiedSize="sm"
on:click={() => {
clearServiceLogsIndexModalOpen = true
}}
>
Clear service logs index
</Button>
<div class="flex flex-col gap-3">
<div class="flex flex-row items-center gap-2">
<Button
variant="default"
unifiedSize="sm"
onclick={() => {
clearJobsIndexModalOpen = true
}}
>
Clear jobs index
</Button>
</div>
<div class="flex flex-row items-center gap-2">
<Button
variant="default"
unifiedSize="sm"
onclick={() => {
clearServiceLogsIndexModalOpen = true
}}
>
Clear service logs index
</Button>
</div>
</div>
</Label>
<ConfirmationModal