-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add log links to alarm chat messages #2487
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,3 +1,4 @@ | ||||||||||||||||||||||||
import { groupBy } from '@modules/arrayFunctions'; | ||||||||||||||||||||||||
import { getIfDefined } from '@modules/nullAndUndefined'; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
type Team = 'VALUE' | 'GROWTH' | 'PORTFOLIO' | 'PLATFORM' | 'SRE'; | ||||||||||||||||||||||||
|
@@ -17,7 +18,8 @@ const sharedMobilePurchasesApps = [ | |||||||||||||||||||||||
'mobile-purchases-google-update-subscriptions', | ||||||||||||||||||||||||
]; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const teamToAppMappings: Record<Team, string[]> = { | ||||||||||||||||||||||||
type AppInfo = string | { app: string; logGroups: string[] }; | ||||||||||||||||||||||||
export const teamToAppMappings: Record<Team, AppInfo[]> = { | ||||||||||||||||||||||||
GROWTH: [ | ||||||||||||||||||||||||
'acquisition-events-api', | ||||||||||||||||||||||||
'admin-console', | ||||||||||||||||||||||||
|
@@ -71,11 +73,17 @@ const teamToAppMappings: Record<Team, string[]> = { | |||||||||||||||||||||||
'zuora-creditor', | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// support-frontend | ||||||||||||||||||||||||
'frontend', | ||||||||||||||||||||||||
{ app: 'frontend', logGroups: ['support-frontend'] }, | ||||||||||||||||||||||||
'it-test-runner', | ||||||||||||||||||||||||
'stripe-intent', | ||||||||||||||||||||||||
'workers', | ||||||||||||||||||||||||
'payment-api', | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
app: 'workers', | ||||||||||||||||||||||||
logGroups: [ | ||||||||||||||||||||||||
'/aws/lambda/CreatePaymentMethod', | ||||||||||||||||||||||||
'/aws/lambda/CreateZuoraSubscription', //etc | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in future it might be useful to have a link to the state machine executions logs also, rather than just to the lambdas. |
||||||||||||||||||||||||
], | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
{ app: 'payment-api', logGroups: ['support-payment-api'] }, | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// support-service-lambdas | ||||||||||||||||||||||||
'digital-voucher-suspension-processor', | ||||||||||||||||||||||||
|
@@ -107,33 +115,80 @@ const teamToAppMappings: Record<Team, string[]> = { | |||||||||||||||||||||||
], | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const buildAppToTeamMappings = (): Record<string, Team[]> => { | ||||||||||||||||||||||||
const mappings: Record<string, Team[]> = {}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
for (const [team, apps] of Object.entries(teamToAppMappings)) { | ||||||||||||||||||||||||
for (const app of apps) { | ||||||||||||||||||||||||
const teams = mappings[app] ?? []; | ||||||||||||||||||||||||
teams.push(team as Team); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
mappings[app] = teams; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
export class AlarmMappings { | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this into a class so I could use simplified mappings in the tests more easily There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With Typescript you can use both OOP or functional style but at the Guardian we usually use a functional style with typescript. So in this scenario I am not sure about the benefits or changing this part of the code to use a class since the PR does not change the behaviour of this part of the code (how mappings work). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok yes thanks for that, I get super confused between all the different ways to write a function/method. The idea behind making it parameterised rather than a singleton was so I could write a test that used mocked data rather than having to use the real data (which might get edited). |
||||||||||||||||||||||||
constructor(mappings: Record<string, AppInfo[]> = teamToAppMappings) { | ||||||||||||||||||||||||
this.appToTeamMappings = this.buildAppToTeamMappings(mappings); | ||||||||||||||||||||||||
this.appToLogGroupOverrides = this.buildAppToLogGroupOverrides(mappings); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
return mappings; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const appToTeamMappings: Record<string, Team[]> = buildAppToTeamMappings(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export const getTeams = (appName?: string): Team[] => { | ||||||||||||||||||||||||
if (appName && appToTeamMappings[appName]) { | ||||||||||||||||||||||||
return appToTeamMappings[appName] as Team[]; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
private buildAppToTeamMappings = ( | ||||||||||||||||||||||||
theMappings: Record<Team, AppInfo[]>, | ||||||||||||||||||||||||
): Record<string, Team[]> => { | ||||||||||||||||||||||||
const entries: Array<[Team, AppInfo[]]> = Object.entries( | ||||||||||||||||||||||||
theMappings, | ||||||||||||||||||||||||
) as Array<[Team, AppInfo[]]>; // `as` - hmm? | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for some reason calling Object.entries on a Record<Team, AppInfo[]> gives Array<[string, AppInfo[]]> so I had to give the type system the info back again. Not sure if this is the safest way of doing it though? |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
const teamToApp: Array<{ app: string; team: Team }> = entries.flatMap( | ||||||||||||||||||||||||
([team, appInfos]) => | ||||||||||||||||||||||||
appInfos.map((appInfo) => { | ||||||||||||||||||||||||
const app = typeof appInfo === 'string' ? appInfo : appInfo.app; | ||||||||||||||||||||||||
return { team, app }; | ||||||||||||||||||||||||
}), | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
const groups = groupBy(teamToApp, ({ app }) => app); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const mappings: Record<string, Team[]> = Object.fromEntries( | ||||||||||||||||||||||||
Object.entries(groups).map(([app, info]) => [ | ||||||||||||||||||||||||
app, | ||||||||||||||||||||||||
info.map(({ team }) => team), | ||||||||||||||||||||||||
]), | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tempted to make an object mapValues function - if there isn't already such a thing?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above - I am not sure why this needs changing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was because I changed it from a simple team->app mapping to a team->(app|{app, loglink[]}) mapping so I could extract the log links. |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
return mappings; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private buildAppToLogGroupOverrides = ( | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if an app is in multiple teams with different log groups, it will just pick one arbitrarily. |
||||||||||||||||||||||||
theMappings: Record<Team, AppInfo[]>, | ||||||||||||||||||||||||
): Record<string, string[]> => { | ||||||||||||||||||||||||
return Object.fromEntries( | ||||||||||||||||||||||||
Object.values(theMappings) | ||||||||||||||||||||||||
.flatMap((appInfos) => appInfos) | ||||||||||||||||||||||||
.flatMap((appInfo) => | ||||||||||||||||||||||||
typeof appInfo !== 'string' ? [[appInfo.app, appInfo.logGroups]] : [], | ||||||||||||||||||||||||
), | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private appToTeamMappings: Record<string, Team[]>; | ||||||||||||||||||||||||
private appToLogGroupOverrides: Record<string, string[]>; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
getTeams = (appName?: string): Team[] => { | ||||||||||||||||||||||||
if (appName && this.appToTeamMappings[appName]) { | ||||||||||||||||||||||||
return this.appToTeamMappings[appName] as Team[]; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return ['SRE']; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
return ['SRE']; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
getTeamWebhookUrl = (team: Team): string => { | ||||||||||||||||||||||||
return getIfDefined<string>( | ||||||||||||||||||||||||
process.env[`${team}_WEBHOOK`], | ||||||||||||||||||||||||
`${team}_WEBHOOK environment variable not set`, | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
getLogGroups = (appName: string, stage: string): string[] => { | ||||||||||||||||||||||||
// currently we assume the log group is /aws/lambda/<app>-<stage>, we can add overrides to the appToTeamMappings later | ||||||||||||||||||||||||
const logGroup = this.appToLogGroupOverrides[appName]; | ||||||||||||||||||||||||
if (logGroup === undefined) { | ||||||||||||||||||||||||
// assume it's a lambda | ||||||||||||||||||||||||
console.log('logGroup', logGroup); | ||||||||||||||||||||||||
const lambdaName = appName + '-' + stage; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const logGroupName = '/aws/lambda/' + lambdaName; | ||||||||||||||||||||||||
return [logGroupName]; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export const getTeamWebhookUrl = (team: Team): string => { | ||||||||||||||||||||||||
return getIfDefined<string>( | ||||||||||||||||||||||||
process.env[`${team}_WEBHOOK`], | ||||||||||||||||||||||||
`${team}_WEBHOOK environment variable not set`, | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
return logGroup.map((override) => override + '-' + stage); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,24 +49,25 @@ const buildCloudwatchClient = (awsAccountId: string): CloudWatchClient => { | |
return new CloudWatchClient({ region: 'eu-west-1' }); | ||
}; | ||
|
||
const getTags = async ( | ||
export type Tags = { | ||
App?: string; | ||
Stage?: string; | ||
}; | ||
|
||
export const getTags = async ( | ||
alarmArn: string, | ||
awsAccountId: string, | ||
): Promise<Tag[]> => { | ||
): Promise<Tags> => { | ||
const client = buildCloudwatchClient(awsAccountId); | ||
|
||
const request = new ListTagsForResourceCommand({ | ||
ResourceARN: alarmArn, | ||
}); | ||
|
||
const response = await client.send(request); | ||
return response.Tags ?? []; | ||
}; | ||
|
||
export const getAppNameTag = async ( | ||
alarmArn: string, | ||
awsAccountId: string, | ||
): Promise<string | undefined> => { | ||
const tags = await getTags(alarmArn, awsAccountId); | ||
return tags.find((tag: Tag) => tag.Key === 'App')?.Value; | ||
const tags = response.Tags ?? []; | ||
const entries = tags.flatMap((tag: Tag) => | ||
tag.Key && tag.Value ? [[tag.Key, tag.Value]] : [], | ||
); | ||
return Object.fromEntries(entries) as Tags; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice improvement! |
||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now an app can have either just a name, or it can have a name and a list of associated log groups.
In future, rather than hard coding, this could maybe be generated by a call to AWS to find all log groups associated with lambdas/ec2 instances.