mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
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:
@@ -1 +1 @@
|
||||
8ffae1f43b31dc8136714fa612d22b6301773e27
|
||||
9b3339730eb4bb0b564c7c56ac546f33fb3d8905
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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}·{/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}·{/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
|
||||
|
||||
Reference in New Issue
Block a user