Created
December 2, 2022 16:56
-
-
Save matchaxnb/dd823c3d9132d591371e311ecc6bd92e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
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, | |
}), | |
); | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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