Skip to main content

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 CollectionConfigs 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 Integrations 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, Integrations 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 Integrations, 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:

Zod verification
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:

From your Backstage root directory
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".

Backstage cli

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.

Backstage cli

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:

From your Backstage root directory
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.

note

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:

ExamplePagerDutyFactCollector.ts
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.

note

The ConfigurableFactCollector interface, as discussed above in Understanding Integrations, 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:

ExamplePagerDutyFactCollector.ts
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:

ExamplePagerDutyFactCollector.ts
/**
* 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:

ExamplePagerDutyFactCollector.ts
/**
* 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:

ExamplePagerDutyFactCollector.ts
  /**
* 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:

ExamplePagerDutyFactCollector.ts
  /**
* 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:

ExamplePagerDutyFactCollector.ts
  /**
* 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:

PagerDutyFactCollector.ts
  /**
* 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:

ExamplePagerDutyFactCollector.ts
  /**
* 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:

PagerDuty Fact in NCUI

Next, let's implement getCollectionConfigs:

ExamplePagerDutyFactCollector.ts
  /**
* 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:

module.ts
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:

From your Backstage root directory
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: PagerDuty Track

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:

PagerDuty Track - Checks Executed

That's it! Your new PagerDuty Integration is up and running!