Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0c6a26c
initial commit
chennn1990 Feb 10, 2026
247b5a6
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 11, 2026
e7ce286
emtityMaintainer task biolerplate
chennn1990 Feb 11, 2026
1163568
tmp commit
chennn1990 Feb 12, 2026
54bd453
tmp changes
chennn1990 Feb 12, 2026
9656aa0
remove name
chennn1990 Feb 12, 2026
cbcdcc5
save objects creation
chennn1990 Feb 12, 2026
2848ee8
save object integration
chennn1990 Feb 12, 2026
af1f246
minor changes
chennn1990 Feb 15, 2026
5516b54
code cleanup
chennn1990 Feb 15, 2026
94257bc
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 15, 2026
f382b01
remove dummy registration
chennn1990 Feb 15, 2026
7940c03
remove dummy
chennn1990 Feb 15, 2026
06597cb
refactoring and reordering files
chennn1990 Feb 16, 2026
47ee01f
add optional chaining
chennn1990 Feb 16, 2026
1e7ae7e
expose esClient and namespace
chennn1990 Feb 16, 2026
f487b32
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 16, 2026
dba40fd
Changes from node scripts/check_mappings_update --fix
kibanamachine Feb 16, 2026
f7ae5b5
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 16, 2026
d63d895
ts fixes
chennn1990 Feb 16, 2026
2dd6abf
add PR comment
chennn1990 Feb 17, 2026
442c007
CR comments
chennn1990 Feb 17, 2026
f3cfb44
Changes from node scripts/jest_integration -u src/core/server/integra…
kibanamachine Feb 17, 2026
9aadac0
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 17, 2026
684a41f
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 17, 2026
f6a2287
Merge branch 'entity-store/maintainer-framework-poc' of https://githu…
chennn1990 Feb 17, 2026
d53e732
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 18, 2026
deb6274
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 18, 2026
6b9cf97
Add framework description to the plugin readme
chennn1990 Feb 18, 2026
9ded95b
Merge branch 'entity-store/maintainer-framework-poc' of https://githu…
chennn1990 Feb 18, 2026
2e792a2
Changes from node scripts/build_plugin_list_docs
kibanamachine Feb 18, 2026
ae48b1f
rename directory
chennn1990 Feb 18, 2026
9d9c7bd
Merge branch 'entity-store/maintainer-framework-poc' of https://githu…
chennn1990 Feb 18, 2026
ef9c929
in mamory management for maintainers tasks
chennn1990 Feb 18, 2026
39e0807
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 18, 2026
19fc383
convert to EntityMaintainersRegistry
chennn1990 Feb 19, 2026
cd5cae7
Merge branch 'entity-store/maintainer-framework-poc' of https://githu…
chennn1990 Feb 19, 2026
e8b290d
Merge branch 'main' into entity-store/maintainer-framework-poc
chennn1990 Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/kbn-check-saved-objects-cli/current_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@
"timestampField",
"type"
],
"entity-maintainers-tasks": [
"entity-maintainers-tasks",
"entity-maintainers-tasks.id",
"entity-maintainers-tasks.interval"
],
"epm-packages": [
"additional_spaces_installed_kibana",
"es_index_patterns",
Expand Down
16 changes: 16 additions & 0 deletions packages/kbn-check-saved-objects-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,22 @@
}
}
},
"entity-maintainers-tasks": {
"dynamic": false,
"properties": {
"entity-maintainers-tasks": {
"properties": {
"id": {
"type": "keyword"
},
"interval": {
"type": "keyword"
}
},
"type": "nested"
}
}
},
"epm-packages": {
"dynamic": false,
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import type {
ManagedEntityDefinition,
} from '../../common/domain/definitions/entity_schema';
import { scheduleExtractEntityTask, stopExtractEntityTask } from '../tasks/extract_entity_task';
import { scheduleEntityMaintainerTasks } from '../tasks/entity_maintainer';
import { installElasticsearchAssets, uninstallElasticsearchAssets } from './assets/install_assets';
import type {
EngineDescriptor,
EngineDescriptorClient,
EntityMaintainersTasksClient,
LogExtractionState,
} from './definitions/saved_objects';
import type { LogExtractionBodyParams } from '../routes/constants';
Expand Down Expand Up @@ -48,6 +50,7 @@ interface AssetManagerDependencies {
namespace: string;
isServerless: boolean;
logsExtractionClient: LogsExtractionClient;
entityMaintainersTasksClient: EntityMaintainersTasksClient;
}

export class AssetManager {
Expand All @@ -58,6 +61,7 @@ export class AssetManager {
private readonly namespace: string;
private readonly isServerless: boolean;
private readonly logsExtractionClient: LogsExtractionClient;
private readonly entityMaintainersTasksClient: EntityMaintainersTasksClient;

constructor(deps: AssetManagerDependencies) {
this.logger = deps.logger;
Expand All @@ -67,15 +71,30 @@ export class AssetManager {
this.namespace = deps.namespace;
this.isServerless = deps.isServerless;
this.logsExtractionClient = deps.logsExtractionClient;
this.entityMaintainersTasksClient = deps.entityMaintainersTasksClient;
}

public async initEntity(
public async init(
request: KibanaRequest,
type: EntityType,
entityTypes: EntityType[],
logExtractionParams?: LogExtractionBodyParams
) {
await this.install(type, logExtractionParams); // TODO: async
await this.start(request, type);
try {
await Promise.all(
entityTypes.map((type) => this.initEntity(request, type, logExtractionParams))
);

await scheduleEntityMaintainerTasks({
logger: this.logger,
taskManager: this.taskManager,
namespace: this.namespace,
request,
entityMaintainersTasksClient: this.entityMaintainersTasksClient,
});
} catch (error) {
this.logger.error('Error during entity store init:', error);
throw error;
}
}

public async start(request: KibanaRequest, type: EntityType) {
Expand Down Expand Up @@ -113,37 +132,6 @@ export class AssetManager {
}
}

public async install(
type: EntityType,
logExtractionParams?: LogExtractionBodyParams
): Promise<ManagedEntityDefinition> {
// TODO: return early if already installed
try {
this.logger.get(type).debug(`Installing assets for entity type: ${type}`);
const definition = getEntityDefinition(type, this.namespace);
const initialState: Partial<LogExtractionState> = logExtractionParams ?? {};

await Promise.all([
this.engineDescriptorClient.init(type, initialState),
installElasticsearchAssets({
esClient: this.esClient,
logger: this.logger,
definition,
namespace: this.namespace,
}),
]);

await this.engineDescriptorClient.update(type, { status: ENGINE_STATUS.STARTED });

this.logger.debug(`Installed definition: ${type}`);

return definition;
} catch (error) {
this.logger.error(`Error installing assets for entity type ${type}`, { error });
throw error;
}
}

public async uninstall(type: EntityType) {
Comment thread
chennn1990 marked this conversation as resolved.
try {
const definition = getEntityDefinition(type, this.namespace);
Expand Down Expand Up @@ -185,6 +173,46 @@ export class AssetManager {
}
}

private async initEntity(
request: KibanaRequest,
type: EntityType,
logExtractionParams?: LogExtractionBodyParams
) {
await this.install(type, logExtractionParams);
await this.start(request, type);
}

private async install(
type: EntityType,
logExtractionParams?: LogExtractionBodyParams
): Promise<ManagedEntityDefinition> {
// TODO: return early if already installed
try {
this.logger.get(type).debug(`Installing assets for entity type: ${type}`);
const definition = getEntityDefinition(type, this.namespace);
const initialState: Partial<LogExtractionState> = logExtractionParams ?? {};

await Promise.all([
this.engineDescriptorClient.init(type, initialState),
installElasticsearchAssets({
esClient: this.esClient,
logger: this.logger,
definition,
namespace: this.namespace,
}),
]);

await this.engineDescriptorClient.update(type, { status: ENGINE_STATUS.STARTED });

this.logger.debug(`Installed definition: ${type}`);

return definition;
} catch (error) {
this.logger.error(`Error installing assets for entity type ${type}`, { error });
throw error;
}
}

private async getEngineWithComponents(
engine: EngineDescriptor
): Promise<EngineDescriptor & { components: EngineComponentStatus[] }> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
*/

import { z } from '@kbn/zod';
import { TasksConfig } from '../../../tasks/config';
import { EntityStoreTaskType } from '../../../tasks/constants';
import { EntityType } from '../../../../common/domain/definitions/entity_schema';
import { TasksConfig } from '../../../../tasks/config';
import { EntityStoreTaskType } from '../../../../tasks/constants';
import { EntityType } from '../../../../../common/domain/definitions/entity_schema';

export type EngineStatus = z.infer<typeof EngineStatus>;
export const EngineStatus = z.enum(['installing', 'started', 'stopped', 'updating', 'error']);
Expand All @@ -34,7 +34,7 @@ export const LogExtractionState = z.object({
frequency: z
.string()
.regex(/[smdh]$/)
.default(TasksConfig[EntityStoreTaskType.Values.extractEntity].interval),
.default(TasksConfig[EntityStoreTaskType.Values.extractEntity].interval || '30s'),
paginationTimestamp: z.string().optional(),
lastExecutionTimestamp: z.string().optional(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import type {
SavedObjectsFindResponse,
} from '@kbn/core-saved-objects-api-server';
import { SavedObjectsErrorHelpers, type Logger } from '@kbn/core/server';
import type { EntityType } from '../../../../common/domain/definitions/entity_schema';
import type { EntityType } from '../../../../../common/domain/definitions/entity_schema';
import type { EngineDescriptor } from './constants';
import { LogExtractionState, VersionState } from './constants';
import { EngineDescriptorTypeName } from './engine_descriptor_type';
import { ENGINE_STATUS } from '../../constants';
import { EngineDescriptorTypeName } from './types';
import { ENGINE_STATUS } from '../../../constants';

export class EngineDescriptorClient {
constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { z } from '@kbn/zod';

export type EntityMaintainerTaskEntry = z.infer<typeof EntityMaintainerTaskEntry>;
export const EntityMaintainerTaskEntry = z.object({
id: z.string(),
interval: z.string().regex(/[smdh]$/),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
Comment thread
chennn1990 marked this conversation as resolved.
Outdated
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ISavedObjectsRepository } from '@kbn/core/server';
import { SavedObjectsErrorHelpers, type Logger } from '@kbn/core/server';
import type { EntityMaintainerTaskEntry } from './constants';
import { EntityMaintainerTaskEntry as EntityMaintainerTaskEntrySchema } from './constants';
import { EntityMaintainersTasksTypeName, EntityMaintainersTasksId } from './types';

const ENTITY_MAINTAINERS_TASKS_ATTR = 'entity-maintainers-tasks' as const;

export class EntityMaintainersTasksClient {
constructor(
private readonly repository: ISavedObjectsRepository,
private readonly logger: Logger
) {}

async getAll(): Promise<EntityMaintainerTaskEntry[]> {
try {
const doc = await this.repository.get<
Record<typeof ENTITY_MAINTAINERS_TASKS_ATTR, unknown[]>
Comment thread
chennn1990 marked this conversation as resolved.
Outdated
>(EntityMaintainersTasksTypeName, EntityMaintainersTasksId);
const raw = doc.attributes[ENTITY_MAINTAINERS_TASKS_ATTR] ?? [];
return raw.map((entry) => EntityMaintainerTaskEntrySchema.parse(entry));
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
return [];
}
this.logger.error(`Failed to get entity maintainer tasks: ${err?.message}`);
throw err;
}
}

async addOrUpdate(entry: EntityMaintainerTaskEntry): Promise<void> {
const taskEntry = EntityMaintainerTaskEntrySchema.parse(entry);
try {
const existing = await this.repository.get<
Record<typeof ENTITY_MAINTAINERS_TASKS_ATTR, EntityMaintainerTaskEntry[]>
>(EntityMaintainersTasksTypeName, EntityMaintainersTasksId);
const tasks = existing.attributes[ENTITY_MAINTAINERS_TASKS_ATTR] ?? [];
this.logger.debug(`Tasks registered: ${JSON.stringify(tasks)}`);
const filtered = tasks.filter((t) => t.id !== taskEntry.id);
await this.repository.update(EntityMaintainersTasksTypeName, EntityMaintainersTasksId, {
[ENTITY_MAINTAINERS_TASKS_ATTR]: [...filtered, taskEntry],
});
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
this.logger.debug(`Creating entity maintainers tasks document with first entry`);
await this.repository.create(
EntityMaintainersTasksTypeName,
{ [ENTITY_MAINTAINERS_TASKS_ATTR]: [taskEntry] },
{ id: EntityMaintainersTasksId }
);
return;
}
Comment thread
chennn1990 marked this conversation as resolved.
Outdated
this.logger.error(
`Failed to register entity maintainer task in saved object: ${err?.message}`
);
throw err;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { SavedObjectsFullModelVersion } from '@kbn/core-saved-objects-server';
import type { SavedObjectsType } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';

export const EntityMaintainersTasksTypeName = 'entity-maintainers-tasks';
export const EntityMaintainersTasksId = 'entity-maintainers-tasks';

export const EntityMaintainersTasksTypeMappings: SavedObjectsType['mappings'] = {
dynamic: false,
properties: {
'entity-maintainers-tasks': {
type: 'nested',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of nested I think it would be simpler and more in line with other saved object types to have one document per maintainer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hop-dev do you have examples of how others behave?

Tbh, I wonder if we even need to have saved objects for this. Isn't this something that we have all in memory always? Why do we need to store in ES? If we have a kibana restart, the maintainer will be registered again, won't it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if you go to packages/kbn-check-saved-objects-cli/current_mappings.json which is part of this PRs diff we dump all of the saved object mappings in there. This would be the 11th use of nested in a saved object type in Kibana.

The other ones have lots of other properties on the object, whereas here we have one single property which is nested, which to me is like saying "these things are separate documents" because we have no shared maintainer properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romulets The reason we are using saved object is because we need the id and interval for the scheduling phase.
This is the order:

  1. on setup phase - registering to the maintainers with all the configuration needed for it.
  2. on entity-store install phase - scheduling the maintainers, here we should get al the ids of the registered maintainers.

@hop-dev I will go over to figure out if there is a better option then nested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type: 'nested' – In Elasticsearch, an array of objects should be mapped as nested so each element is a single “document” and the pair id/interval stays together

@hop-dev that is exactly what i want

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is if that's what you want, and you have no other properties on this saved object, why not just have them as separate objects? I won't keep pushing the point, the core team will review too so they might give us some helpful guidance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want them to be together, because on install, when fetching all the registered id's, i need their related intervals.
If i will keep it separately i will lose the connection between maintainer id to its interval

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romulets The reason we are using saved object is because we need the id and interval for the

Don't we have this in memory always?

properties: {
id: { type: 'keyword' },
interval: { type: 'keyword' },
},
},
},
};

const entityMaintainerTaskEntrySchema = schema.object({
id: schema.string(),
interval: schema.string(),
});

const entityMaintainersTasksAttributesSchema = {
'entity-maintainers-tasks': schema.arrayOf(entityMaintainerTaskEntrySchema),
Comment thread Fixed
};

const version1: SavedObjectsFullModelVersion = {
changes: [],
schemas: {
create: schema.object(entityMaintainersTasksAttributesSchema),
forwardCompatibility: schema.object(entityMaintainersTasksAttributesSchema, {
unknowns: 'ignore',
}),
},
};

export const EntityMaintainersTasksType: SavedObjectsType = {
name: EntityMaintainersTasksTypeName,
hidden: true,
namespaceType: 'single',
mappings: EntityMaintainersTasksTypeMappings,
modelVersions: { 1: version1 },
hiddenFromHttpApis: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* 2.0.
*/

export * from './engine_descriptor_type';
export * from './engine_descriptor/constants';
export * from './engine_descriptor/types';
export * from './engine_descriptor';
export * from './constants';
export * from './entity_maintainers_client/constants';
export * from './entity_maintainers_client/types';
export * from './entity_maintainers_client';
Loading
Loading