Skip to content

Instantly share code, notes, and snippets.

@matchaxnb
Created December 2, 2022 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matchaxnb/dd823c3d9132d591371e311ecc6bd92e to your computer and use it in GitHub Desktop.
Save matchaxnb/dd823c3d9132d591371e311ecc6bd92e to your computer and use it in GitHub Desktop.
// ...
import { createInputErrorSubscriber } from './InputErrorProcessor';
// ...
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = CatalogBuilder.create(env);
builder.subscribe(
createInputErrorSubscriber({
logger: env.logger,
scheduler: env.scheduler,
}),
);
// ...
}
import { InputError } from '@backstage/errors';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { Logger } from 'winston';
import {
PluginTaskScheduler,
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
type IdentifyingName = string;
type StringLocation = string;
type ProcessingErrorEvent = {
unprocessedEntity: Entity;
errors: Error[];
};
type IdentifyingProblematicEntityData = {
kind: string;
name: string;
namespace: string;
managedByLocation: StringLocation;
};
type Problems = {
location: StringLocation;
errors: Set<ErrorMessage>;
};
function getProblemId(
identifier: IdentifyingProblematicEntityData,
): IdentifyingName {
const { kind, name, namespace, managedByLocation } = identifier;
return `${kind}:${namespace}/${name}@${managedByLocation}`;
}
type ErrorMessage = typeof InputError.prototype.message;
const defaultSchedule: TaskScheduleDefinition = {
frequency: {
cron: "35 6-20 * * Mon-Fri",
},
timeout: { seconds: 10 },
};
const dailySchedule: TaskScheduleDefinition = {
frequency: {
days: 1,
},
timeout: { seconds: 10 },
initialDelay: { days: 1 },
};
class InputErrorProcessor {
public onProcessingError(event: ProcessingErrorEvent) {
const { unprocessedEntity, errors } = event;
const relevantErrors: Error[] = [];
for (const err of errors) {
if (!(err instanceof InputError)) {
continue;
}
relevantErrors.push(err);
}
if (relevantErrors.length > 0) {
const managedByLocation: StringLocation =
unprocessedEntity.metadata.annotations?.[
'backstage.io/managed-by-location'
] ?? 'unknown';
const usefulInfo: IdentifyingProblematicEntityData = {
kind: unprocessedEntity.kind,
name: unprocessedEntity.metadata.name,
namespace: unprocessedEntity.metadata.namespace ?? DEFAULT_NAMESPACE,
managedByLocation: managedByLocation,
};
const uq = getProblemId(usefulInfo);
const errorSet =
this.errorRegistry.get(uq) ??
({
location: managedByLocation,
errors: new Set<ErrorMessage>(),
} as Problems);
errorSet.location = managedByLocation;
relevantErrors.forEach(value => errorSet.errors.add(value.message));
this.errorRegistry.set(uq, errorSet);
}
}
getErrorRegistry() {
return this.errorRegistry as ReadonlyMap<IdentifyingName, Problems>;
}
dumpErrors(): Record<string, Array<string>> {
const outRecord: Record<string, Array<string>> = {};
for (const k of this.errorRegistry.keys()) {
const val = this.errorRegistry.get(k);
outRecord[val!.location] = new Array<string>(...val!.errors);
}
return outRecord;
}
constructor(
logger: Logger,
scheduler: PluginTaskScheduler,
schedule: TaskScheduleDefinition,
) {
this.errorRegistry = new Map<IdentifyingName, Problems>();
this.logger = logger;
this.logger.info('Constructing InputErrorProcessor');
// on a regular basis, send out alerts about the entities with processing problems.
scheduler.scheduleTask({
...schedule,
id: 'complain-about-invalid-entities',
fn: () => {
this.logger.info('Invalid entities', {
problems: JSON.stringify(this.dumpErrors()),
issueType: 'backstage-invalid-entities',
});
},
});
scheduler.scheduleTask({
...dailySchedule,
id: 'cleanup-invalid-entities-complain-list',
fn: () => {
this.logger.info(
'Cleaning up the invalid entities list. One last time displayed in the next message.',
);
this.logger.info('Invalid entities', {
problems: JSON.stringify(this.dumpErrors()),
issueType: 'backstage-invalid-entities',
});
this.errorRegistry.clear();
},
});
return this;
}
private logger;
private errorRegistry;
}
type createInputErrorSubscriberArguments = {
logger: Logger;
scheduler: PluginTaskScheduler;
schedule?: TaskScheduleDefinition;
};
export function createInputErrorSubscriber(
args: createInputErrorSubscriberArguments,
) {
const { logger, scheduler, schedule } = args;
const properSchedule = schedule ?? defaultSchedule;
const f = new InputErrorProcessor(logger, scheduler, properSchedule);
return {
onProcessingError: (event: ProcessingErrorEvent) =>
f.onProcessingError(event),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment