Creating a new Third-Party Integration
Introduction
One of the most fundamental components of Soundcheck are its third party Integrations, also known as Fact Collectors. Integrations are responsible for collecting data about entities from third parties. A single piece of data collected about a single entity is called a Fact in the Soundcheck system. Facts are referenced in Soundcheck Checks to determine if an entity is adhering to the rules defined by the Check.
Soundcheck comes with a set of Integrations out-of-the-box, but adopters will inevitably want to customize their Soundcheck instance by adding new Integrations specific to their organizational needs.
To that end, this guide will walk you through the process of creating a new Integration
for a
third party from scratch, using the PagerDuty API as an example. This Integration
will collect
service details from PagerDuty for entities with a pagerduty.com/service-id
annotation, which
associates the entities with a PagerDuty service. We'll call this new Integration the
ExamplePagerDutyFactCollector
.
Specific callouts for best practices and tips from the Soundcheck team are provided throughout. Let's get started!
Pre-requisites
This guide assumes you have Soundcheck installed in your backstage instance. If you don't, follow this guide to install Soundcheck backend and this guide to install the Soundcheck frontend before proceeding.
This guide covers creating an Integration in the new backend system. The old system is set to be deleted in the first quarter of 2025, so it is recommended to use the new system for all new Integrations.
Understanding Integrations
All Integrations should implement the ConfigurableFactCollector
interface or the FactCollector
interface. These interfaces are defined in the @spotify/backstage-plugin-soundcheck-node
package.
We replicate both here for convenience:
/**
* Fact collectors collect one or more facts about entities.
*
* @public
*/
export interface FactCollector {
/**
* A unique identifier for the {@link FactCollector}.
*
* Generally shorthand for the source (e.g., catalog, github).
*/
id: string;
/**
* A name for the {@link FactCollector} suitable for display in a user interface.
*/
name?: string;
/**
* A description of the {@link FactCollector} suitable for display in a user interface.
*/
description?: string;
/**
* Collect the requested facts for the given entities.
*
* @param entities - The entities to collect facts on.
* @param params - Optional parameters for the collection including:
* factRefs - references to the facts to be collected
* refresh - a hint that the specified facts should be refreshed rather than served from any
* sort of (local to the collector) cache.
*/
collect(
entities: Entity[],
params?: {
factRefs?: FactRef[];
refresh?: FactRef[];
},
): Promise<Fact[]>;
/**
* Returns the names of the facts that this collector can collect.
*/
getFactNames(): Promise<string[]>;
/**
* Returns the data schema for the requested fact. The returned object should be a JSON schema
* describing the data structure of the fact. This is used in Soundchecks NCUI to provide paths
* for a selected fact when creating a new Check.
*
* An example schema for the FileExists fact from the SCMFactCollector that comes with Soundcheck
* is as follows:
* {
* "title": "File Exists",
* "description": "File Exists",
* "type": "object",
* "properties": {
* "classpath": {
* "type": "boolean"
* },
* "project": {
* "type": "boolean"
* }
* }
* }
*
* @param factRef - Reference to the fact whose data schema should be returned.
*/
getDataSchema(factRef: FactRef): Promise<string | undefined>;
/**
* Returns the collection configurations for this {@link FactCollector}.
*/
getCollectionConfigs(): Promise<CollectionConfig[]>;
}
/**
* A {@link FactCollector} supporting dynamic configuration updates.
*
* @public
*/
export interface ConfigurableFactCollector extends FactCollector {
/**
* Returns the collector's current configuration.
*/
getConfig(): Promise<JsonValue | undefined>;
/**
* Sets the configuration of the fact collector.
*
* @param config - The new configuration settings for the fact collector.
*/
setConfig(config: JsonValue): Promise<void>;
/**
* Returns a JSON schema for the collectors config settings which can be used to validate them.
*/
getConfigSchema(): Promise<string>;
}
The ConfigurableFactCollector
's additional methods allow Soundcheck to configure the collector at runtime.
This is useful for setting up API tokens, configuring the frequency of fact collection, and any other
configuration options that may be specific to the Integration
.
Understanding the CollectionConfig
type and the getCollectionConfigs()
function
ThegetCollectionConfigs
method in the FactCollector
interface returns an
array of CollectionConfig
objects. The CollectionConfig
s returned by an Integration
specifies
how the integration collects facts. Most integrations will only return a single CollectionConfig
that applies to all facts the integration collects, but this is not a requirement.
The CollectionConfig
type is defined in the @spotify/backstage-plugin-soundcheck-common
package, but again, we provide it here for convenience:
/**
* Collection configuration for one or more facts such as schedules, filters, and cache settings.
*
* @public
*/
export type CollectionConfig = {
/**
* The facts to which this collection configuration applies.
*/
factRefs?: FactRef[];
/**
* A filter specifying which entities to collect the specified facts for.
*
* Matches the filter format used by the Catalog API:
* {@link https://backstage.io/docs/reference/catalog-client.entityfilterquery}
*/
filter?: EntityFilter;
/**
* A filter specifying which entities to exclude from collection.
*
* Matches the filter format used by the Catalog API:
* {@link https://backstage.io/docs/reference/catalog-client.entityfilterquery}
*/
exclude?: EntityFilter;
/**
* The frequency at which the specified facts should be collected.
*
* If not provided fact collection will not be scheduled.
*/
frequency?:
| {
cron: string;
}
| HumanDuration;
initialDelay?: Duration | HumanDuration;
/**
* A number of entities the fact collection will be scheduled for at once (if configured).
*
* If not provided defaults to a single entity.
*/
batchSize?: number;
/**
* If the collected facts should be cached, and if so for how long.
*
* If not provided facts will not be cached.
*/
cache?: CacheConfig;
};
Configuring an Integration
Now that we've covered the structure of an Integration
, we need to know how to configure it.
Configurations for Integration
s are specified under the soundcheck.collectors
key in the
app-config.yaml
file. Here is the configuration we'll be using for our ExamplePagerDutyFactCollector
:
soundcheck:
collectors:
example_pagerduty:
token: <redacted> # Token used to authenticate to PagerDuty to pull service details.
# Defaults for the collect configuration(s).
frequency:
minutes: 5
initialDelay:
seconds: 15
filter:
kind: Component
spec.lifecycle: production
# The 'collects' configurations specify what facts the integration should collect.
collects:
# Collect the 'Service' fact. Uses all defaults defined above.
- type: Service
# Also collect the 'Service' fact for all components with the 'redacted1' tag, but only every 30 minutes.
# Will use the same initial delay specified in the defaults above.
- type: Service
frequency:
minutes: 30
filter:
kind: Component
metadata.tags: ['redacted1']
Typically, Integration
s take a configuration object similar to the one shown above. The frequency
,
cache
, and filter
keys are common and are meant to dictate when the integration collects facts,
how long it caches them, and what entities it collects facts for, respectively. The 'collects'
configuration is specific to the integration and specifies what facts the integration should collect.
Validating Configurations
Note that our Integration
, and indeed all Integration
s, take no specific configuration types
other than a JsonValue. This allows for flexibility in how they are configured. However,
configurations should still be validated, and so best practice is to define a schema which should be
used to validate a configuration object. We recommend using Zod for creating
schemas and validating them.
Here is a simple snippet showing how the SCM Integration validates its configuration:
const schema = ScmFactCollectorSchema.safeParse(collectorConfig);
if (!schema.success) {
this.#logger.error(`Failed to parse SCMFactCollector from schema.`);
throw new InputError(schema.error.message);
}
For brevity, we've omitted zod definitions and schema validation in this guide.
Creating a New Module
With the basics of Fact Collectors covered, we can now use the backstage-cli
to create a new
backend module, in which we'll write the code for our new Integration
.
Make sure you've run yarn install
and installed dependencies, then run the following on your
command line:
yarn new
You will see the prompt below. Pick the 'backend-module' option. Then for the ID of the plugin enter "soundcheck" and for the ID of the module enter "example-pagerduty".
This will create a new Backstage backend module based on the ID that was provided. It will be built and added to the Backstage App automatically.
You will see the above output when the 'backend-module' is created successfully.
Creating the Example PagerDuty Integration
Now, let's walk through the creation of a new third-party Integration that we'll use to collect facts from PagerDuty.
Note: This example integration focuses only on PagerDuty service details on entities in our Catalog. Other features offered by pagerduty can be integrated, and have been in the official Pagerduty integration that can be added to Soundcheck.
At this point, you're assumed to have created a new module to house the code we'll be writing. If you've deviated from the names in the examples above, you'll need to ensure that you continue to use the names you've chosen in the following steps.
First let's add the packages we need, you can do this with the following command:
yarn --cwd plugins/soundcheck-backend-module-example-pagerduty add @spotify/backstage-plugin-soundcheck-node @backstage/catalog-model @backstage/config @spotify/backstage-plugin-soundcheck-common @pagerduty/pdjs @backstage/types luxon qs
Create a new file called ExamplePagerDutyFactCollector.ts
in the src
folder of the 'backend-module'
you just created and open it in your editor.
All code snippets in this guide are provided in order. If you cut and paste them into your
ExamplePagerDutyFactCollector.ts
file in the order in which they are presented, your new
Integration
should work without issues.
Let's add the imports. We'll need:
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { api } from '@pagerduty/pdjs';
import {
CollectionConfig,
Fact,
FactRef,
stringifyFactRef,
} from '@spotify/backstage-plugin-soundcheck-common';
import { ConfigurableFactCollector } from '@spotify/backstage-plugin-soundcheck-node';
import { DateTime } from 'luxon';
import qs from 'qs';
import { JsonObject, JsonValue } from '@backstage/types';
import { Config, ConfigReader } from '@backstage/config';
import { PartialCall } from '@pagerduty/pdjs/build/src/api';
import { LoggerService } from '@backstage/backend-plugin-api';
To create our Integration we'll need to implement the methods in the ConfigurableFactCollector
interface.
The ConfigurableFactCollector
interface, as discussed above in ,
extends FactCollector
and allows the integration to be updated when its configuration changes. If you have
a simpler use case that does not and will not require configurations to be loaded/updated, you can
use the FactCollector
interface instead. This tutorial covers the ConfigurableFactCollector
interface
use case.
We start by importing Entity
and a function called stringifyEntityRef
from the @backstage/catalog-model
package.
Entity
is a representation of a single entity in the Backstage ecosystem against which an Integration
can collect facts. stringifyEntityRef
is a function that takes an Entity
and returns a string
representation of it, this is used commonly in Soundcheck to represent entities in situations where
sending the entire Entity is undesired.
We also import the Config
type from the @backstage/config
package. This is used to parse the JSON
object that is passed to the Integration
in setConfig(), and from which our Integration
can
then pull its own sub-configuration.
We next import the api
function from the @pagerduty/pdjs
package. This is the function we will use to communicate
with the PagerDuty API.
The final import lines pull in common types from the @spotify/backstage-plugin-soundcheck-common
package, as well as some utility functions.
Now, we'll define some constants:
const ID = 'example_pagerduty';
const SCOPE = 'default';
const SERVICE_FACT_REFERENCE = `${ID}:${SCOPE}/service`;
The ID
is the identifier for this Integration
. It is used to uniquely identify the integration.
SCOPE
is a reserved word in Soundcheck and should, for now, be set to 'default'. Some integrations
support scope, for instance the SCM integration uses scope to pull from different SCM branches.
The SERVICE_FACT_REFERENCE
constant is a string that represents a complete fact reference in the
'collector-scope-factName' format. This is the type of fact that is returned by this integration.
Next, add the following constant to the file:
/**
* Sent to the pagerduty API to get the service details.
*/
const SERVICE_DETAILS = [
'escalation_policies',
'teams',
'auto_pause_notifications_parameters',
'integrations',
];
SERVICE_DETAILS
is an array of strings that represent the details we want to pull from PagerDuty.
With our constants defined and our imports all set, we can begin to define our ExamplePagerDutyFactCollector
in earnest. Let's begin by creating the ExamplePagerDutyFactCollector
class and having it implement the
ConfigurableFactCollector
interface:
/**
* An example integration with PagerDuty.
*/
export class ExamplePagerDutyFactCollector
implements ConfigurableFactCollector
{
id = ID;
name = 'Example PagerDuty Integration';
description = 'PagerDuty integration!';
/**
* The configuration given to this collector.
*/
#rawConfig?: JsonValue;
/**
* The reader for the configuration.
* @private
*/
#config?: Config;
/**
* The PagerDuty API client.
* @private
*/
#pd?: PartialCall;
readonly #logger: LoggerService;
constructor(logger: LoggerService) {
this.#logger = logger.child({
target: this.id,
});
}
/**
* Collects the requested facts for the given entities.
* @param entities
* @param params
*/
async collect(
entities: Entity[],
params?: {
factRefs?: FactRef[];
refresh?: FactRef[];
},
): Promise<Fact[]> {
throw new Error('Method not implemented.');
}
}
Excellent, we've defined our new Integration
, and given it an id
, name
, and description
, as well
as defining a few fields for the logger
, config
, and the PagerDuty API client.
Let's get into the heart of the collector, the collect method. Replace the scaffolded collect
method
with the following:
/**
* Collects the requested facts for the given entities.
* @param entities
* @param params
*/
async collect(
entities: Entity[],
params?: {
factRefs?: FactRef[];
refresh?: FactRef[];
},
): Promise<Fact[]> {
// The facts that we'll return
const facts: Fact[] = [];
// If this collector supported multiple facts, we could switch on which facts to collect
// based on the factRefs parameter. Since we only support one fact, we'll just collect it
// (see below) and log a warning if the requested factRefs are not supported.
if(params?.factRefs) {
if(!params.factRefs.map(value => stringifyFactRef(value)).includes(SERVICE_FACT_REFERENCE)) {
this.#logger.warn('Unsupported factRefs requested in PagerDutyFactCollector');
return [];
}
}
// Likewise, if this collector supported refreshing facts, we would implement logic here to
// ensure that the facts we're collecting aren't served from any sort of (local) cache (separate
// from the fact cache that Soundcheck uses). Since we don't support refreshing, we'll just log
// a warning if the refresh parameter is set.
if(params?.refresh) {
this.#logger.warn('Refresh is not supported for this collector');
}
// Collect for each entity.
for (const entity of entities) {
const fact = await this.#collectServiceFact(entity);
if (fact) {
facts.push(fact);
}
}
return facts;
}
The arguments to the collect method are entities
and params
. Entities
is an array of entities for
which we want to collect facts. The params
parameter tells
the integration what facts to collect and if they should be fetched freshly rather than from any sort
of cache provided by the integration itself. This does not impact fact caching provided by Soundcheck.
The collect
method is where the Integration does its work. For all entities, and for any fact references
provided by the params
argument, the collector must collect and return the facts for the entities.
Above, we are calling to a private method, #collectServiceFact
, to collect the service details for
each entity. We add the fact for each entity to the facts array and return it.
Now let's implement the #collectServiceFact
function:
/**
* Collects the service details for the given entity.
* @param entity
* @private
*/
async #collectServiceFact(entity: Entity): Promise<Fact | undefined> {
// Note: This collector will only collect facts for entities which have
// this annotation. If the entity does not define a service-id, we will not collect a fact.
// This is how the entity (probably a production component) is associated with a PagerDuty service.
const serviceId = entity.metadata.annotations?.['pagerduty.com/service-id'];
if (serviceId) {
const service = await this.#getService(serviceId, SERVICE_DETAILS);
if (service) {
// Return the collected data in the shape of a Fact:
return {
factRef: SERVICE_FACT_REFERENCE,
entityRef: stringifyEntityRef(entity),
data: service,
timestamp: DateTime.utc().toISO()!,
};
}
}
return undefined;
}
The #collectServiceFact
function takes an entity
and returns a fact
for that entity. It first checks
that the entity is annotated with a pagerduty.com/service-id
annotation, which is the PagerDuty
service id associated with the entity. This means that this Integration
will only collect facts
from those entities that have this annotation. If the entity has the annotation, we call the #getService
function, and return a fact for the entity with the service details as the fact's data.
Let's implement the #getService
function next:
/**
* Fetches the service details for the given service ID.
* @param serviceId
* @param include
* @private
*/
async #getService(serviceId: string, include?: string[]): Promise<any> {
// If our configuration wasn't set or didn't include a PD token, we can't make the request.
if(!this.#pd)
return undefined
const query = qs.stringify(
{
'include[]': include,
},
{
indices: false,
},
);
const { data } = await this.#pd.get(`/services/${serviceId}?${query}`);
return data?.service;
}
This function takes a serviceId
and an optional array of strings called include
, which is specific
to the PagerDuty API and informs PagerDuty what information we want about the service. The #getService
function constructs a query string from the include
array and calls to the PagerDuty API to get the
service details for the ID associated with the entity. It then returns the service details.
That wraps up our collect()
function.
The next functions we need to implement are getFactNames
, getDataSchema
, and getCollectionConfigs
.
Let's start with the simplest, getFactNames
:
/**
* Return the names of the facts that this collector can collect. In this example, we only support
* one fact, so we return that fact's reference.
*/
async getFactNames(): Promise<string[]> {
return [SERVICE_FACT_REFERENCE];
}
This method returns an array of strings representing the fact references that this Integration can collect. In this case, we only collect one type of fact, so we return an array with a single element. This method is used by Soundcheck's frontend to determine what facts are available when creating a Check via the Soundcheck No-Code User Interface (NCUI).
Next, let's implement getDataSchema
:
/**
* Return the data schema for the requested fact. This is a JSON schema that describes the shape of
* the data that will be returned for the fact. In this example we're just returning a static schema
* that describes (some of) the shape of the data that we're collecting for the single supported
* pagerduty fact.
*
* This is used by the front-end to display path options for the selected fact.
* If the entire shape of the fact is not represented, it only means that any excluded paths
* won't show up in the front end, you can still use any data within the fact in your checks.
*
* @param factName the name of the fact for which to return the schema
*/
async getDataSchema(factName: FactRef): Promise<string | undefined> {
// Given that this collector only supports a single fact, let's ensure that the 'service'
// fact is being requested, and if so, respond with the schema.
if(factName === SERVICE_FACT_REFERENCE.split('/')[1]) {
return JSON.stringify({
"title": "Service Details",
"description": "Pager Duty Service Details",
"type": "object",
"properties": {
'id': {
type: "string"
},
'name': {
type: "string"
},
'description': {
type: "string"
},
'status': {
type: "string"
},
'escalation_policies': {
type: "object"
},
'teams': {
type: "array"
},
'acknowledgement_timeout': {
type: "number"
},
'integrations':
{
type: "object"
}
}
});
}
return undefined;
}
The getDataSchema
method returns a JSON schema describing the data structure of the fact. This is used
in Soundcheck's NCUI to provide paths for a selected fact when creating a new check. In this case, we
return a schema that describes (some of) the structure of the service details fact that we collect from PagerDuty.
Note that this schema is not the full schema of the data returned by the PagerDuty API, but rather a
subset for the sake of brevity and demonstration.
With the getdataSchema
and getFactNames
methods implemented, we'll be able to see the service
details fact in the NCUI when creating a new check:
Next, let's implement getCollectionConfigs
:
/**
* Return a CollectionConfig that describes the fact that we collect.
*
* This is used by Soundcheck to schedule the collection of facts for all
* configurations which have a 'frequency' field.
*
* A collection config is defined as:
*
* type CollectionConfig = {
* factRefs?: FactRef[];
* filter?: EntityFilter;
* exclude?: EntityFilter;
* frequency?:
* | {
* cron: string;
* }
* | HumanDuration;
* initialDelay?: Duration | HumanDuration;
* batchSize?: number;
* cache?: CacheConfig;
* };
*
*/
async getCollectionConfigs(): Promise<CollectionConfig[]> {
// Nothing to collect if we don't have a config or if there are no collects configured.
const collects = this.#config?.getConfigArray('collects');
if(this.#config === undefined || !collects || collects?.length === 0) {
return [];
}
// Use any defaults in the configuration for this collector to apply to all Collect configurations:
return collects.map((collect: Config) => {
const collectionConfig = collect;
return {
factRefs: [SERVICE_FACT_REFERENCE], // We only support one fact.
filter: collectionConfig.getOptional('filter') ?? this.#config?.getOptional('filter') ?? undefined,
exclude: collectionConfig.getOptional('exclude') ?? this.#config?.getOptional('exclude') ?? undefined,
frequency: collectionConfig.getOptional('frequency') ?? this.#config?.getOptional('frequency') ?? undefined,
initialDelay: collectionConfig.getOptional('initialDelay') ?? this.#config?.getOptional('initialDelay') ?? undefined,
batchSize: collectionConfig.getOptional('batchSize') ?? this.#config?.getOptional('batchSize') ?? undefined,
cache: collectionConfig.getOptional('cache') ?? this.#config?.getOptional('cache') ?? undefined
}
}
);
}
The getCollectionConfigs
method returns an array of CollectionConfig
objects. This method is used by
Soundcheck to determine how often the collector should collect facts, what facts it should collect,
and for which entities it should collect facts. Above, defaults are applied to all collect configurations
if they are not specified in the individual collect configurations.
Finally, let's implement the methods of the ConfigurableFactCollector
interface:
/**
* Return the configuration for this collector. This is a JSON object that describes the configuration
* for the collector, so we'll return the configuration that was last passed to the collector.
*/
getConfig(): Promise<JsonValue | undefined> {
return this.#rawConfig ? Promise.resolve(this.#rawConfig) : Promise.resolve(undefined);
}
/**
* Return the configuration schema for this collector. This is a JSON schema that describes the shape
* of the configuration that this collector expects.
*
* Typically, we'd have a Zod schema that's also used to validate the config sent to setConfig()
* and we'd return something like 'JSON.stringify(zodToJsonSchema(ExamplePagerDutyConfigSchema))'
* but for this example, we'll just return a tiny portion of the schema for brevity.
*/
getConfigSchema(): Promise<string> {
return Promise.resolve(JSON.stringify({
type: 'object',
properties: {
token: {
type: 'string',
title: 'PagerDuty API Token',
description: 'The API token for the Pager Duty API',
}
},
required: ['token'],
}))
}
/**
* Set the configuration for this collector. This is called by Soundcheck on initialization and
* when the configuration for the collector is updated.
*
* Typically, you'd validate the configuration with a Zod schema here, but for this example, we'll
* just store the configuration and log a warning if the token is missing, even though collection
* and scheduling still depend on the configuration.
*
* @param config
*/
setConfig(config: JsonValue): Promise<void> {
this.#rawConfig = config;
this.#config = new ConfigReader(config as JsonObject);
if(!this.#config) {
this.#logger.warn('No configuration provided for ExamplePagerDutyFactCollector');
}
const token = this.#config.getOptionalString('token');
if(token === undefined){
this.#logger.warn('No token provided for ExamplePagerDutyFactCollector, facts cannot be collected.');
}
this.#pd = api({ token });
return Promise.resolve();
}
Wrapping Up the ExamplePagerDutyFactCollector
As it stands, the code snippets provided above and tagged with 'ExamplePagerDutyFactCollector.ts' are enough
to create a working ExamplePagerDutyFactCollector
, but we have a bit of configuration left to make use of it.
Let's do that next.
Updating the backend module
In the same folder as our ExamplePagerDutyFactCollector.ts
file is a module.ts
file that we need
to make some changes to. Open the module.ts
file and replace its entire contents with the following:
import {
createBackendModule,
coreServices,
} from '@backstage/backend-plugin-api';
import {
checkTemplateExtensionPoint,
factCollectionExtensionPoint,
} from '@spotify/backstage-plugin-soundcheck-node';
import { ExamplePagerDutyFactCollector } from './ExamplePagerDutyFactCollector';
/** @public */
export const soundcheckModuleExamplePagerduty = createBackendModule({
pluginId: 'soundcheck',
moduleId: 'example_pagerduty',
register(env) {
env.registerInit({
deps: {
cache: coreServices.cache,
config: coreServices.rootConfig,
logger: coreServices.logger,
collectorsExtension: factCollectionExtensionPoint,
templatesExtension: checkTemplateExtensionPoint,
},
async init({ logger, collectorsExtension }) {
collectorsExtension.addFactCollector(
new ExamplePagerDutyFactCollector(logger),
);
},
});
},
});
What this does is create a new backend module that uses the factCollectionExtensionPoint
's
addFactCollector
method to add our new ExamplePagerDutyFactCollector
.
Install the new module into the Backstage app
Now that we've created our new Integration
, we need to install it into our Backstage app.
In backend-next's index.ts file, add this line:
backend.add(
import(
'@spotify/backstage-plugin-soundcheck-backend-module-example-pagerduty'
),
);
This will tell the DI system to inject our new module into the backend.
Finally, add a dependency on the new module to the package.json
file in backend-next:
"dependencies": {
...
"@spotify/backstage-plugin-soundcheck-backend-module-example-pagerduty": "workspace:^",
...
}
And run one final yarn install to make sure everything is set up correctly:
yarn install
We're all set! Let's get to using our new integration.
Using the ExamplePagerDutyFactCollector
Entity Configuration
Add the necessary metadata annotation to the catalog-info.yaml
file of an entity to allow the
plugin to map an entity to a service in PagerDuty.
metadata:
annotations:
pagerduty.com/service-id: pd-test-service-id #replace with your service ID
app-config.yaml configuration
Add the following to the collectors
section under soundcheck
in your app-config.yaml
file,
and please note that you'll need your own PagerDuty API token:
soundcheck:
collectors:
example_pagerduty:
token: <redacted> # Token used to authenticate to PagerDuty to pull service details.
# Defaults for the collect configuration(s).
frequency:
minutes: 5
initialDelay:
seconds: 15
filter:
kind: Component
spec.lifecycle: production
# The 'collects' configurations specify what facts the integration should collect.
collects:
# Collect the 'Service' fact. Uses all defaults defined above.
- type: Service
# Also collect the 'Service' fact for all components with the 'redacted1' tag, but only every 30 minutes.
# Will use the same initial delay specified in the defaults above.
- type: Service
frequency:
minutes: 30
filter:
kind: Component
metadata.tags: ['redacted1']
Feel free to simplify the above by deleting the second 'collects' configuration,
which is meant to show the flexibility of the configuration options.
The first collect configuration uses the defaults set in the example_pagerduty
configuration,
while the second collect configuration overrides the frequency and filter, only collecting
for those components with the specified tags every 30 minutes.
Create Checks and Tracks
To use our new integration, we'll need to define a Soundcheck Track as well as a Soundcheck Check to
analyze the Facts collected by our new PagerDuty Integration. Let's start with the Check which
we'll define in yaml. Go ahead and add this Check to your app-config.yaml
or app-config.local.yaml
file:
soundcheck:
checks:
- id: requires_service_status_to_be_active
description: Requires service status to be active
passedMessage: The check has passed!
failedMessage: The check has failed!
rule:
factRef: example_pagerduty:default/service
path: $.status
operator: equal
value: active
The check above is a simple example of checks that can be used to check the data collected by the
PagerDuty Integration. The check ensures that the status of the service is active. Note we did not
schedule the check. This is because Soundcheck will automatically trigger the check when the facts
upon which it depends are collected, and so this check will be run whenever our ExamplePagerDutyFactCollector
collects the service details, on the schedule we defined for it in the app-config.yaml
file.
Now, let's define a Soundcheck Track that uses our new PagerDuty Integration and the check we just
defined. Go ahead and add the following to your app-config.yaml
or app-config.local.yaml
file as well:
soundcheck:
programs:
- id: pager_duty_track
name: Pager Duty
ownerEntityRef: <an owner entity reference in your organization, like: group:default/backstage>
description: >
Ensure that your component is properly set up to use PagerDuty for incident
management.
filter:
catalog:
spec.lifecycle: 'production'
levels:
- ordinal: 1
name: Demonstration of Soundcheck PagerDuty Fact Collector
description: Checks leveraging SoundCheck's PagerDuty Fact Collector
checks:
- id: requires_service_status_to_be_active
name: Requires service status to be active
description: Requires service status to be active
The simple Track defined above has one level, with the check we defined earlier. Start your Backstage instance and navigate to the Soundcheck tab to see your new Track. Note that in the example Track, we've filtered so that the Track only applies to components with production lifecycle.
Here's what the Track looks like in the Soundcheck UI:
Note that we configured our PagerDuty Fact Collector to run every five minutes, and every
30 minutes for the redacted1
tagged components. Once the collector
collects the fact, you'll see the Checks run and update the Track for your components:
That's it! Your new PagerDuty Integration is up and running!