From 81dad1abf840ad752cb3ea3d9d683822cf07a571 Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Thu, 22 Feb 2024 12:29:32 +0000 Subject: [PATCH] PI-1947 Report to App Insights when CAS3 officer is not in HPT team (#3319) --- .../actions/app-insights-to-slack/action.yml | 148 +++++++++ .github/workflows/reports.yml | 301 +++--------------- .../integrations/delius/ProviderService.kt | 23 +- .../integrations/delius/entity/Provider.kt | 9 +- .../delius/ProviderServiceTest.kt | 31 +- 5 files changed, 245 insertions(+), 267 deletions(-) create mode 100644 .github/actions/app-insights-to-slack/action.yml diff --git a/.github/actions/app-insights-to-slack/action.yml b/.github/actions/app-insights-to-slack/action.yml new file mode 100644 index 0000000000..9a2630670f --- /dev/null +++ b/.github/actions/app-insights-to-slack/action.yml @@ -0,0 +1,148 @@ +name: Report to Slack +description: Publish results of an Azure Monitor Application Insights query to Slack + +inputs: + title: + description: Report title + required: true + summary: + description: Report summary + required: true + event_name: + description: Comma-separated list of custom events to include in the report (e.g. Event1,Event2,Event3) + required: true + project_name: + description: Cloud role name of the service to report on + required: true + column1_header: + description: Column 1 header + default: CRN + required: true + column1_value: + description: Column 1 value from customDimensions + default: crn + required: true + column2_header: + description: Column 2 header + default: Reason + required: true + column2_value: + description: Column 2 value from customDimensions + default: reason + required: true + time_range: + description: Filter the timestamp + default: ago(7d)..now() + required: true + slack_channel: + description: Name to the Slack channel to report to + required: true + slack_token: + description: Slack token + required: true + app_insights_key: + description: Application Insights API key + required: true + app_insights_guid: + description: Application Insights GUID + required: true + app_insights_subscription_id: + description: Application Insights Subscription ID + required: true + +runs: + using: "composite" + steps: + - name: Format event name list # adds quotes for app insights e.g. 'a,b,c' -> 'a","b","c' + id: event_names + run: echo "result=$(echo "$event_name" | sed 's/,/","/g')" | tee -a "$GITHUB_OUTPUT" + shell: bash + env: + event_name: ${{ inputs.event_name }} + + - name: Search app insights + id: search + run: | + echo "result=$(curl -fsSL -H "x-api-key: $key" --data-urlencode "query=$query" --get "$url")" | tee -a "$GITHUB_OUTPUT" + shell: bash + env: + url: https://api.applicationinsights.io/v1/apps/${{ inputs.app_insights_guid }}/query + key: ${{ inputs.app_insights_key }} + query: | + customEvents + | where timestamp between (${{ inputs.time_range }}) + | where cloud_RoleName in ("${{ inputs.project_name }}") + | where name in ("${{ steps.event_names.outputs.result }}") + | project customDimensions.${{ inputs.column1_value }}, customDimensions.${{ inputs.column2_value }}, itemId, timestamp + | order by tostring(customDimensions_${{ inputs.column1_value }}) asc + + - name: Transform results into Slack message + id: transform + if: fromJson(steps.search.outputs.result).tables[0].rows[0] != null + shell: bash + run: | + echo "result=$(echo "$search_result" | jq -rc '.tables[0].rows | + { + "blocks": ([ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":information_source: ${{ inputs.title }}" + } + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "${{ inputs.summary }}" + }, + "fields": [ + { + "type": "mrkdwn", + "text": "*${{ inputs.column1_header }}*" + }, + { + "type": "mrkdwn", + "text": "*${{ inputs.column2_header }}*" + } + ] + } + ] + + (. | [_nwise(5)] | map({ + "type": "section", + "fields": (. | map([ + { + type: "plain_text", + text: .[0] + }, + { + type: "mrkdwn", + text: (.[1] + " ().") + } + ]) | flatten) + })) + + [ + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": ">This report was generated automatically. For more information, contact the ." + } + ] + } + ]) + }')" | tee -a "$GITHUB_OUTPUT" + env: + search_result: ${{ steps.search.outputs.result }} + + - name: Send message to Slack + id: send + if: steps.transform.outputs.result != null + uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + with: + channel-id: ${{ inputs.slack_channel }} + payload: ${{ steps.transform.outputs.result }} + env: + SLACK_BOT_TOKEN: ${{ inputs.slack_token }} diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 7be08b7108..f1dee85e08 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -9,263 +9,66 @@ jobs: allocation-failures: runs-on: ubuntu-latest steps: - - name: Search app insights - id: search - run: | - echo "result=$(curl -fsSL -H "x-api-key: $key" --data-urlencode "query=$query" --get "$url")" | tee -a "$GITHUB_OUTPUT" - env: - url: https://api.applicationinsights.io/v1/apps/${{ secrets.APP_INSIGHTS_APPLICATION_GUID }}/query - key: ${{ secrets.APP_INSIGHTS_API_KEY }} - query: | - customEvents - | where timestamp > ago(7d) and timestamp < now() - | where cloud_RoleName in ("workforce-allocations-to-delius") - | where name in ("AllocationFailed") - | project customDimensions.crn, customDimensions.reason, itemId, timestamp - | order by tostring(customDimensions_crn) asc - - - name: Transform results into Slack message - id: transform - if: fromJson(steps.search.outputs.result).tables[0].rows[0] != null - run: | - echo "result=$(echo "$search_result" | jq -rc '.tables[0].rows | - { - "blocks": ([ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":information_source: Allocation Failures" - } - }, - { - "type": "section", - "text": { - "type": "plain_text", - "text": "The following cases failed to allocate in Delius over the last 7 days. Please check and manually correct the cases if required." - }, - "fields": [ - { - "type": "mrkdwn", - "text": "*CRN*" - }, - { - "type": "mrkdwn", - "text": "*Reason*" - } - ] - } - ] + - (. | [_nwise(5)] | map({ - "type": "section", - "fields": (. | map([ - { - type: "plain_text", - text: .[0] - }, - { - type: "mrkdwn", - text: (.[1] + " ().") - } - ]) | flatten) - })) + - [ - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": ">This report was generated automatically. For more information, contact the probation integration team." - } - ] - } - ]) - }')" | tee -a "$GITHUB_OUTPUT" - env: - search_result: ${{ steps.search.outputs.result }} - - - name: Send message to Slack - id: send - if: steps.transform.outputs.result != null - uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + - uses: actions/checkout@v4 + - uses: ./.github/actions/app-insights-to-slack with: - channel-id: topic-pi-workforce-allocation - payload: ${{ steps.transform.outputs.result }} - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + title: Allocations Integration Report + summary: The following cases failed to allocate in Delius over the last 7 days. Please check and manually correct the cases if required. + event_name: AllocationFailed + project_name: workforce-allocations-to-delius + slack_channel: topic-pi-workforce-allocation + slack_token: ${{ secrets.SLACK_BOT_TOKEN }} + app_insights_key: ${{ secrets.APP_INSIGHTS_API_KEY }} + app_insights_guid: ${{ secrets.APP_INSIGHTS_APPLICATION_GUID }} + app_insights_subscription_id: ${{ secrets.APP_INSIGHTS_SUBSCRIPTION_ID }} - approved-premises-failures: + cas1-failures: runs-on: ubuntu-latest steps: - - name: Search app insights - id: search - run: | - echo "result=$(curl -fsSL -H "x-api-key: $key" --data-urlencode "query=$query" --get "$url")" | tee -a "$GITHUB_OUTPUT" - env: - url: https://api.applicationinsights.io/v1/apps/${{ secrets.APP_INSIGHTS_APPLICATION_GUID }}/query - key: ${{ secrets.APP_INSIGHTS_API_KEY }} - query: | - customEvents - | where timestamp > ago(7d) and timestamp < now() - | where cloud_RoleName in ("approved-premises-and-delius") - | where name in ("ApprovedPremisesFailureReport") - | project customDimensions.crn, customDimensions.reason, itemId, timestamp - | order by tostring(customDimensions_crn) asc - - - name: Transform results into Slack message - id: transform - if: fromJson(steps.search.outputs.result).tables[0].rows[0] != null - run: | - echo "result=$(echo "$search_result" | jq -rc '.tables[0].rows | - { - "blocks": ([ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":information_source: Approved Premises Processing Failures" - } - }, - { - "type": "section", - "text": { - "type": "plain_text", - "text": "The following cases failed to process in Delius over the last 7 days. Please check and manually correct the cases if required." - }, - "fields": [ - { - "type": "mrkdwn", - "text": "*CRN*" - }, - { - "type": "mrkdwn", - "text": "*Reason*" - } - ] - } - ] + - (. | [_nwise(5)] | map({ - "type": "section", - "fields": (. | map([ - { - type: "plain_text", - text: .[0] - }, - { - type: "mrkdwn", - text: (.[1] + " ().") - } - ]) | flatten) - })) + - [ - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": ">This report was generated automatically. For more information, contact the probation integration team." - } - ] - } - ]) - }')" | tee -a "$GITHUB_OUTPUT" - env: - search_result: ${{ steps.search.outputs.result }} + - uses: actions/checkout@v4 + - uses: ./.github/actions/app-insights-to-slack + with: + title: Approved Premises (CAS1) Integration Report + summary: The following cases failed to process in Delius over the last 7 days. Please check and manually correct the cases if required. + event_name: ApprovedPremisesFailureReport + project_name: approved-premises-and-delius + slack_channel: topic-pi-approved-premises + slack_token: ${{ secrets.SLACK_BOT_TOKEN }} + app_insights_key: ${{ secrets.APP_INSIGHTS_API_KEY }} + app_insights_guid: ${{ secrets.APP_INSIGHTS_APPLICATION_GUID }} + app_insights_subscription_id: ${{ secrets.APP_INSIGHTS_SUBSCRIPTION_ID }} - - name: Send message to Slack - id: send - if: steps.transform.outputs.result != null - uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + cas3-warnings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/app-insights-to-slack with: - channel-id: topic-pi-approved-premises - payload: ${{ steps.transform.outputs.result }} - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + title: Transitional Accommodation (CAS3) HPT Report + summary: The following messages were generated by the CAS3 and Delius integration service over the last 7 days. Please check and manually correct the cases if required. + event_name: StaffNotInTeam + column1_header: Staff Code + column1_value: staffCode + project_name: cas3-and-delius + slack_channel: topic-pi-approved-premises + slack_token: ${{ secrets.SLACK_BOT_TOKEN }} + app_insights_key: ${{ secrets.APP_INSIGHTS_API_KEY }} + app_insights_guid: ${{ secrets.APP_INSIGHTS_APPLICATION_GUID }} + app_insights_subscription_id: ${{ secrets.APP_INSIGHTS_SUBSCRIPTION_ID }} refer-and-monitor-failures: runs-on: ubuntu-latest steps: - - name: Search app insights - id: search - run: | - echo "result=$(curl -fsSL -H "x-api-key: $key" --data-urlencode "query=$query" --get "$url")" | tee -a "$GITHUB_OUTPUT" - env: - url: https://api.applicationinsights.io/v1/apps/${{ secrets.APP_INSIGHTS_APPLICATION_GUID }}/query - key: ${{ secrets.APP_INSIGHTS_API_KEY }} - query: | - customEvents - | where timestamp > ago(7d) and timestamp < now() - | where cloud_RoleName in ("refer-and-monitor-and-delius") - | where name in ("ReferAndMonitorFailureReport") - | project customDimensions.crn, customDimensions.message, itemId, timestamp - | order by tostring(customDimensions_crn) asc - - - name: Transform results into Slack message - id: transform - if: fromJson(steps.search.outputs.result).tables[0].rows[0] != null - run: | - echo "result=$(echo "$search_result" | jq -rc '.tables[0].rows | - { - "blocks": ([ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":information_source: Refer and Monitor Processing Failures" - } - }, - { - "type": "section", - "text": { - "type": "plain_text", - "text": "The following cases failed to process in Delius over the last 7 days. Please check and manually correct the cases if required." - }, - "fields": [ - { - "type": "mrkdwn", - "text": "*CRN*" - }, - { - "type": "mrkdwn", - "text": "*Reason*" - } - ] - } - ] + - (. | [_nwise(5)] | map({ - "type": "section", - "fields": (. | map([ - { - type: "plain_text", - text: .[0] - }, - { - type: "mrkdwn", - text: (.[1] + " ().") - } - ]) | flatten) - })) + - [ - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": ">This report was generated automatically. For more information, contact the probation integration team." - } - ] - } - ]) - }')" | tee -a "$GITHUB_OUTPUT" - env: - search_result: ${{ steps.search.outputs.result }} - - - name: Send message to Slack - id: send - if: steps.transform.outputs.result != null - uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + - uses: actions/checkout@v4 + - uses: ./.github/actions/app-insights-to-slack with: - channel-id: topic-pi-referandmonitor - payload: ${{ steps.transform.outputs.result }} - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} \ No newline at end of file + title: Refer and Monitor Integration Report + summary: The following cases failed to process in Delius over the last 7 days. Please check and manually correct the cases if required. + event_name: ReferAndMonitorFailureReport + column2_value: message + project_name: refer-and-monitor-and-delius + slack_channel: topic-pi-referandmonitor + slack_token: ${{ secrets.SLACK_BOT_TOKEN }} + app_insights_key: ${{ secrets.APP_INSIGHTS_API_KEY }} + app_insights_guid: ${{ secrets.APP_INSIGHTS_APPLICATION_GUID }} + app_insights_subscription_id: ${{ secrets.APP_INSIGHTS_SUBSCRIPTION_ID }} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderService.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderService.kt index 0d3cc6bda6..19a6ea7dff 100644 --- a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderService.kt +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderService.kt @@ -2,21 +2,30 @@ package uk.gov.justice.digital.hmpps.integrations.delius import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.integrations.approvedpremesis.By -import uk.gov.justice.digital.hmpps.integrations.delius.entity.ManagerIds -import uk.gov.justice.digital.hmpps.integrations.delius.entity.ProviderRepository -import uk.gov.justice.digital.hmpps.integrations.delius.entity.StaffRepository -import uk.gov.justice.digital.hmpps.integrations.delius.entity.getByCode +import uk.gov.justice.digital.hmpps.integrations.delius.entity.* +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService @Service class ProviderService( private val providerRepository: ProviderRepository, - private val staffRepository: StaffRepository + private val staffRepository: StaffRepository, + private val teamRepository: TeamRepository, + private val telemetryService: TelemetryService, ) { fun findManagerIds(by: By): ManagerIds { val provider = providerRepository.getByCode(by.probationRegionCode) val staff = staffRepository.getByCode(by.staffCode) - val team = staff.teams.firstOrNull { it.code == provider.homelessPreventionTeamCode() } - ?: throw IllegalStateException("Staff ${staff.code} not in Team ${provider.homelessPreventionTeamCode()}") + val team = teamRepository.getByCode(provider.homelessPreventionTeamCode()) + if (staff.teams.none { it.code == provider.homelessPreventionTeamCode() }) { + telemetryService.trackEvent( + "StaffNotInTeam", + mapOf( + "staffCode" to staff.code, + "reason" to "Officer not in team `${provider.homelessPreventionTeamCode()}`" + ) + ) + } + return object : ManagerIds { override val probationAreaId = provider.id override val teamId = team.id diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Provider.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Provider.kt index 48b82e8337..20ff747058 100644 --- a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Provider.kt +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Provider.kt @@ -69,4 +69,11 @@ interface StaffRepository : JpaRepository { } fun StaffRepository.getByCode(code: String) = - findByCode(code) ?: throw NotFoundException("Staff", "code", code) \ No newline at end of file + findByCode(code) ?: throw NotFoundException("Staff", "code", code) + +interface TeamRepository : JpaRepository { + fun findByCode(code: String): Team? +} + +fun TeamRepository.getByCode(code: String) = + findByCode(code) ?: throw NotFoundException("Team", "code", code) diff --git a/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderServiceTest.kt b/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderServiceTest.kt index 295c30441e..890d02983c 100644 --- a/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderServiceTest.kt +++ b/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ProviderServiceTest.kt @@ -1,18 +1,18 @@ package uk.gov.justice.digital.hmpps.integrations.delius import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import uk.gov.justice.digital.hmpps.data.generator.ProviderGenerator -import uk.gov.justice.digital.hmpps.data.generator.ProviderGenerator.generateTeam import uk.gov.justice.digital.hmpps.integrations.approvedpremesis.By import uk.gov.justice.digital.hmpps.integrations.delius.entity.ProviderRepository import uk.gov.justice.digital.hmpps.integrations.delius.entity.StaffRepository -import uk.gov.justice.digital.hmpps.integrations.delius.entity.getByCode +import uk.gov.justice.digital.hmpps.integrations.delius.entity.TeamRepository +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService @ExtendWith(MockitoExtension::class) class ProviderServiceTest { @@ -22,21 +22,32 @@ class ProviderServiceTest { @Mock internal lateinit var staffRepository: StaffRepository + @Mock + internal lateinit var teamRepository: TeamRepository + + @Mock + internal lateinit var telemetryService: TelemetryService + @InjectMocks internal lateinit var providerService: ProviderService @Test - fun `exception thrown if staff not in team`() { + fun `log to telemetry if staff not in team`() { val provider = ProviderGenerator.generateProvider("N01") - val staff = ProviderGenerator.generateStaff( - "N01UAT1", listOf( - generateTeam("N01UAT") - ) - ) + val expectedTeam = ProviderGenerator.generateTeam(provider.homelessPreventionTeamCode()) + val actualTeam = ProviderGenerator.generateTeam("N01UAT") + val staff = ProviderGenerator.generateStaff("N01UAT1", listOf(actualTeam)) whenever(providerRepository.findByCode(provider.code)).thenReturn(provider) + whenever(teamRepository.findByCode(expectedTeam.code)).thenReturn(expectedTeam) whenever(staffRepository.findByCode(staff.code)).thenReturn(staff) - assertThrows { providerService.findManagerIds(By(staff.code, provider.code)) } + providerService.findManagerIds(By(staff.code, provider.code)) + + verify(telemetryService).trackEvent( + "StaffNotInTeam", + mapOf("staffCode" to "N01UAT1", "reason" to "Officer not in team `N01HPT`"), + mapOf() + ) } } \ No newline at end of file