feat(events): add events management capabilities
This change introduces some new plugins which provide the basics for managing events inside of backstage. Hereby, it offers extension points to add event publishers and subscribers as well as to exchange the event broker implementation. - `@backstage/plugin-events-backend`: backend for the events management which connects all parts and provides a simple in-memory event broker - `@backstage/plugin-events-node`: interfaces and API for `@backstage/plugin-events-backend` - `@backstage/plugin-events-test-utils`: test utilities like implementations useful for writing tests at modules All plugins support the new backend-plugin-api. Relates-to: #11082 Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Backstage backend plugin "events" that provides the event management.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { EventsBackend } from './service/EventsBackend';
|
||||
export { eventsPlugin } from './service/EventsPlugin';
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
TestEventBroker,
|
||||
TestEventPublisher,
|
||||
TestEventSubscriber,
|
||||
} from '@backstage/plugin-events-backend-test-utils';
|
||||
import { EventsBackend } from './EventsBackend';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
describe('EventsBackend', () => {
|
||||
it('wires up all components', async () => {
|
||||
const eventBroker = new TestEventBroker();
|
||||
const publisher1 = new TestEventPublisher();
|
||||
const publisher2 = new TestEventPublisher();
|
||||
|
||||
await new EventsBackend(logger)
|
||||
.setEventBroker(eventBroker)
|
||||
.addPublishers(publisher1, [publisher2])
|
||||
.addSubscribers(new TestEventSubscriber('one', ['topicA']), [
|
||||
new TestEventSubscriber('two', ['topicA', 'topicB']),
|
||||
])
|
||||
.start();
|
||||
|
||||
await eventBroker.publish({
|
||||
topic: 'topicA',
|
||||
eventPayload: { test: 'payload' },
|
||||
});
|
||||
|
||||
expect(eventBroker.published.length).toEqual(1);
|
||||
expect(eventBroker.published[0].topic).toEqual('topicA');
|
||||
expect(eventBroker.published[0].eventPayload).toEqual({ test: 'payload' });
|
||||
|
||||
expect(eventBroker.subscribed.length).toEqual(2);
|
||||
expect(
|
||||
eventBroker.subscribed.map(
|
||||
sub => (sub as unknown as TestEventSubscriber).name,
|
||||
),
|
||||
).toEqual(['one', 'two']);
|
||||
|
||||
expect(publisher1.eventBroker).toBe(eventBroker);
|
||||
expect(publisher2.eventBroker).toBe(eventBroker);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventBroker,
|
||||
EventPublisher,
|
||||
EventSubscriber,
|
||||
} from '@backstage/plugin-events-node';
|
||||
import { Logger } from 'winston';
|
||||
import { InMemoryEventBroker } from './InMemoryEventBroker';
|
||||
|
||||
/**
|
||||
* A builder that helps wire up all component parts of the event management.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class EventsBackend {
|
||||
private eventBroker: EventBroker;
|
||||
private publishers: EventPublisher[] = [];
|
||||
private subscribers: EventSubscriber[] = [];
|
||||
|
||||
constructor(logger: Logger) {
|
||||
this.eventBroker = new InMemoryEventBroker(logger);
|
||||
}
|
||||
|
||||
setEventBroker(eventBroker: EventBroker): EventsBackend {
|
||||
this.eventBroker = eventBroker;
|
||||
return this;
|
||||
}
|
||||
|
||||
addPublishers(
|
||||
...publishers: Array<EventPublisher | Array<EventPublisher>>
|
||||
): EventsBackend {
|
||||
this.publishers.push(...publishers.flat());
|
||||
return this;
|
||||
}
|
||||
|
||||
addSubscribers(
|
||||
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
|
||||
): EventsBackend {
|
||||
this.subscribers.push(...subscribers.flat());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires up and returns all component parts of the event management.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
this.eventBroker.subscribe(this.subscribers);
|
||||
this.publishers.forEach(publisher =>
|
||||
publisher.setEventBroker(this.eventBroker),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createBackendModule,
|
||||
loggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
|
||||
import {
|
||||
TestEventBroker,
|
||||
TestEventPublisher,
|
||||
TestEventSubscriber,
|
||||
} from '@backstage/plugin-events-backend-test-utils';
|
||||
import { eventsPlugin } from './EventsPlugin';
|
||||
|
||||
describe('eventPlugin', () => {
|
||||
it('should be initialized properly', async () => {
|
||||
const eventBroker = new TestEventBroker();
|
||||
const publisher = new TestEventPublisher();
|
||||
const subscriber = new TestEventSubscriber('sub', ['topicA']);
|
||||
|
||||
const testModule = createBackendModule({
|
||||
pluginId: 'events',
|
||||
moduleId: 'test',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
events: eventsExtensionPoint,
|
||||
},
|
||||
async init({ events }) {
|
||||
events.setEventBroker(eventBroker);
|
||||
events.addPublishers(publisher);
|
||||
events.addSubscribers(subscriber);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [],
|
||||
services: [[loggerServiceRef, getVoidLogger()]],
|
||||
features: [eventsPlugin(), testModule()],
|
||||
});
|
||||
|
||||
expect(publisher.eventBroker).toBe(eventBroker);
|
||||
expect(eventBroker.subscribed.length).toEqual(1);
|
||||
expect(eventBroker.subscribed[0]).toBe(subscriber);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createBackendPlugin,
|
||||
loggerServiceRef,
|
||||
loggerToWinstonLogger,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
EventBroker,
|
||||
EventPublisher,
|
||||
EventSubscriber,
|
||||
eventsExtensionPoint,
|
||||
EventsExtensionPoint,
|
||||
} from '@backstage/plugin-events-node';
|
||||
import { InMemoryEventBroker } from './InMemoryEventBroker';
|
||||
|
||||
class EventsExtensionPointImpl implements EventsExtensionPoint {
|
||||
#eventBroker: EventBroker | undefined;
|
||||
#publishers: EventPublisher[] = [];
|
||||
#subscribers: EventSubscriber[] = [];
|
||||
|
||||
setEventBroker(eventBroker: EventBroker): void {
|
||||
this.#eventBroker = eventBroker;
|
||||
}
|
||||
|
||||
addPublishers(
|
||||
...publishers: Array<EventPublisher | Array<EventPublisher>>
|
||||
): void {
|
||||
this.#publishers.push(...publishers.flat());
|
||||
}
|
||||
|
||||
addSubscribers(
|
||||
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
|
||||
): void {
|
||||
this.#subscribers.push(...subscribers.flat());
|
||||
}
|
||||
|
||||
get eventBroker() {
|
||||
return this.#eventBroker;
|
||||
}
|
||||
|
||||
get publishers() {
|
||||
return this.#publishers;
|
||||
}
|
||||
|
||||
get subscribers() {
|
||||
return this.#subscribers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Events plugin
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const eventsPlugin = createBackendPlugin({
|
||||
id: 'events',
|
||||
register(env) {
|
||||
const extensionPoint = new EventsExtensionPointImpl();
|
||||
env.registerExtensionPoint(eventsExtensionPoint, extensionPoint);
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
async init({ logger }) {
|
||||
if (!extensionPoint.eventBroker) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
extensionPoint.setEventBroker(new InMemoryEventBroker(winstonLogger));
|
||||
}
|
||||
|
||||
extensionPoint.eventBroker!.subscribe(extensionPoint.subscribers);
|
||||
extensionPoint.publishers.forEach(publisher =>
|
||||
publisher.setEventBroker(extensionPoint.eventBroker!),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestEventSubscriber } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { InMemoryEventBroker } from './InMemoryEventBroker';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
describe('InMemoryEventBroker', () => {
|
||||
it('passes events to interested subscribers', () => {
|
||||
const subscriber1 = new TestEventSubscriber('test1', ['topicA', 'topicB']);
|
||||
const subscriber2 = new TestEventSubscriber('test2', ['topicB', 'topicC']);
|
||||
const eventBroker = new InMemoryEventBroker(logger);
|
||||
|
||||
eventBroker.subscribe(subscriber1);
|
||||
eventBroker.subscribe(subscriber2);
|
||||
eventBroker.publish({ topic: 'topicA', eventPayload: { test: 'topicA' } });
|
||||
eventBroker.publish({ topic: 'topicB', eventPayload: { test: 'topicB' } });
|
||||
eventBroker.publish({ topic: 'topicC', eventPayload: { test: 'topicC' } });
|
||||
eventBroker.publish({ topic: 'topicD', eventPayload: { test: 'topicD' } });
|
||||
|
||||
expect(Object.keys(subscriber1.receivedEvents)).toEqual([
|
||||
'topicA',
|
||||
'topicB',
|
||||
]);
|
||||
expect(subscriber1.receivedEvents.topicA.length).toEqual(1);
|
||||
expect(subscriber1.receivedEvents.topicA[0]).toEqual({
|
||||
topic: 'topicA',
|
||||
eventPayload: { test: 'topicA' },
|
||||
});
|
||||
expect(subscriber1.receivedEvents.topicB.length).toEqual(1);
|
||||
expect(subscriber1.receivedEvents.topicB[0]).toEqual({
|
||||
topic: 'topicB',
|
||||
eventPayload: { test: 'topicB' },
|
||||
});
|
||||
|
||||
expect(Object.keys(subscriber2.receivedEvents)).toEqual([
|
||||
'topicB',
|
||||
'topicC',
|
||||
]);
|
||||
expect(subscriber2.receivedEvents.topicB.length).toEqual(1);
|
||||
expect(subscriber2.receivedEvents.topicB[0]).toEqual({
|
||||
topic: 'topicB',
|
||||
eventPayload: { test: 'topicB' },
|
||||
});
|
||||
expect(subscriber2.receivedEvents.topicC.length).toEqual(1);
|
||||
expect(subscriber2.receivedEvents.topicC[0]).toEqual({
|
||||
topic: 'topicC',
|
||||
eventPayload: { test: 'topicC' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventBroker,
|
||||
EventParams,
|
||||
EventSubscriber,
|
||||
} from '@backstage/plugin-events-node';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/**
|
||||
* In-memory event broker which will pass the event to all registered subscribers
|
||||
* interested in it.
|
||||
* Events will not be persisted in any form.
|
||||
*/
|
||||
// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.)
|
||||
export class InMemoryEventBroker implements EventBroker {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
private readonly subscribers: {
|
||||
[topic: string]: EventSubscriber[];
|
||||
} = {};
|
||||
|
||||
async publish(params: EventParams): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Event received: topic=${params.topic}, metadata=${JSON.stringify(
|
||||
params.metadata,
|
||||
)}, payload=${JSON.stringify(params.eventPayload)}`,
|
||||
);
|
||||
|
||||
const subscribed = this.subscribers[params.topic] ?? [];
|
||||
subscribed.forEach(subscriber => subscriber.onEvent(params));
|
||||
}
|
||||
|
||||
subscribe(
|
||||
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
|
||||
): void {
|
||||
subscribers.flat().forEach(subscriber => {
|
||||
subscriber.supportsEventTopics().forEach(topic => {
|
||||
this.subscribers[topic] = this.subscribers[topic] ?? [];
|
||||
this.subscribers[topic].push(subscriber);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user