├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── activities ├── accept_email.go.html ├── accept_email.go.tmpl ├── activities.go ├── decline_email.go.html ├── decline_email.go.tmpl ├── employment_verification_request.go.html ├── employment_verification_request.go.tmpl ├── report_email.go.html └── report_email.go.tmpl ├── api ├── api.go └── types.go ├── cli ├── bgc-backend │ ├── cmd │ │ ├── api.go │ │ ├── root.go │ │ ├── ui.go │ │ └── worker.go │ └── main.go ├── bgc-candidate │ ├── cmd │ │ ├── accept.go │ │ ├── decline.go │ │ ├── flags.go │ │ └── root.go │ └── main.go ├── bgc-company │ ├── cmd │ │ ├── cancel.go │ │ ├── flags.go │ │ ├── list.go │ │ ├── root.go │ │ └── start.go │ └── main.go └── bgc-researcher │ ├── cmd │ ├── employmentverify.go │ ├── flags.go │ └── root.go │ └── main.go ├── deployment ├── grafana │ ├── Dockerfile │ ├── config.ini │ ├── dashboards │ │ ├── sdk.json │ │ └── temporal.json │ └── provisioning │ │ ├── dashboards │ │ └── all.yml │ │ └── datasources │ │ └── all.yml ├── prometheus │ └── config.yml └── thirdparty-simulator │ ├── Dockerfile │ ├── api │ └── api.go │ ├── cmd │ ├── api.go │ └── root.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── docker-compose.override.yml ├── docker-compose.yml ├── go.mod ├── go.sum ├── run-cli ├── start ├── temporal ├── client.go ├── dataconverter-server │ └── main.go └── dataconverter │ ├── crypt.go │ ├── data_converter.go │ └── propagator.go ├── ui ├── accept.go.html ├── accepted.go.html ├── declined.go.html ├── employment_verification.go.html ├── employment_verified.go.html ├── report.go.html └── ui.go ├── utils └── http.go └── workflows ├── accept.go ├── accept_test.go ├── activities.go ├── background_check.go ├── background_check_test.go ├── employment_verification.go ├── employment_verification_test.go ├── federal_criminal_search.go ├── motor_vehicle_incident_search.go ├── ssn_trace.go ├── state_criminal_search.go └── workflows.go /.dockerignore: -------------------------------------------------------------------------------- 1 | docker-compose.yml 2 | docker-compose.override.yml 3 | run-cli 4 | start 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .idea/ 3 | *.iml 4 | *.cov 5 | .tmp/ 6 | .DS_Store 7 | test 8 | test.log 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS base 2 | 3 | WORKDIR /go/src/background-checks 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | FROM base AS build 10 | 11 | COPY activities ./activities 12 | COPY api ./api 13 | COPY cli ./cli 14 | COPY temporal ./temporal 15 | COPY utils ./utils 16 | COPY ui ./ui 17 | COPY workflows ./workflows 18 | 19 | RUN go install -v ./cli/bgc-backend 20 | RUN go install -v ./cli/bgc-company 21 | RUN go install -v ./cli/bgc-candidate 22 | RUN go install -v ./cli/bgc-researcher 23 | RUN go install -v ./temporal/dataconverter-server 24 | 25 | FROM golang:1.20 AS app 26 | 27 | COPY --from=temporalio/admin-tools:1.21.1 /usr/local/bin/tctl /usr/local/bin/tctl 28 | COPY --from=temporalio/admin-tools:1.21.1 /usr/local/bin/temporal /usr/local/bin/temporal 29 | COPY --from=build /go/bin/dataconverter-server /usr/local/bin/dataconverter-server 30 | 31 | COPY --from=build /go/bin/bgc-backend /usr/local/bin/bgc-backend 32 | COPY --from=build /go/bin/bgc-company /usr/local/bin/bgc-company 33 | COPY --from=build /go/bin/bgc-candidate /usr/local/bin/bgc-candidate 34 | COPY --from=build /go/bin/bgc-researcher /usr/local/bin/bgc-researcher -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Background Check application 2 | 3 | This is an example [Temporal Application](https://docs.temporal.io/temporal#temporal-application) project. 4 | 5 | The application is written using the [Temporal Go SDK](https://github.com/temporalio/sdk-go). 6 | 7 | This project showcases how to implement a Temporal Application within the context of a [Human-Driven Long-Running Workflow](https://docs.temporal.io/learning-paths/background-checks/project-narrative#what-is-a-long-running-human-driven-workflow) use case. 8 | 9 | ## How to use the application 10 | 11 | - [Guide to using this application](https://docs.temporal.io/learning-paths/background-checks/how-to-use) 12 | 13 | ## Learn about the application 14 | 15 | - [Learning goals & project narrative](https://docs.temporal.io/learning-paths/background-checks/project-narrative) 16 | - [Application requirements](https://docs.temporal.io/learning-paths/background-checks/application-requirements) 17 | - [Application design & implementation](https://docs.temporal.io/learning-paths/background-checks/application-design) 18 | - [Application deployment](https://docs.temporal.io/learning-paths/background-checks/application-deployment) 19 | 20 | ## Application references 21 | 22 | - [CLI reference](https://docs.temporal.io/learning-paths/background-checks/cli-reference) 23 | - [API reference](https://docs.temporal.io/learning-paths/background-checks/api-reference) 24 | -------------------------------------------------------------------------------- /activities/accept_email.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Background Check Request 100 |
  • 101 |
  • Enter Personal Information
  • 102 |
  • Background Check Started
  • 103 |
104 |
105 |
106 |
107 |

Hello {{.Email}}

108 |

Your potential employer has requested that we conduct a background check on their behalf. 109 |

110 |

111 | 112 | Continue 113 | 114 |

115 |
116 |
117 | 118 | -------------------------------------------------------------------------------- /activities/accept_email.go.tmpl: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | Your potential employer has requested that we conduct a background check on their behalf. 4 | 5 | To accept this check, please visit: 6 | 7 | http://localhost:8083/candidate/{{.Token}} 8 | 9 | Thanks, 10 | 11 | Background Check System 12 | -------------------------------------------------------------------------------- /activities/activities.go: -------------------------------------------------------------------------------- 1 | package activities 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "text/template" 12 | "time" 13 | 14 | mail "github.com/xhit/go-simple-mail/v2" 15 | ) 16 | 17 | const ( 18 | HiringManagerEmail = "Hiring Manager " 19 | HiringSupportEmail = "BackgroundChecks " 20 | CandidateSupportEmail = "BackgroundChecks " 21 | ResearcherSupportEmail = "BackgroundChecks " 22 | 23 | federalCriminalSearchAPITimeout = time.Second * 5 24 | stateCriminalSearchAPITimeout = time.Second * 5 25 | ssnTraceAPITimeout = time.Second * 5 26 | ) 27 | 28 | type Activities struct { 29 | SMTPHost string 30 | SMTPPort int 31 | SMTPStub bool 32 | HTTPStub bool 33 | } 34 | 35 | type PostJSONOptions struct { 36 | Timeout time.Duration 37 | } 38 | 39 | func (a *Activities) sendMail(from string, to string, subject string, htmlTemplate *template.Template, textTemplate *template.Template, input interface{}) error { 40 | var htmlContent, textContent bytes.Buffer 41 | 42 | err := htmlTemplate.Execute(&htmlContent, input) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = textTemplate.Execute(&textContent, input) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | email := mail.NewMSG() 53 | email.SetFrom(from). 54 | AddTo(to). 55 | SetSubject(subject). 56 | SetBody(mail.TextHTML, htmlContent.String()). 57 | AddAlternative(mail.TextPlain, textContent.String()) 58 | 59 | if email.Error != nil { 60 | return email.Error 61 | } 62 | 63 | if a.SMTPStub { 64 | return nil 65 | } 66 | 67 | server := mail.NewSMTPClient() 68 | server.Host = a.SMTPHost 69 | server.Port = a.SMTPPort 70 | server.ConnectTimeout = time.Second 71 | server.SendTimeout = time.Second 72 | 73 | client, err := server.Connect() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return email.Send(client) 79 | } 80 | 81 | func (a *Activities) postJSON(ctx context.Context, url string, input interface{}, options PostJSONOptions) (*http.Response, error) { 82 | jsonInput, err := json.Marshal(input) 83 | if err != nil { 84 | return nil, fmt.Errorf("unable to encode input: %w", err) 85 | } 86 | 87 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonInput)) 88 | if err != nil { 89 | return nil, fmt.Errorf("unable to build request: %w", err) 90 | } 91 | 92 | req.Header.Set("Content-Type", "application/json") 93 | 94 | client := http.Client{ 95 | Timeout: options.Timeout, 96 | } 97 | 98 | return client.Do(req) 99 | } 100 | 101 | type FederalCriminalSearchInput struct { 102 | FullName string 103 | Address string 104 | } 105 | 106 | type FederalCriminalSearchResult struct { 107 | Crimes []string 108 | } 109 | 110 | func (a *Activities) FederalCriminalSearch(ctx context.Context, input *FederalCriminalSearchInput) (*FederalCriminalSearchResult, error) { 111 | var result FederalCriminalSearchResult 112 | 113 | if a.HTTPStub { 114 | return &result, nil 115 | } 116 | 117 | r, err := a.postJSON(ctx, "http://thirdparty:8082/federalcriminalsearch", input, PostJSONOptions{Timeout: federalCriminalSearchAPITimeout}) 118 | if err != nil { 119 | return &result, err 120 | } 121 | defer r.Body.Close() 122 | 123 | if r.StatusCode != http.StatusOK { 124 | body, _ := io.ReadAll(r.Body) 125 | 126 | return &result, fmt.Errorf("%s: %s", http.StatusText(r.StatusCode), body) 127 | } 128 | 129 | err = json.NewDecoder(r.Body).Decode(&result) 130 | return &result, err 131 | } 132 | 133 | //go:embed accept_email.go.html 134 | var acceptEmailHTML string 135 | var acceptEmailHTMLTemplate = template.Must(template.New("acceptEmailHTML").Parse(acceptEmailHTML)) 136 | 137 | //go:embed accept_email.go.tmpl 138 | var acceptEmailText string 139 | var acceptEmailTextTemplate = template.Must(template.New("acceptEmailText").Parse(acceptEmailText)) 140 | 141 | type SendAcceptEmailInput struct { 142 | Email string 143 | Token string 144 | } 145 | 146 | type SendAcceptEmailResult struct{} 147 | 148 | func (a *Activities) SendAcceptEmail(ctx context.Context, input *SendAcceptEmailInput) (*SendAcceptEmailResult, error) { 149 | var result SendAcceptEmailResult 150 | 151 | err := a.sendMail(CandidateSupportEmail, input.Email, "Background Check Request", acceptEmailHTMLTemplate, acceptEmailTextTemplate, input) 152 | return &result, err 153 | } 154 | 155 | //go:embed decline_email.go.html 156 | var declineEmailHTML string 157 | var declineEmailHTMLTemplate = template.Must(template.New("declineEmailHTML").Parse(declineEmailHTML)) 158 | 159 | //go:embed decline_email.go.tmpl 160 | var declineEmailText string 161 | var declineEmailTextTemplate = template.Must(template.New("declineEmailText").Parse(declineEmailText)) 162 | 163 | type SendDeclineEmailInput struct { 164 | Email string 165 | } 166 | 167 | type SendDeclineEmailResult struct{} 168 | 169 | func (a *Activities) SendDeclineEmail(ctx context.Context, input *SendDeclineEmailInput) (*SendDeclineEmailResult, error) { 170 | var result SendDeclineEmailResult 171 | 172 | err := a.sendMail(HiringSupportEmail, HiringManagerEmail, "Background Check Declined", declineEmailHTMLTemplate, declineEmailTextTemplate, input) 173 | return &result, err 174 | } 175 | 176 | //go:embed employment_verification_request.go.html 177 | var employmentVerificationRequestEmailHTML string 178 | var employmentVerificationRequestEmailHTMLTemplate = template.Must(template.New("employmentVerificationRequestEmailHTML").Parse(employmentVerificationRequestEmailHTML)) 179 | 180 | //go:embed employment_verification_request.go.tmpl 181 | var employmentVerificationRequestEmailText string 182 | var employmentVerificationRequestEmailTextTemplate = template.Must(template.New("employmentVerificationRequestEmailText").Parse(employmentVerificationRequestEmailText)) 183 | 184 | type SendEmploymentVerificationEmailInput struct { 185 | Email string 186 | Token string 187 | } 188 | 189 | type SendEmploymentVerificationEmailResult struct{} 190 | 191 | func (a *Activities) SendEmploymentVerificationRequestEmail(ctx context.Context, input *SendEmploymentVerificationEmailInput) (*SendEmploymentVerificationEmailResult, error) { 192 | var result SendEmploymentVerificationEmailResult 193 | 194 | err := a.sendMail(ResearcherSupportEmail, input.Email, "Employment Verification Request", employmentVerificationRequestEmailHTMLTemplate, employmentVerificationRequestEmailTextTemplate, input) 195 | 196 | return &result, err 197 | } 198 | 199 | //go:embed report_email.go.html 200 | var reportEmailHTML string 201 | var reportEmailHTMLTemplate = template.Must(template.New("reportEmailHTML").Parse(reportEmailHTML)) 202 | 203 | //go:embed report_email.go.tmpl 204 | var reportEmailText string 205 | var reportEmailTextTemplate = template.Must(template.New("reportEmailText").Parse(reportEmailText)) 206 | 207 | type SendReportEmailInput struct { 208 | Email string 209 | Token string 210 | } 211 | 212 | type SendReportEmailResult struct{} 213 | 214 | func (a *Activities) SendReportEmail(ctx context.Context, input *SendReportEmailInput) (*SendReportEmailResult, error) { 215 | var result SendReportEmailResult 216 | 217 | err := a.sendMail(CandidateSupportEmail, HiringManagerEmail, "Background Check Report", reportEmailHTMLTemplate, reportEmailTextTemplate, input) 218 | return &result, err 219 | } 220 | 221 | type SSNTraceInput struct { 222 | FullName string 223 | SSN string 224 | } 225 | 226 | type SSNTraceResult struct { 227 | SSNIsValid bool 228 | KnownAddresses []string 229 | } 230 | 231 | func (a *Activities) SSNTrace(ctx context.Context, input *SSNTraceInput) (*SSNTraceResult, error) { 232 | var result SSNTraceResult 233 | 234 | if a.HTTPStub { 235 | return &SSNTraceResult{ 236 | SSNIsValid: true, 237 | }, nil 238 | } 239 | 240 | r, err := a.postJSON(ctx, "http://thirdparty:8082/ssntrace", input, PostJSONOptions{Timeout: ssnTraceAPITimeout}) 241 | if err != nil { 242 | return &result, err 243 | } 244 | 245 | if r.StatusCode != http.StatusOK { 246 | defer r.Body.Close() 247 | body, _ := io.ReadAll(r.Body) 248 | 249 | return &result, fmt.Errorf("%s: %s", http.StatusText(r.StatusCode), body) 250 | } 251 | 252 | err = json.NewDecoder(r.Body).Decode(&result) 253 | return &result, err 254 | } 255 | 256 | type StateCriminalSearchInput struct { 257 | FullName string 258 | Address string 259 | } 260 | 261 | type StateCriminalSearchResult struct { 262 | FullName string 263 | Address string 264 | Crimes []string 265 | } 266 | 267 | func (a *Activities) StateCriminalSearch(ctx context.Context, input *StateCriminalSearchInput) (*StateCriminalSearchResult, error) { 268 | var result StateCriminalSearchResult 269 | 270 | if a.HTTPStub { 271 | return &result, nil 272 | } 273 | 274 | r, err := a.postJSON(ctx, "http://thirdparty:8082/statecriminalsearch", input, PostJSONOptions{Timeout: stateCriminalSearchAPITimeout}) 275 | if err != nil { 276 | return &result, err 277 | } 278 | defer r.Body.Close() 279 | 280 | if r.StatusCode != http.StatusOK { 281 | body, _ := io.ReadAll(r.Body) 282 | 283 | return &result, fmt.Errorf("%s: %s", http.StatusText(r.StatusCode), body) 284 | } 285 | 286 | err = json.NewDecoder(r.Body).Decode(&result) 287 | return &result, err 288 | } 289 | 290 | type MotorVehicleIncidentSearchInput struct { 291 | FullName string 292 | Address string 293 | } 294 | 295 | type MotorVehicleIncidentSearchResult struct { 296 | LicenseValid bool 297 | MotorVehicleIncidents []string 298 | } 299 | 300 | func (a *Activities) MotorVehicleIncidentSearch(ctx context.Context, input *MotorVehicleIncidentSearchInput) (*MotorVehicleIncidentSearchResult, error) { 301 | var result MotorVehicleIncidentSearchResult 302 | 303 | if a.HTTPStub { 304 | return &result, nil 305 | } 306 | 307 | r, err := a.postJSON(ctx, "http://thirdparty:8082/motorvehiclesearch", input, PostJSONOptions{Timeout: stateCriminalSearchAPITimeout}) 308 | if err != nil { 309 | return &result, err 310 | } 311 | defer r.Body.Close() 312 | 313 | if r.StatusCode != http.StatusOK { 314 | body, _ := io.ReadAll(r.Body) 315 | 316 | return &result, fmt.Errorf("%s: %s", http.StatusText(r.StatusCode), body) 317 | } 318 | 319 | err = json.NewDecoder(r.Body).Decode(&result) 320 | return &result, err 321 | } 322 | -------------------------------------------------------------------------------- /activities/decline_email.go.html: -------------------------------------------------------------------------------- 1 | {{- $email := .Email -}} 2 | Your background check for {{$email}} has been declined by the candidate. 3 | 4 | Thanks, 5 | 6 | Background Check System 7 | -------------------------------------------------------------------------------- /activities/decline_email.go.tmpl: -------------------------------------------------------------------------------- 1 | {{- $email := .Email -}} 2 | Your background check for {{$email}} has been declined by the candidate. 3 | 4 | Thanks, 5 | 6 | Background Check System 7 | -------------------------------------------------------------------------------- /activities/employment_verification_request.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Start Employment Verification 100 |
  • 101 |
  • Confirm Employment Verification
  • 102 |
  • Verification Submitted
  • 103 |
104 |
105 |
106 |
107 |

Hello Researcher {{.Email}}

108 |

A candidate is undergoing a background check and we need to verify their current employer. 109 |

110 |

111 | 112 | Continue 113 | 114 |

115 |
116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /activities/employment_verification_request.go.tmpl: -------------------------------------------------------------------------------- 1 | Hello Background Check Researcher, 2 | 3 | A candidate is undergoing a background check we need to verify their current employer. 4 | 5 | To perform the verification please visit: 6 | 7 | http://localhost:8083/employment/{{.Token}} 8 | 9 | Thanks, 10 | 11 | Background Check System 12 | -------------------------------------------------------------------------------- /activities/report_email.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Candidate Report Confirmation 100 |
  • 101 |
  • View Candidate Report
  • 102 |
103 |
104 |
105 |
106 |

Hello Hiring Manager

107 |

Your background check for {{.Email}} is complete. 108 |

109 |

110 | 111 | View Report 112 | 113 |

114 |
115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /activities/report_email.go.tmpl: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | Your background check for {{.Email}} is complete. 4 | 5 | To see the report please visit: 6 | 7 | http://localhost:8083/report/{{.Token}} 8 | 9 | Thanks, 10 | 11 | Background Check System 12 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/gorilla/mux" 12 | 13 | "go.temporal.io/api/enums/v1" 14 | workflowpb "go.temporal.io/api/workflow/v1" 15 | "go.temporal.io/api/workflowservice/v1" 16 | "go.temporal.io/sdk/client" 17 | "go.temporal.io/sdk/converter" 18 | 19 | "github.com/temporalio/background-checks/workflows" 20 | ) 21 | 22 | const ( 23 | TaskQueue = "background-checks-main" 24 | ) 25 | 26 | type handlers struct { 27 | temporalClient client.Client 28 | } 29 | 30 | func getBackgroundCheckCandidateEmail(we *workflowpb.WorkflowExecutionInfo) (string, error) { 31 | var email string 32 | 33 | attrs := we.GetSearchAttributes().GetIndexedFields() 34 | 35 | err := converter.GetDefaultDataConverter().FromPayload(attrs["CandidateEmail"], &email) 36 | 37 | return email, err 38 | } 39 | 40 | func getBackgroundCheckStatus(we *workflowpb.WorkflowExecutionInfo) (string, error) { 41 | var status string 42 | 43 | attrs := we.GetSearchAttributes().GetIndexedFields() 44 | 45 | err := converter.GetDefaultDataConverter().FromPayload(attrs["BackgroundCheckStatus"], &status) 46 | 47 | return status, err 48 | } 49 | 50 | func presentBackgroundCheck(we *workflowpb.WorkflowExecutionInfo) (BackgroundCheck, error) { 51 | var result BackgroundCheck 52 | 53 | result.ID = we.Execution.RunId 54 | 55 | email, err := getBackgroundCheckCandidateEmail(we) 56 | if err != nil { 57 | return result, err 58 | } 59 | result.Email = email 60 | 61 | checkStatus, err := getBackgroundCheckStatus(we) 62 | if err != nil { 63 | return result, err 64 | } 65 | 66 | switch we.Status { 67 | case enums.WORKFLOW_EXECUTION_STATUS_RUNNING: 68 | result.Status = checkStatus 69 | case enums.WORKFLOW_EXECUTION_STATUS_TIMED_OUT, 70 | enums.WORKFLOW_EXECUTION_STATUS_FAILED: 71 | result.Status = "failed" 72 | case enums.WORKFLOW_EXECUTION_STATUS_COMPLETED: 73 | if checkStatus == "declined" { 74 | result.Status = "declined" 75 | } else { 76 | result.Status = "completed" 77 | } 78 | case enums.WORKFLOW_EXECUTION_STATUS_TERMINATED: 79 | result.Status = "terminated" 80 | case enums.WORKFLOW_EXECUTION_STATUS_CANCELED: 81 | result.Status = "cancelled" 82 | default: 83 | result.Status = "unknown" 84 | } 85 | 86 | return result, nil 87 | } 88 | 89 | type listWorkflowFilters struct { 90 | Email string 91 | Status string 92 | } 93 | 94 | func queryForFilters(filters listWorkflowFilters) (string, error) { 95 | q := []string{ 96 | "WorkflowType = 'BackgroundCheck'", 97 | } 98 | 99 | if filters.Email != "" { 100 | q = append(q, candidateQuery(filters.Email)) 101 | } 102 | if filters.Status != "" { 103 | f, err := statusQuery(filters.Status) 104 | if err != nil { 105 | return "", err 106 | } 107 | q = append(q, f) 108 | } 109 | 110 | query := strings.Join(q, " AND ") 111 | 112 | return query, nil 113 | } 114 | 115 | func candidateQuery(email string) string { 116 | return fmt.Sprintf("CandidateEmail = '%s'", email) 117 | } 118 | 119 | func statusQuery(status string) (string, error) { 120 | switch status { 121 | case "pending_accept": 122 | return fmt.Sprintf("ExecutionStatus = 'Running' AND BackgroundCheckStatus = '%s'", status), nil 123 | case "running": 124 | return fmt.Sprintf("ExecutionStatus = 'Running' AND BackgroundCheckStatus = '%s'", status), nil 125 | case "completed": 126 | return fmt.Sprintf("ExecutionStatus = 'Completed' AND BackgroundCheckStatus = '%s'", status), nil 127 | case "declined": 128 | return fmt.Sprintf("ExecutionStatus = 'Completed' AND BackgroundCheckStatus = '%s'", status), nil 129 | case "failed": 130 | return "ExecutionStatus = 'Failed'", nil 131 | case "terminated": 132 | return "ExecutionStatus = 'Terminated'", nil 133 | case "cancelled": 134 | return "ExecutionStatus = 'Cancelled'", nil 135 | default: 136 | return "", fmt.Errorf("unknown status: %s", status) 137 | } 138 | } 139 | 140 | func (h *handlers) listWorkflows(ctx context.Context, filters listWorkflowFilters) ([]*workflowpb.WorkflowExecutionInfo, error) { 141 | var executions []*workflowpb.WorkflowExecutionInfo 142 | var nextPageToken []byte 143 | 144 | query, err := queryForFilters(filters) 145 | if err != nil { 146 | return executions, err 147 | } 148 | 149 | for { 150 | resp, err := h.temporalClient.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ 151 | PageSize: 10, 152 | NextPageToken: nextPageToken, 153 | Query: query, 154 | }) 155 | if err != nil { 156 | return executions, err 157 | } 158 | 159 | executions = append(executions, resp.Executions...) 160 | if len(resp.NextPageToken) == 0 { 161 | return executions, nil 162 | } 163 | nextPageToken = resp.NextPageToken 164 | } 165 | } 166 | 167 | func (h *handlers) handleCheckList(w http.ResponseWriter, r *http.Request) { 168 | query := r.URL.Query() 169 | 170 | filters := listWorkflowFilters{ 171 | Email: query.Get("email"), 172 | Status: query.Get("status"), 173 | } 174 | 175 | wfs, err := h.listWorkflows(r.Context(), filters) 176 | if err != nil { 177 | http.Error(w, err.Error(), http.StatusInternalServerError) 178 | return 179 | } 180 | 181 | checks := make([]BackgroundCheck, len(wfs)) 182 | for i, wf := range wfs { 183 | check, err := presentBackgroundCheck(wf) 184 | if err != nil { 185 | http.Error(w, err.Error(), http.StatusInternalServerError) 186 | return 187 | } 188 | checks[i] = check 189 | } 190 | 191 | w.Header().Set("Content-Type", "application/json") 192 | json.NewEncoder(w).Encode(checks) 193 | } 194 | 195 | func (h *handlers) handleCheckCreate(w http.ResponseWriter, r *http.Request) { 196 | var input workflows.BackgroundCheckWorkflowInput 197 | 198 | err := json.NewDecoder(r.Body).Decode(&input) 199 | if err != nil { 200 | http.Error(w, err.Error(), http.StatusBadRequest) 201 | return 202 | } 203 | 204 | _, err = h.temporalClient.ExecuteWorkflow( 205 | r.Context(), 206 | client.StartWorkflowOptions{ 207 | TaskQueue: TaskQueue, 208 | ID: workflows.BackgroundCheckWorkflowID(input.Email), 209 | SearchAttributes: map[string]interface{}{ 210 | "CandidateEmail": input.Email, 211 | }, 212 | }, 213 | workflows.BackgroundCheck, 214 | &input, 215 | ) 216 | 217 | if err != nil { 218 | log.Printf("failed to start workflow: %v", err) 219 | http.Error(w, err.Error(), http.StatusInternalServerError) 220 | return 221 | } 222 | 223 | w.WriteHeader(http.StatusCreated) 224 | } 225 | 226 | func (h *handlers) handleCheckStatus(w http.ResponseWriter, r *http.Request) { 227 | vars := mux.Vars(r) 228 | 229 | email := vars["email"] 230 | 231 | v, err := h.temporalClient.QueryWorkflow( 232 | r.Context(), 233 | workflows.BackgroundCheckWorkflowID(email), 234 | "", 235 | workflows.BackgroundCheckStatusQuery, 236 | ) 237 | if err != nil { 238 | http.Error(w, err.Error(), http.StatusInternalServerError) 239 | return 240 | } 241 | 242 | var result workflows.BackgroundCheckState 243 | err = v.Get(&result) 244 | if err != nil { 245 | http.Error(w, err.Error(), http.StatusInternalServerError) 246 | return 247 | } 248 | 249 | w.Header().Set("Content-Type", "application/json") 250 | json.NewEncoder(w).Encode(result) 251 | } 252 | 253 | func (h *handlers) handleCheckReport(w http.ResponseWriter, r *http.Request) { 254 | vars := mux.Vars(r) 255 | token := vars["token"] 256 | 257 | wfid, runid, err := workflows.WorkflowFromToken(token) 258 | if err != nil { 259 | http.Error(w, err.Error(), http.StatusBadRequest) 260 | return 261 | } 262 | 263 | enc, err := h.temporalClient.QueryWorkflow( 264 | r.Context(), 265 | wfid, 266 | runid, 267 | workflows.BackgroundCheckStatusQuery, 268 | ) 269 | if err != nil { 270 | http.Error(w, err.Error(), http.StatusInternalServerError) 271 | return 272 | } 273 | 274 | var result workflows.BackgroundCheckState 275 | err = enc.Get(&result) 276 | if err != nil { 277 | http.Error(w, err.Error(), http.StatusInternalServerError) 278 | return 279 | } 280 | 281 | w.Header().Set("Content-Type", "application/json") 282 | json.NewEncoder(w).Encode(result) 283 | } 284 | 285 | func (h *handlers) handleAccept(w http.ResponseWriter, r *http.Request) { 286 | vars := mux.Vars(r) 287 | token := vars["token"] 288 | 289 | wfid, runid, err := workflows.WorkflowFromToken(token) 290 | if err != nil { 291 | http.Error(w, err.Error(), http.StatusBadRequest) 292 | return 293 | } 294 | 295 | var result workflows.AcceptSubmissionSignal 296 | 297 | err = json.NewDecoder(r.Body).Decode(&result) 298 | if err != nil { 299 | http.Error(w, err.Error(), http.StatusBadRequest) 300 | return 301 | } 302 | result.Accepted = true 303 | 304 | err = h.temporalClient.SignalWorkflow( 305 | r.Context(), 306 | wfid, 307 | runid, 308 | workflows.AcceptSubmissionSignalName, 309 | result, 310 | ) 311 | if err != nil { 312 | http.Error(w, err.Error(), http.StatusInternalServerError) 313 | return 314 | } 315 | } 316 | 317 | func (h *handlers) handleDecline(w http.ResponseWriter, r *http.Request) { 318 | vars := mux.Vars(r) 319 | token := vars["token"] 320 | 321 | wfid, runid, err := workflows.WorkflowFromToken(token) 322 | if err != nil { 323 | http.Error(w, err.Error(), http.StatusBadRequest) 324 | return 325 | } 326 | 327 | result := workflows.AcceptSubmissionSignal{ 328 | Accepted: false, 329 | } 330 | 331 | err = h.temporalClient.SignalWorkflow( 332 | r.Context(), 333 | wfid, 334 | runid, 335 | workflows.AcceptSubmissionSignalName, 336 | result, 337 | ) 338 | if err != nil { 339 | http.Error(w, err.Error(), http.StatusInternalServerError) 340 | return 341 | } 342 | } 343 | 344 | func (h *handlers) handleEmploymentVerificationDetails(w http.ResponseWriter, r *http.Request) { 345 | vars := mux.Vars(r) 346 | token := vars["token"] 347 | 348 | wfid, runid, err := workflows.WorkflowFromToken(token) 349 | if err != nil { 350 | http.Error(w, err.Error(), http.StatusBadRequest) 351 | return 352 | } 353 | 354 | enc, err := h.temporalClient.QueryWorkflow( 355 | r.Context(), 356 | wfid, 357 | runid, 358 | workflows.EmploymentVerificationDetailsQuery, 359 | ) 360 | if err != nil { 361 | http.Error(w, err.Error(), http.StatusInternalServerError) 362 | return 363 | } 364 | 365 | var result workflows.CandidateDetails 366 | err = enc.Get(&result) 367 | if err != nil { 368 | http.Error(w, err.Error(), http.StatusInternalServerError) 369 | return 370 | } 371 | 372 | w.Header().Set("Content-Type", "application/json") 373 | json.NewEncoder(w).Encode(result) 374 | } 375 | 376 | func (h *handlers) handleEmploymentVerificationSubmission(w http.ResponseWriter, r *http.Request) { 377 | vars := mux.Vars(r) 378 | token := vars["token"] 379 | 380 | wfid, runid, err := workflows.WorkflowFromToken(token) 381 | if err != nil { 382 | http.Error(w, err.Error(), http.StatusBadRequest) 383 | return 384 | } 385 | 386 | var input workflows.EmploymentVerificationSubmissionSignal 387 | 388 | err = json.NewDecoder(r.Body).Decode(&input) 389 | if err != nil { 390 | log.Println("Error: ", err) 391 | http.Error(w, err.Error(), http.StatusBadRequest) 392 | return 393 | } 394 | 395 | result := input 396 | 397 | err = h.temporalClient.SignalWorkflow( 398 | r.Context(), 399 | wfid, 400 | runid, 401 | workflows.EmploymentVerificationSubmissionSignalName, 402 | result, 403 | ) 404 | if err != nil { 405 | http.Error(w, err.Error(), http.StatusInternalServerError) 406 | return 407 | } 408 | 409 | w.Header().Set("Content-Type", "application/json") 410 | json.NewEncoder(w).Encode(result) 411 | } 412 | 413 | func (h *handlers) handleCheckCancel(w http.ResponseWriter, r *http.Request) { 414 | vars := mux.Vars(r) 415 | 416 | email := vars["email"] 417 | wfid := workflows.BackgroundCheckWorkflowID(email) 418 | id := vars["id"] 419 | 420 | err := h.temporalClient.CancelWorkflow(r.Context(), wfid, id) 421 | if err != nil { 422 | http.Error(w, err.Error(), http.StatusInternalServerError) 423 | return 424 | } 425 | } 426 | 427 | func Router(c client.Client) *mux.Router { 428 | r := mux.NewRouter() 429 | 430 | h := handlers{temporalClient: c} 431 | 432 | r.HandleFunc("/checks", h.handleCheckList).Methods("GET").Name("checks_list") 433 | r.HandleFunc("/checks", h.handleCheckCreate).Methods("POST").Name("checks_create") 434 | r.HandleFunc("/checks/{email}/{id}/cancel", h.handleCheckCancel).Methods("POST").Name("check_cancel") 435 | r.HandleFunc("/checks/{email}", h.handleCheckStatus).Methods("GET").Name("check") 436 | 437 | r.HandleFunc("/checks/{token}/accept", h.handleAccept).Methods("POST").Name("accept") 438 | r.HandleFunc("/checks/{token}/decline", h.handleDecline).Methods("POST").Name("decline") 439 | 440 | r.HandleFunc("/checks/{token}/employment", h.handleEmploymentVerificationDetails).Methods("GET").Name("employmentverify_details") 441 | r.HandleFunc("/checks/{token}/employment", h.handleEmploymentVerificationSubmission).Methods("POST").Name("employmentverify") 442 | 443 | r.HandleFunc("/checks/{token}/report", h.handleCheckReport).Methods("GET").Name("check_report") 444 | 445 | return r 446 | } 447 | -------------------------------------------------------------------------------- /api/types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type BackgroundCheck struct { 4 | ID string 5 | Email string 6 | Status string 7 | } 8 | -------------------------------------------------------------------------------- /cli/bgc-backend/cmd/api.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/temporalio/background-checks/api" 11 | "github.com/temporalio/background-checks/temporal" 12 | "go.temporal.io/sdk/client" 13 | ) 14 | 15 | // apiCmd represents the api command 16 | var apiCmd = &cobra.Command{ 17 | Use: "api", 18 | Short: "Run API Server", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | c, err := temporal.NewClient(client.Options{}) 21 | if err != nil { 22 | log.Fatalf("error: %v", err) 23 | } 24 | defer c.Close() 25 | 26 | srv := &http.Server{ 27 | Handler: api.Router(c), 28 | Addr: "0.0.0.0:8081", 29 | } 30 | 31 | errCh := make(chan error, 1) 32 | go func() { errCh <- srv.ListenAndServe() }() 33 | 34 | sigCh := make(chan os.Signal, 1) 35 | signal.Notify(sigCh, os.Interrupt) 36 | 37 | select { 38 | case <-sigCh: 39 | srv.Close() 40 | case err = <-errCh: 41 | log.Fatalf("error: %v", err) 42 | } 43 | }, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(apiCmd) 48 | } 49 | -------------------------------------------------------------------------------- /cli/bgc-backend/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // rootCmd represents the base command when called without any subcommands 8 | var rootCmd = &cobra.Command{ 9 | Use: "bgc-backend", 10 | Short: "Backend for the Background Checks application", 11 | CompletionOptions: cobra.CompletionOptions{ 12 | DisableDefaultCmd: true, 13 | }, 14 | } 15 | 16 | // Execute adds all child commands to the root command and sets flags appropriately. 17 | // This is called by main.main(). It only needs to happen once to the rootCmd. 18 | func Execute() { 19 | cobra.CheckErr(rootCmd.Execute()) 20 | } 21 | -------------------------------------------------------------------------------- /cli/bgc-backend/cmd/ui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/temporalio/background-checks/ui" 11 | ) 12 | 13 | // uiCmd represents the ui command 14 | var uiCmd = &cobra.Command{ 15 | Use: "ui", 16 | Short: "Run UI Server", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | srv := &http.Server{ 19 | Handler: ui.Router(), 20 | Addr: "0.0.0.0:8083", 21 | } 22 | 23 | errCh := make(chan error, 1) 24 | go func() { errCh <- srv.ListenAndServe() }() 25 | 26 | sigCh := make(chan os.Signal, 1) 27 | signal.Notify(sigCh, os.Interrupt) 28 | 29 | select { 30 | case <-sigCh: 31 | srv.Close() 32 | case err := <-errCh: 33 | log.Fatalf("error: %v", err) 34 | } 35 | }, 36 | } 37 | 38 | func init() { 39 | rootCmd.AddCommand(uiCmd) 40 | } 41 | -------------------------------------------------------------------------------- /cli/bgc-backend/cmd/worker.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | prom "github.com/prometheus/client_golang/prometheus" 10 | "github.com/uber-go/tally/v4" 11 | "github.com/uber-go/tally/v4/prometheus" 12 | "go.temporal.io/sdk/client" 13 | tallyhandler "go.temporal.io/sdk/contrib/tally" 14 | "go.temporal.io/sdk/worker" 15 | 16 | "github.com/temporalio/background-checks/activities" 17 | "github.com/temporalio/background-checks/temporal" 18 | "github.com/temporalio/background-checks/workflows" 19 | ) 20 | 21 | // workerCmd represents the worker command 22 | var workerCmd = &cobra.Command{ 23 | Use: "worker", 24 | Short: "Run worker", 25 | Run: func(cmd *cobra.Command, args []string) { 26 | c, err := temporal.NewClient(client.Options{ 27 | MetricsHandler: tallyhandler.NewMetricsHandler(newPrometheusScope(prometheus.Configuration{ 28 | ListenAddress: "0.0.0.0:8001", 29 | TimerType: "histogram", 30 | })), 31 | }) 32 | if err != nil { 33 | log.Fatalf("client error: %v", err) 34 | } 35 | defer c.Close() 36 | 37 | w := worker.New(c, "background-checks-main", worker.Options{}) 38 | 39 | w.RegisterWorkflow(workflows.BackgroundCheck) 40 | w.RegisterWorkflow(workflows.Accept) 41 | w.RegisterWorkflow(workflows.EmploymentVerification) 42 | w.RegisterActivity(&activities.Activities{SMTPHost: "mailhog", SMTPPort: 1025}) 43 | w.RegisterWorkflow(workflows.SSNTrace) 44 | w.RegisterWorkflow(workflows.FederalCriminalSearch) 45 | w.RegisterWorkflow(workflows.StateCriminalSearch) 46 | w.RegisterWorkflow(workflows.MotorVehicleIncidentSearch) 47 | 48 | err = w.Run(worker.InterruptCh()) 49 | if err != nil { 50 | log.Fatalf("worker exited: %v", err) 51 | } 52 | }, 53 | } 54 | 55 | func newPrometheusScope(c prometheus.Configuration) tally.Scope { 56 | reporter, err := c.NewReporter( 57 | prometheus.ConfigurationOptions{ 58 | Registry: prom.NewRegistry(), 59 | OnError: func(err error) { 60 | log.Println("error in prometheus reporter", err) 61 | }, 62 | }, 63 | ) 64 | if err != nil { 65 | log.Fatalln("error creating prometheus reporter", err) 66 | } 67 | scopeOpts := tally.ScopeOptions{ 68 | CachedReporter: reporter, 69 | Separator: prometheus.DefaultSeparator, 70 | } 71 | scope, _ := tally.NewRootScope(scopeOpts, time.Second) 72 | 73 | return scope 74 | } 75 | 76 | func init() { 77 | rootCmd.AddCommand(workerCmd) 78 | } 79 | -------------------------------------------------------------------------------- /cli/bgc-backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/temporalio/background-checks/cli/bgc-backend/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /cli/bgc-candidate/cmd/accept.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/temporalio/background-checks/api" 11 | "github.com/temporalio/background-checks/utils" 12 | "github.com/temporalio/background-checks/workflows" 13 | ) 14 | 15 | // acceptCmd represents the accept command 16 | var acceptCmd = &cobra.Command{ 17 | Use: "accept", 18 | Short: "Accept a background check", 19 | Args: cobra.NoArgs, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | router := api.Router(nil) 22 | 23 | requestURL, err := router.Get("accept").Host(APIEndpoint).URL("token", Token) 24 | if err != nil { 25 | log.Fatalf("cannot create URL: %v", err) 26 | } 27 | 28 | candidatedetails := workflows.CandidateDetails{ 29 | FullName: FullName, 30 | SSN: SSN, 31 | Employer: Employer} 32 | submission := workflows.AcceptSubmissionSignal{ 33 | CandidateDetails: candidatedetails, 34 | } 35 | 36 | response, err := utils.PostJSON(requestURL, submission) 37 | if err != nil { 38 | log.Fatalf(err.Error()) 39 | } 40 | defer response.Body.Close() 41 | 42 | body, _ := io.ReadAll(response.Body) 43 | 44 | if response.StatusCode != http.StatusOK { 45 | log.Fatalf("%s: %s", http.StatusText(response.StatusCode), body) 46 | } 47 | 48 | fmt.Println("Accepted") 49 | }, 50 | } 51 | 52 | func init() { 53 | rootCmd.AddCommand(acceptCmd) 54 | acceptCmd.Flags().StringVar(&Token, "token", "", "Token") 55 | acceptCmd.MarkFlagRequired("token") 56 | acceptCmd.Flags().StringVar(&FullName, "fullname", "", "Candidate's full name") 57 | acceptCmd.MarkFlagRequired("fullname") 58 | acceptCmd.Flags().StringVar(&SSN, "ssn", "", "Social Security #") 59 | acceptCmd.MarkFlagRequired("ssn") 60 | acceptCmd.Flags().StringVar(&Employer, "employer", "", "Social Security #") 61 | acceptCmd.MarkFlagRequired("employer") 62 | 63 | } 64 | -------------------------------------------------------------------------------- /cli/bgc-candidate/cmd/decline.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/temporalio/background-checks/api" 11 | "github.com/temporalio/background-checks/utils" 12 | ) 13 | 14 | // declineCmd represents the decline command 15 | var declineCmd = &cobra.Command{ 16 | Use: "decline", 17 | Short: "Decline a background check", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | router := api.Router(nil) 21 | 22 | requestURL, err := router.Get("decline").Host(APIEndpoint).URL("token", Token) 23 | if err != nil { 24 | log.Fatalf("cannot create URL: %v", err) 25 | } 26 | 27 | response, err := utils.PostJSON(requestURL, nil) 28 | if err != nil { 29 | log.Fatalf(err.Error()) 30 | } 31 | defer response.Body.Close() 32 | 33 | body, _ := io.ReadAll(response.Body) 34 | 35 | if response.StatusCode != http.StatusOK { 36 | log.Fatalf("%s: %s", http.StatusText(response.StatusCode), body) 37 | } 38 | 39 | fmt.Println("Declined") 40 | }, 41 | } 42 | 43 | func init() { 44 | rootCmd.AddCommand(declineCmd) 45 | declineCmd.Flags().StringVar(&Token, "token", "", "Token") 46 | declineCmd.MarkFlagRequired("token") 47 | } 48 | -------------------------------------------------------------------------------- /cli/bgc-candidate/cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | var ( 4 | Token string 5 | FullName string 6 | SSN string 7 | Employer string 8 | ) 9 | -------------------------------------------------------------------------------- /cli/bgc-candidate/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const APIEndpoint = "api:8081" 8 | 9 | // rootCmd represents the base command when called without any subcommands 10 | var rootCmd = &cobra.Command{ 11 | Use: "bgc-candidate", 12 | Short: "Candidate CLI for the Background Check application", 13 | CompletionOptions: cobra.CompletionOptions{ 14 | DisableDefaultCmd: true, 15 | }, 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | cobra.CheckErr(rootCmd.Execute()) 22 | } 23 | -------------------------------------------------------------------------------- /cli/bgc-candidate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/temporalio/background-checks/cli/bgc-candidate/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /cli/bgc-company/cmd/cancel.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/temporalio/background-checks/api" 12 | "github.com/temporalio/background-checks/utils" 13 | ) 14 | 15 | // cancelCmd represents the cancel command 16 | var cancelCmd = &cobra.Command{ 17 | Use: "cancel", 18 | Short: "cancels a background check", 19 | Args: cobra.NoArgs, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | router := api.Router(nil) 22 | 23 | requestURL, err := router.Get("check_cancel").Host(APIEndpoint).URL("email", email, "id", id) 24 | if err != nil { 25 | log.Fatalf("cannot create URL: %v", err) 26 | } 27 | 28 | response, err := utils.PostJSON(requestURL, nil) 29 | if err != nil { 30 | log.Fatalf("request error: %v", err) 31 | } 32 | defer response.Body.Close() 33 | 34 | body, _ := io.ReadAll(response.Body) 35 | 36 | if response.StatusCode != http.StatusOK { 37 | log.Fatalf("%s: %s", http.StatusText(response.StatusCode), body) 38 | } 39 | 40 | fmt.Printf("Cancelled check\n") 41 | }, 42 | } 43 | 44 | func init() { 45 | rootCmd.AddCommand(cancelCmd) 46 | 47 | cancelCmd.Flags().StringVar(&email, "email", "", "Candidate's email address") 48 | cancelCmd.MarkFlagRequired("email") 49 | cancelCmd.Flags().StringVar(&id, "id", "", "Check ID") 50 | cancelCmd.MarkFlagRequired("id") 51 | } 52 | -------------------------------------------------------------------------------- /cli/bgc-company/cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | var ( 4 | id string 5 | email string 6 | pkg string 7 | status string 8 | ) 9 | -------------------------------------------------------------------------------- /cli/bgc-company/cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/temporalio/background-checks/api" 9 | "github.com/temporalio/background-checks/utils" 10 | ) 11 | 12 | // listCmd represents the list command 13 | var listCmd = &cobra.Command{ 14 | Use: "list", 15 | Short: "List background checks", 16 | Args: cobra.NoArgs, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | router := api.Router(nil) 19 | 20 | requestURL, err := router.Get("checks_create").Host(APIEndpoint).URL() 21 | if err != nil { 22 | log.Fatalf("cannot create URL: %v", err) 23 | } 24 | 25 | query := requestURL.Query() 26 | query.Set("email", email) 27 | query.Set("status", status) 28 | requestURL.RawQuery = query.Encode() 29 | 30 | var checks []api.BackgroundCheck 31 | _, err = utils.GetJSON(requestURL, &checks) 32 | if err != nil { 33 | log.Fatalf("request error: %v", err) 34 | } 35 | 36 | fmt.Printf("Background Checks:\n") 37 | for _, check := range checks { 38 | fmt.Printf("ID: %s Email: %s Status: %s\n", check.ID, check.Email, check.Status) 39 | } 40 | }, 41 | } 42 | 43 | func init() { 44 | rootCmd.AddCommand(listCmd) 45 | 46 | listCmd.Flags().StringVar(&email, "email", "", "Candidate's email address") 47 | listCmd.Flags().StringVar(&status, "status", "", "Status") 48 | } 49 | -------------------------------------------------------------------------------- /cli/bgc-company/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const APIEndpoint = "api:8081" 8 | 9 | // rootCmd represents the base command when called without any subcommands 10 | var rootCmd = &cobra.Command{ 11 | Use: "bgc-company", 12 | Short: "Company CLI for the Background Check application", 13 | CompletionOptions: cobra.CompletionOptions{ 14 | DisableDefaultCmd: true, 15 | }, 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | cobra.CheckErr(rootCmd.Execute()) 22 | } 23 | -------------------------------------------------------------------------------- /cli/bgc-company/cmd/start.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/temporalio/background-checks/api" 12 | "github.com/temporalio/background-checks/utils" 13 | "github.com/temporalio/background-checks/workflows" 14 | ) 15 | 16 | // startCmd represents the start command 17 | var startCmd = &cobra.Command{ 18 | Use: "start", 19 | Short: "starts a background check for a candidate", 20 | Args: cobra.NoArgs, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | router := api.Router(nil) 23 | 24 | requestURL, err := router.Get("checks_create").Host(APIEndpoint).URL() 25 | if err != nil { 26 | log.Fatalf("cannot create URL: %v", err) 27 | } 28 | 29 | input := workflows.BackgroundCheckWorkflowInput{ 30 | Email: email, 31 | Tier: pkg, 32 | } 33 | 34 | response, err := utils.PostJSON(requestURL, input) 35 | if err != nil { 36 | log.Fatalf("request error: %v", err) 37 | } 38 | defer response.Body.Close() 39 | 40 | body, _ := io.ReadAll(response.Body) 41 | 42 | if response.StatusCode != http.StatusCreated { 43 | log.Fatalf("%s: %s", http.StatusText(response.StatusCode), body) 44 | } 45 | 46 | fmt.Printf("Created check\n") 47 | }, 48 | } 49 | 50 | func init() { 51 | rootCmd.AddCommand(startCmd) 52 | 53 | startCmd.Flags().StringVar(&email, "email", "", "Candidate's email address") 54 | startCmd.MarkFlagRequired("email") 55 | startCmd.Flags().StringVar(&pkg, "package", "standard", "Check package (standard/full)") 56 | } 57 | -------------------------------------------------------------------------------- /cli/bgc-company/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/temporalio/background-checks/cli/bgc-company/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /cli/bgc-researcher/cmd/employmentverify.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/temporalio/background-checks/api" 11 | "github.com/temporalio/background-checks/utils" 12 | "github.com/temporalio/background-checks/workflows" 13 | ) 14 | 15 | var employmentVerifyCmd = &cobra.Command{ 16 | Use: "employmentverify", 17 | Short: "Complete the employment verification process for a candidate", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | router := api.Router(nil) 21 | 22 | requestURL, err := router.Get("employmentverify").Host(APIEndpoint).URL("token", Token) 23 | if err != nil { 24 | log.Fatalf("cannot create URL: %v", err) 25 | } 26 | 27 | submission := workflows.EmploymentVerificationSubmissionSignal{ 28 | EmploymentVerificationComplete: true, 29 | EmployerVerified: true, 30 | } 31 | 32 | response, err := utils.PostJSON(requestURL, submission) 33 | if err != nil { 34 | log.Fatalln(err.Error()) 35 | } 36 | defer response.Body.Close() 37 | 38 | body, _ := io.ReadAll(response.Body) 39 | 40 | if response.StatusCode != http.StatusOK { 41 | log.Fatalf("%s: %s", http.StatusText(response.StatusCode), body) 42 | } 43 | fmt.Println("Employment verification received") 44 | }, 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(employmentVerifyCmd) 49 | employmentVerifyCmd.Flags().StringVar(&Token, "token", "", "Token") 50 | employmentVerifyCmd.MarkFlagRequired("token") 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cli/bgc-researcher/cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | var ( 4 | Token string 5 | ) 6 | -------------------------------------------------------------------------------- /cli/bgc-researcher/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const APIEndpoint = "api:8081" 8 | 9 | // rootCmd represents the base command when called without any subcommands 10 | var rootCmd = &cobra.Command{ 11 | Use: "bgc-researcher", 12 | Short: "Researcher CLI for the Background Check application", 13 | CompletionOptions: cobra.CompletionOptions{ 14 | DisableDefaultCmd: true, 15 | }, 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | cobra.CheckErr(rootCmd.Execute()) 22 | } 23 | -------------------------------------------------------------------------------- /cli/bgc-researcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/temporalio/background-checks/cli/bgc-researcher/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /deployment/grafana/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grafana/grafana 2 | ADD ./provisioning /etc/grafana/provisioning 3 | ADD ./config.ini /etc/grafana/config.ini 4 | ADD ./dashboards /var/lib/grafana/dashboards -------------------------------------------------------------------------------- /deployment/grafana/config.ini: -------------------------------------------------------------------------------- 1 | [paths] 2 | provisioning = /etc/grafana/provisioning 3 | 4 | [server] 5 | enable_gzip = true 6 | 7 | [users] 8 | default_theme = light -------------------------------------------------------------------------------- /deployment/grafana/dashboards/sdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "target": { 12 | "limit": 100, 13 | "matchAny": false, 14 | "tags": [], 15 | "type": "dashboard" 16 | }, 17 | "type": "dashboard" 18 | } 19 | ] 20 | }, 21 | "editable": true, 22 | "fiscalYearStartMonth": 0, 23 | "graphTooltip": 0, 24 | "id": 1, 25 | "iteration": 1642562334429, 26 | "links": [], 27 | "liveNow": false, 28 | "panels": [ 29 | { 30 | "collapsed": false, 31 | "gridPos": { 32 | "h": 1, 33 | "w": 24, 34 | "x": 0, 35 | "y": 0 36 | }, 37 | "id": 8, 38 | "panels": [], 39 | "title": "RPC Overview ($Namespace)", 40 | "type": "row" 41 | }, 42 | { 43 | "aliasColors": {}, 44 | "bars": false, 45 | "dashLength": 10, 46 | "dashes": false, 47 | "datasource": "$datasource", 48 | "fieldConfig": { 49 | "defaults": { 50 | "links": [] 51 | }, 52 | "overrides": [] 53 | }, 54 | "fill": 1, 55 | "fillGradient": 0, 56 | "gridPos": { 57 | "h": 10, 58 | "w": 8, 59 | "x": 0, 60 | "y": 1 61 | }, 62 | "hiddenSeries": false, 63 | "id": 2, 64 | "legend": { 65 | "avg": false, 66 | "current": false, 67 | "max": false, 68 | "min": false, 69 | "show": true, 70 | "total": false, 71 | "values": false 72 | }, 73 | "lines": true, 74 | "linewidth": 1, 75 | "nullPointMode": "null", 76 | "options": { 77 | "alertThreshold": true 78 | }, 79 | "percentage": false, 80 | "pluginVersion": "8.3.4", 81 | "pointradius": 2, 82 | "points": false, 83 | "renderer": "flot", 84 | "seriesOverrides": [], 85 | "spaceLength": 10, 86 | "stack": false, 87 | "steppedLine": false, 88 | "targets": [ 89 | { 90 | "datasource": "$datasource", 91 | "exemplar": true, 92 | "expr": "sum(rate(temporal_request[5m]))", 93 | "interval": "", 94 | "legendFormat": "requests", 95 | "refId": "A" 96 | }, 97 | { 98 | "datasource": "$datasource", 99 | "exemplar": true, 100 | "expr": "sum(rate(temporal_request_failure[5m]))", 101 | "hide": false, 102 | "interval": "", 103 | "legendFormat": "failures", 104 | "refId": "B" 105 | } 106 | ], 107 | "thresholds": [], 108 | "timeRegions": [], 109 | "title": "Requests Vs Failures", 110 | "tooltip": { 111 | "shared": true, 112 | "sort": 0, 113 | "value_type": "individual" 114 | }, 115 | "type": "graph", 116 | "xaxis": { 117 | "mode": "time", 118 | "show": true, 119 | "values": [] 120 | }, 121 | "yaxes": [ 122 | { 123 | "format": "short", 124 | "logBase": 1, 125 | "show": true 126 | }, 127 | { 128 | "format": "short", 129 | "logBase": 1, 130 | "show": true 131 | } 132 | ], 133 | "yaxis": { 134 | "align": false 135 | } 136 | }, 137 | { 138 | "aliasColors": {}, 139 | "bars": false, 140 | "dashLength": 10, 141 | "dashes": false, 142 | "datasource": "$datasource", 143 | "fieldConfig": { 144 | "defaults": { 145 | "links": [] 146 | }, 147 | "overrides": [] 148 | }, 149 | "fill": 1, 150 | "fillGradient": 0, 151 | "gridPos": { 152 | "h": 10, 153 | "w": 7, 154 | "x": 8, 155 | "y": 1 156 | }, 157 | "hiddenSeries": false, 158 | "id": 5, 159 | "legend": { 160 | "avg": false, 161 | "current": false, 162 | "max": false, 163 | "min": false, 164 | "show": true, 165 | "total": false, 166 | "values": false 167 | }, 168 | "lines": true, 169 | "linewidth": 1, 170 | "nullPointMode": "null", 171 | "options": { 172 | "alertThreshold": true 173 | }, 174 | "percentage": false, 175 | "pluginVersion": "8.3.4", 176 | "pointradius": 2, 177 | "points": false, 178 | "renderer": "flot", 179 | "seriesOverrides": [], 180 | "spaceLength": 10, 181 | "stack": false, 182 | "steppedLine": false, 183 | "targets": [ 184 | { 185 | "datasource": "$datasource", 186 | "exemplar": true, 187 | "expr": "sum by (operation) ((rate(temporal_request[5m])))", 188 | "interval": "", 189 | "legendFormat": "{{ operation }}", 190 | "refId": "A" 191 | } 192 | ], 193 | "thresholds": [], 194 | "timeRegions": [], 195 | "title": "RPC Requests Per Operation", 196 | "tooltip": { 197 | "shared": true, 198 | "sort": 0, 199 | "value_type": "individual" 200 | }, 201 | "type": "graph", 202 | "xaxis": { 203 | "mode": "time", 204 | "show": true, 205 | "values": [] 206 | }, 207 | "yaxes": [ 208 | { 209 | "format": "short", 210 | "logBase": 1, 211 | "show": true 212 | }, 213 | { 214 | "format": "short", 215 | "logBase": 1, 216 | "show": true 217 | } 218 | ], 219 | "yaxis": { 220 | "align": false 221 | } 222 | }, 223 | { 224 | "aliasColors": {}, 225 | "bars": false, 226 | "dashLength": 10, 227 | "dashes": false, 228 | "datasource": "$datasource", 229 | "fieldConfig": { 230 | "defaults": { 231 | "links": [] 232 | }, 233 | "overrides": [] 234 | }, 235 | "fill": 1, 236 | "fillGradient": 0, 237 | "gridPos": { 238 | "h": 10, 239 | "w": 9, 240 | "x": 15, 241 | "y": 1 242 | }, 243 | "hiddenSeries": false, 244 | "id": 4, 245 | "legend": { 246 | "avg": false, 247 | "current": false, 248 | "max": false, 249 | "min": false, 250 | "show": true, 251 | "total": false, 252 | "values": false 253 | }, 254 | "lines": true, 255 | "linewidth": 1, 256 | "nullPointMode": "null", 257 | "options": { 258 | "alertThreshold": true 259 | }, 260 | "percentage": false, 261 | "pluginVersion": "8.3.4", 262 | "pointradius": 2, 263 | "points": false, 264 | "renderer": "flot", 265 | "seriesOverrides": [], 266 | "spaceLength": 10, 267 | "stack": false, 268 | "steppedLine": false, 269 | "targets": [ 270 | { 271 | "datasource": "$datasource", 272 | "exemplar": true, 273 | "expr": "sum by (operation) (rate(temporal_request_failure[5m]))", 274 | "interval": "", 275 | "legendFormat": "{{ operation }}", 276 | "refId": "A" 277 | } 278 | ], 279 | "thresholds": [], 280 | "timeRegions": [], 281 | "title": "RPC Failures Per Operation", 282 | "tooltip": { 283 | "shared": true, 284 | "sort": 0, 285 | "value_type": "individual" 286 | }, 287 | "type": "graph", 288 | "xaxis": { 289 | "mode": "time", 290 | "show": true, 291 | "values": [] 292 | }, 293 | "yaxes": [ 294 | { 295 | "format": "short", 296 | "logBase": 1, 297 | "show": true 298 | }, 299 | { 300 | "format": "short", 301 | "logBase": 1, 302 | "show": true 303 | } 304 | ], 305 | "yaxis": { 306 | "align": false 307 | } 308 | }, 309 | { 310 | "aliasColors": {}, 311 | "bars": false, 312 | "dashLength": 10, 313 | "dashes": false, 314 | "datasource": "$datasource", 315 | "fieldConfig": { 316 | "defaults": { 317 | "links": [] 318 | }, 319 | "overrides": [] 320 | }, 321 | "fill": 1, 322 | "fillGradient": 0, 323 | "gridPos": { 324 | "h": 9, 325 | "w": 24, 326 | "x": 0, 327 | "y": 11 328 | }, 329 | "hiddenSeries": false, 330 | "id": 6, 331 | "legend": { 332 | "avg": false, 333 | "current": false, 334 | "max": false, 335 | "min": false, 336 | "show": true, 337 | "total": false, 338 | "values": false 339 | }, 340 | "lines": true, 341 | "linewidth": 1, 342 | "nullPointMode": "null", 343 | "options": { 344 | "alertThreshold": true 345 | }, 346 | "percentage": false, 347 | "pluginVersion": "8.3.4", 348 | "pointradius": 2, 349 | "points": false, 350 | "renderer": "flot", 351 | "seriesOverrides": [], 352 | "spaceLength": 10, 353 | "stack": false, 354 | "steppedLine": false, 355 | "targets": [ 356 | { 357 | "datasource": "$datasource", 358 | "exemplar": true, 359 | "expr": "histogram_quantile(0.95, sum by (temporal_namespace, operation, le) (rate(temporal_request_latency_bucket[5m])))", 360 | "interval": "", 361 | "legendFormat": "{{ temporal_namespace }} - {{ operation }}", 362 | "refId": "A" 363 | } 364 | ], 365 | "thresholds": [], 366 | "timeRegions": [], 367 | "title": "RPC Latencies ($Namespace)", 368 | "tooltip": { 369 | "shared": true, 370 | "sort": 0, 371 | "value_type": "individual" 372 | }, 373 | "type": "graph", 374 | "xaxis": { 375 | "mode": "time", 376 | "show": true, 377 | "values": [] 378 | }, 379 | "yaxes": [ 380 | { 381 | "format": "dtdurations", 382 | "logBase": 1, 383 | "show": true 384 | }, 385 | { 386 | "format": "short", 387 | "logBase": 1, 388 | "show": true 389 | } 390 | ], 391 | "yaxis": { 392 | "align": false 393 | } 394 | }, 395 | { 396 | "collapsed": true, 397 | "gridPos": { 398 | "h": 1, 399 | "w": 24, 400 | "x": 0, 401 | "y": 20 402 | }, 403 | "id": 12, 404 | "panels": [ 405 | { 406 | "aliasColors": {}, 407 | "bars": false, 408 | "dashLength": 10, 409 | "dashes": false, 410 | "datasource": "$datasource", 411 | "fieldConfig": { 412 | "defaults": { 413 | "links": [] 414 | }, 415 | "overrides": [] 416 | }, 417 | "fill": 1, 418 | "fillGradient": 0, 419 | "gridPos": { 420 | "h": 8, 421 | "w": 12, 422 | "x": 0, 423 | "y": 21 424 | }, 425 | "hiddenSeries": false, 426 | "id": 10, 427 | "legend": { 428 | "avg": false, 429 | "current": false, 430 | "max": false, 431 | "min": false, 432 | "show": true, 433 | "total": false, 434 | "values": false 435 | }, 436 | "lines": true, 437 | "linewidth": 1, 438 | "nullPointMode": "null", 439 | "options": { 440 | "alertThreshold": true 441 | }, 442 | "percentage": false, 443 | "pluginVersion": "8.3.4", 444 | "pointradius": 2, 445 | "points": false, 446 | "renderer": "flot", 447 | "seriesOverrides": [], 448 | "spaceLength": 10, 449 | "stack": false, 450 | "steppedLine": false, 451 | "targets": [ 452 | { 453 | "datasource": "$datasource", 454 | "exemplar": true, 455 | "expr": "sum(rate(temporal_workflow_completed[5m]))", 456 | "interval": "", 457 | "legendFormat": "success", 458 | "refId": "A" 459 | }, 460 | { 461 | "datasource": "$datasource", 462 | "exemplar": true, 463 | "expr": "sum(rate(temporal_workflow_failed[5m]))", 464 | "hide": false, 465 | "interval": "", 466 | "legendFormat": "failed", 467 | "refId": "B" 468 | }, 469 | { 470 | "datasource": "$datasource", 471 | "exemplar": true, 472 | "expr": "sum(rate(temporal_workflow_canceled[5m]))", 473 | "hide": false, 474 | "interval": "", 475 | "legendFormat": "cancelled", 476 | "refId": "C" 477 | }, 478 | { 479 | "datasource": "$datasource", 480 | "exemplar": true, 481 | "expr": "sum(rate(temporal_workflow_continue_as_new[5m]))", 482 | "hide": false, 483 | "interval": "", 484 | "legendFormat": "continued_as_new", 485 | "refId": "D" 486 | } 487 | ], 488 | "thresholds": [], 489 | "timeRegions": [], 490 | "title": "Workflow Completion", 491 | "tooltip": { 492 | "shared": true, 493 | "sort": 0, 494 | "value_type": "individual" 495 | }, 496 | "type": "graph", 497 | "xaxis": { 498 | "mode": "time", 499 | "show": true, 500 | "values": [] 501 | }, 502 | "yaxes": [ 503 | { 504 | "format": "short", 505 | "logBase": 1, 506 | "show": true 507 | }, 508 | { 509 | "format": "short", 510 | "logBase": 1, 511 | "show": true 512 | } 513 | ], 514 | "yaxis": { 515 | "align": false 516 | } 517 | }, 518 | { 519 | "aliasColors": {}, 520 | "bars": false, 521 | "dashLength": 10, 522 | "dashes": false, 523 | "datasource": "$datasource", 524 | "fieldConfig": { 525 | "defaults": { 526 | "links": [] 527 | }, 528 | "overrides": [] 529 | }, 530 | "fill": 1, 531 | "fillGradient": 0, 532 | "gridPos": { 533 | "h": 8, 534 | "w": 12, 535 | "x": 12, 536 | "y": 21 537 | }, 538 | "hiddenSeries": false, 539 | "id": 15, 540 | "legend": { 541 | "avg": false, 542 | "current": false, 543 | "max": false, 544 | "min": false, 545 | "show": true, 546 | "total": false, 547 | "values": false 548 | }, 549 | "lines": true, 550 | "linewidth": 1, 551 | "nullPointMode": "null", 552 | "options": { 553 | "alertThreshold": true 554 | }, 555 | "percentage": false, 556 | "pluginVersion": "8.3.4", 557 | "pointradius": 2, 558 | "points": false, 559 | "renderer": "flot", 560 | "seriesOverrides": [], 561 | "spaceLength": 10, 562 | "stack": false, 563 | "steppedLine": false, 564 | "targets": [ 565 | { 566 | "datasource": "$datasource", 567 | "exemplar": true, 568 | "expr": "histogram_quantile(0.95, sum(rate(temporal_workflow_endtoend_latency_bucket[5m])) by (temporal_namespace, workflow_type, le))", 569 | "interval": "", 570 | "legendFormat": "{{ temporal_namespace }} - {{ workflow_type }}", 571 | "refId": "A" 572 | } 573 | ], 574 | "thresholds": [], 575 | "timeRegions": [], 576 | "title": "Workflow End-To-End Latencies", 577 | "tooltip": { 578 | "shared": true, 579 | "sort": 0, 580 | "value_type": "individual" 581 | }, 582 | "type": "graph", 583 | "xaxis": { 584 | "mode": "time", 585 | "show": true, 586 | "values": [] 587 | }, 588 | "yaxes": [ 589 | { 590 | "format": "short", 591 | "logBase": 1, 592 | "show": true 593 | }, 594 | { 595 | "format": "short", 596 | "logBase": 1, 597 | "show": true 598 | } 599 | ], 600 | "yaxis": { 601 | "align": false 602 | } 603 | }, 604 | { 605 | "aliasColors": {}, 606 | "bars": false, 607 | "dashLength": 10, 608 | "dashes": false, 609 | "datasource": "$datasource", 610 | "fieldConfig": { 611 | "defaults": { 612 | "links": [] 613 | }, 614 | "overrides": [] 615 | }, 616 | "fill": 1, 617 | "fillGradient": 0, 618 | "gridPos": { 619 | "h": 8, 620 | "w": 12, 621 | "x": 0, 622 | "y": 29 623 | }, 624 | "hiddenSeries": false, 625 | "id": 16, 626 | "legend": { 627 | "avg": false, 628 | "current": false, 629 | "max": false, 630 | "min": false, 631 | "show": true, 632 | "total": false, 633 | "values": false 634 | }, 635 | "lines": true, 636 | "linewidth": 1, 637 | "nullPointMode": "null", 638 | "options": { 639 | "alertThreshold": true 640 | }, 641 | "percentage": false, 642 | "pluginVersion": "8.3.4", 643 | "pointradius": 2, 644 | "points": false, 645 | "renderer": "flot", 646 | "seriesOverrides": [], 647 | "spaceLength": 10, 648 | "stack": false, 649 | "steppedLine": false, 650 | "targets": [ 651 | { 652 | "datasource": "$datasource", 653 | "exemplar": true, 654 | "expr": "sum by (temporal_namespace, workflow_type) (rate(temporal_workflow_completed[5m]))", 655 | "interval": "", 656 | "legendFormat": "{{ temporal_namespace }} - {{ workflow_type }}", 657 | "refId": "A" 658 | } 659 | ], 660 | "thresholds": [], 661 | "timeRegions": [], 662 | "title": "Workflow Success By Type", 663 | "tooltip": { 664 | "shared": true, 665 | "sort": 0, 666 | "value_type": "individual" 667 | }, 668 | "type": "graph", 669 | "xaxis": { 670 | "mode": "time", 671 | "show": true, 672 | "values": [] 673 | }, 674 | "yaxes": [ 675 | { 676 | "format": "short", 677 | "logBase": 1, 678 | "show": true 679 | }, 680 | { 681 | "format": "short", 682 | "logBase": 1, 683 | "show": true 684 | } 685 | ], 686 | "yaxis": { 687 | "align": false 688 | } 689 | }, 690 | { 691 | "aliasColors": {}, 692 | "bars": false, 693 | "dashLength": 10, 694 | "dashes": false, 695 | "datasource": "$datasource", 696 | "fieldConfig": { 697 | "defaults": { 698 | "links": [] 699 | }, 700 | "overrides": [] 701 | }, 702 | "fill": 1, 703 | "fillGradient": 0, 704 | "gridPos": { 705 | "h": 8, 706 | "w": 12, 707 | "x": 12, 708 | "y": 29 709 | }, 710 | "hiddenSeries": false, 711 | "id": 17, 712 | "legend": { 713 | "avg": false, 714 | "current": false, 715 | "max": false, 716 | "min": false, 717 | "show": true, 718 | "total": false, 719 | "values": false 720 | }, 721 | "lines": true, 722 | "linewidth": 1, 723 | "nullPointMode": "null", 724 | "options": { 725 | "alertThreshold": true 726 | }, 727 | "percentage": false, 728 | "pluginVersion": "8.3.4", 729 | "pointradius": 2, 730 | "points": false, 731 | "renderer": "flot", 732 | "seriesOverrides": [], 733 | "spaceLength": 10, 734 | "stack": false, 735 | "steppedLine": false, 736 | "targets": [ 737 | { 738 | "datasource": "$datasource", 739 | "exemplar": true, 740 | "expr": "sum by (temporal_namespace, workflow_type) (rate(temporal_workflow_failed[5m]))", 741 | "interval": "", 742 | "legendFormat": "{{ temporal_namespace }} - {{ workflow_type }}", 743 | "refId": "A" 744 | } 745 | ], 746 | "thresholds": [], 747 | "timeRegions": [], 748 | "title": "Workflow Failures By Type", 749 | "tooltip": { 750 | "shared": true, 751 | "sort": 0, 752 | "value_type": "individual" 753 | }, 754 | "type": "graph", 755 | "xaxis": { 756 | "mode": "time", 757 | "show": true, 758 | "values": [] 759 | }, 760 | "yaxes": [ 761 | { 762 | "format": "short", 763 | "logBase": 1, 764 | "show": true 765 | }, 766 | { 767 | "format": "short", 768 | "logBase": 1, 769 | "show": true 770 | } 771 | ], 772 | "yaxis": { 773 | "align": false 774 | } 775 | } 776 | ], 777 | "title": "Workflows", 778 | "type": "row" 779 | }, 780 | { 781 | "collapsed": false, 782 | "gridPos": { 783 | "h": 1, 784 | "w": 24, 785 | "x": 0, 786 | "y": 21 787 | }, 788 | "id": 20, 789 | "panels": [], 790 | "title": "Workflow Task Processing", 791 | "type": "row" 792 | }, 793 | { 794 | "aliasColors": {}, 795 | "bars": false, 796 | "dashLength": 10, 797 | "dashes": false, 798 | "datasource": "$datasource", 799 | "fieldConfig": { 800 | "defaults": { 801 | "links": [] 802 | }, 803 | "overrides": [] 804 | }, 805 | "fill": 1, 806 | "fillGradient": 0, 807 | "gridPos": { 808 | "h": 8, 809 | "w": 12, 810 | "x": 0, 811 | "y": 22 812 | }, 813 | "hiddenSeries": false, 814 | "id": 21, 815 | "legend": { 816 | "avg": false, 817 | "current": false, 818 | "max": false, 819 | "min": false, 820 | "show": true, 821 | "total": false, 822 | "values": false 823 | }, 824 | "lines": true, 825 | "linewidth": 1, 826 | "nullPointMode": "null", 827 | "options": { 828 | "alertThreshold": true 829 | }, 830 | "percentage": false, 831 | "pluginVersion": "8.3.4", 832 | "pointradius": 2, 833 | "points": false, 834 | "renderer": "flot", 835 | "seriesOverrides": [], 836 | "spaceLength": 10, 837 | "stack": false, 838 | "steppedLine": false, 839 | "targets": [ 840 | { 841 | "datasource": "$datasource", 842 | "exemplar": true, 843 | "expr": "sum by (temporal_namespace, workflow_type) (rate(temporal_workflow_task_queue_poll_succeed[5m]))", 844 | "interval": "", 845 | "legendFormat": "{{ temporal_namespace }} - {{ workflow_type }}", 846 | "refId": "A" 847 | } 848 | ], 849 | "thresholds": [], 850 | "timeRegions": [], 851 | "title": "Workflow Task Throughput By WorkflowType", 852 | "tooltip": { 853 | "shared": true, 854 | "sort": 0, 855 | "value_type": "individual" 856 | }, 857 | "type": "graph", 858 | "xaxis": { 859 | "mode": "time", 860 | "show": true, 861 | "values": [] 862 | }, 863 | "yaxes": [ 864 | { 865 | "format": "short", 866 | "logBase": 1, 867 | "show": true 868 | }, 869 | { 870 | "format": "short", 871 | "logBase": 1, 872 | "show": true 873 | } 874 | ], 875 | "yaxis": { 876 | "align": false 877 | } 878 | }, 879 | { 880 | "aliasColors": {}, 881 | "bars": false, 882 | "dashLength": 10, 883 | "dashes": false, 884 | "datasource": "$datasource", 885 | "fieldConfig": { 886 | "defaults": { 887 | "links": [] 888 | }, 889 | "overrides": [] 890 | }, 891 | "fill": 1, 892 | "fillGradient": 0, 893 | "gridPos": { 894 | "h": 8, 895 | "w": 12, 896 | "x": 12, 897 | "y": 22 898 | }, 899 | "hiddenSeries": false, 900 | "id": 18, 901 | "legend": { 902 | "avg": false, 903 | "current": false, 904 | "max": false, 905 | "min": false, 906 | "show": true, 907 | "total": false, 908 | "values": false 909 | }, 910 | "lines": true, 911 | "linewidth": 1, 912 | "nullPointMode": "null", 913 | "options": { 914 | "alertThreshold": true 915 | }, 916 | "percentage": false, 917 | "pluginVersion": "8.3.4", 918 | "pointradius": 2, 919 | "points": false, 920 | "renderer": "flot", 921 | "seriesOverrides": [], 922 | "spaceLength": 10, 923 | "stack": false, 924 | "steppedLine": false, 925 | "targets": [ 926 | { 927 | "datasource": "$datasource", 928 | "exemplar": true, 929 | "expr": "sum by (temporal_namespace) (rate(temporal_workflow_task_queue_poll_succeed[5m]))", 930 | "interval": "", 931 | "legendFormat": "{{ temporal_namespace }}", 932 | "refId": "A" 933 | } 934 | ], 935 | "thresholds": [], 936 | "timeRegions": [], 937 | "title": "Workflow Task Throughput By Namespace", 938 | "tooltip": { 939 | "shared": true, 940 | "sort": 0, 941 | "value_type": "individual" 942 | }, 943 | "type": "graph", 944 | "xaxis": { 945 | "mode": "time", 946 | "show": true, 947 | "values": [] 948 | }, 949 | "yaxes": [ 950 | { 951 | "format": "short", 952 | "logBase": 1, 953 | "show": true 954 | }, 955 | { 956 | "format": "short", 957 | "logBase": 1, 958 | "show": true 959 | } 960 | ], 961 | "yaxis": { 962 | "align": false 963 | } 964 | }, 965 | { 966 | "aliasColors": {}, 967 | "bars": false, 968 | "dashLength": 10, 969 | "dashes": false, 970 | "datasource": "$datasource", 971 | "fieldConfig": { 972 | "defaults": { 973 | "links": [] 974 | }, 975 | "overrides": [] 976 | }, 977 | "fill": 1, 978 | "fillGradient": 0, 979 | "gridPos": { 980 | "h": 8, 981 | "w": 24, 982 | "x": 0, 983 | "y": 30 984 | }, 985 | "hiddenSeries": false, 986 | "id": 14, 987 | "legend": { 988 | "avg": false, 989 | "current": false, 990 | "max": false, 991 | "min": false, 992 | "show": true, 993 | "total": false, 994 | "values": false 995 | }, 996 | "lines": true, 997 | "linewidth": 1, 998 | "nullPointMode": "null", 999 | "options": { 1000 | "alertThreshold": true 1001 | }, 1002 | "percentage": false, 1003 | "pluginVersion": "8.3.4", 1004 | "pointradius": 2, 1005 | "points": false, 1006 | "renderer": "flot", 1007 | "seriesOverrides": [], 1008 | "spaceLength": 10, 1009 | "stack": false, 1010 | "steppedLine": false, 1011 | "targets": [ 1012 | { 1013 | "datasource": "$datasource", 1014 | "exemplar": true, 1015 | "expr": "sum by (temporal_namespace) (rate(temporal_workflow_task_queue_poll_empty[5m]))", 1016 | "interval": "", 1017 | "legendFormat": "Empty Poll - {{ temporal_namespace }}", 1018 | "refId": "A" 1019 | } 1020 | ], 1021 | "thresholds": [], 1022 | "timeRegions": [], 1023 | "title": "Empty Polls", 1024 | "tooltip": { 1025 | "shared": true, 1026 | "sort": 0, 1027 | "value_type": "individual" 1028 | }, 1029 | "type": "graph", 1030 | "xaxis": { 1031 | "mode": "time", 1032 | "show": true, 1033 | "values": [] 1034 | }, 1035 | "yaxes": [ 1036 | { 1037 | "format": "short", 1038 | "logBase": 1, 1039 | "show": true 1040 | }, 1041 | { 1042 | "format": "short", 1043 | "logBase": 1, 1044 | "show": true 1045 | } 1046 | ], 1047 | "yaxis": { 1048 | "align": false 1049 | } 1050 | }, 1051 | { 1052 | "aliasColors": {}, 1053 | "bars": false, 1054 | "dashLength": 10, 1055 | "dashes": false, 1056 | "datasource": "$datasource", 1057 | "fieldConfig": { 1058 | "defaults": { 1059 | "links": [] 1060 | }, 1061 | "overrides": [] 1062 | }, 1063 | "fill": 1, 1064 | "fillGradient": 0, 1065 | "gridPos": { 1066 | "h": 8, 1067 | "w": 12, 1068 | "x": 0, 1069 | "y": 38 1070 | }, 1071 | "hiddenSeries": false, 1072 | "id": 25, 1073 | "legend": { 1074 | "avg": false, 1075 | "current": false, 1076 | "max": false, 1077 | "min": false, 1078 | "show": true, 1079 | "total": false, 1080 | "values": false 1081 | }, 1082 | "lines": true, 1083 | "linewidth": 1, 1084 | "nullPointMode": "null", 1085 | "options": { 1086 | "alertThreshold": true 1087 | }, 1088 | "percentage": false, 1089 | "pluginVersion": "8.3.4", 1090 | "pointradius": 2, 1091 | "points": false, 1092 | "renderer": "flot", 1093 | "seriesOverrides": [], 1094 | "spaceLength": 10, 1095 | "stack": false, 1096 | "steppedLine": false, 1097 | "targets": [ 1098 | { 1099 | "datasource": "$datasource", 1100 | "exemplar": true, 1101 | "expr": "histogram_quantile(0.95, sum(rate(temporal_workflow_task_execution_latency_bucket[5m])) by (temporal_namespace, le))", 1102 | "interval": "", 1103 | "legendFormat": "{{ temporal_namespace }}", 1104 | "refId": "A" 1105 | } 1106 | ], 1107 | "thresholds": [], 1108 | "timeRegions": [], 1109 | "title": "Workflow Task Execution Latency By Namespace", 1110 | "tooltip": { 1111 | "shared": true, 1112 | "sort": 0, 1113 | "value_type": "individual" 1114 | }, 1115 | "type": "graph", 1116 | "xaxis": { 1117 | "mode": "time", 1118 | "show": true, 1119 | "values": [] 1120 | }, 1121 | "yaxes": [ 1122 | { 1123 | "format": "dtdurations", 1124 | "logBase": 1, 1125 | "show": true 1126 | }, 1127 | { 1128 | "format": "short", 1129 | "logBase": 1, 1130 | "show": true 1131 | } 1132 | ], 1133 | "yaxis": { 1134 | "align": false 1135 | } 1136 | }, 1137 | { 1138 | "aliasColors": {}, 1139 | "bars": false, 1140 | "dashLength": 10, 1141 | "dashes": false, 1142 | "datasource": "$datasource", 1143 | "fieldConfig": { 1144 | "defaults": { 1145 | "links": [] 1146 | }, 1147 | "overrides": [] 1148 | }, 1149 | "fill": 1, 1150 | "fillGradient": 0, 1151 | "gridPos": { 1152 | "h": 8, 1153 | "w": 12, 1154 | "x": 12, 1155 | "y": 38 1156 | }, 1157 | "hiddenSeries": false, 1158 | "id": 23, 1159 | "legend": { 1160 | "avg": false, 1161 | "current": false, 1162 | "max": false, 1163 | "min": false, 1164 | "show": true, 1165 | "total": false, 1166 | "values": false 1167 | }, 1168 | "lines": true, 1169 | "linewidth": 1, 1170 | "nullPointMode": "null", 1171 | "options": { 1172 | "alertThreshold": true 1173 | }, 1174 | "percentage": false, 1175 | "pluginVersion": "8.3.4", 1176 | "pointradius": 2, 1177 | "points": false, 1178 | "renderer": "flot", 1179 | "seriesOverrides": [], 1180 | "spaceLength": 10, 1181 | "stack": false, 1182 | "steppedLine": false, 1183 | "targets": [ 1184 | { 1185 | "datasource": "$datasource", 1186 | "exemplar": true, 1187 | "expr": "histogram_quantile(0.95, sum(rate(temporal_workflow_task_execution_latency_bucket[5m])) by (temporal_namespace, workflow_type, le))", 1188 | "interval": "", 1189 | "legendFormat": "{{ temporal_namespace }} -- {{ workflow_type }}", 1190 | "refId": "A" 1191 | } 1192 | ], 1193 | "thresholds": [], 1194 | "timeRegions": [], 1195 | "title": "Workflow Task Backlog By Workflow Type", 1196 | "tooltip": { 1197 | "shared": true, 1198 | "sort": 0, 1199 | "value_type": "individual" 1200 | }, 1201 | "type": "graph", 1202 | "xaxis": { 1203 | "mode": "time", 1204 | "show": true, 1205 | "values": [] 1206 | }, 1207 | "yaxes": [ 1208 | { 1209 | "format": "dtdurations", 1210 | "logBase": 1, 1211 | "show": true 1212 | }, 1213 | { 1214 | "format": "short", 1215 | "logBase": 1, 1216 | "show": true 1217 | } 1218 | ], 1219 | "yaxis": { 1220 | "align": false 1221 | } 1222 | }, 1223 | { 1224 | "collapsed": false, 1225 | "gridPos": { 1226 | "h": 1, 1227 | "w": 24, 1228 | "x": 0, 1229 | "y": 46 1230 | }, 1231 | "id": 42, 1232 | "panels": [], 1233 | "title": "Activities", 1234 | "type": "row" 1235 | }, 1236 | { 1237 | "aliasColors": {}, 1238 | "bars": false, 1239 | "dashLength": 10, 1240 | "dashes": false, 1241 | "datasource": "$datasource", 1242 | "fieldConfig": { 1243 | "defaults": { 1244 | "links": [] 1245 | }, 1246 | "overrides": [] 1247 | }, 1248 | "fill": 1, 1249 | "fillGradient": 0, 1250 | "gridPos": { 1251 | "h": 8, 1252 | "w": 12, 1253 | "x": 0, 1254 | "y": 47 1255 | }, 1256 | "hiddenSeries": false, 1257 | "id": 38, 1258 | "legend": { 1259 | "avg": false, 1260 | "current": false, 1261 | "max": false, 1262 | "min": false, 1263 | "show": true, 1264 | "total": false, 1265 | "values": false 1266 | }, 1267 | "lines": true, 1268 | "linewidth": 1, 1269 | "nullPointMode": "null", 1270 | "options": { 1271 | "alertThreshold": true 1272 | }, 1273 | "percentage": false, 1274 | "pluginVersion": "8.3.4", 1275 | "pointradius": 2, 1276 | "points": false, 1277 | "renderer": "flot", 1278 | "seriesOverrides": [], 1279 | "spaceLength": 10, 1280 | "stack": false, 1281 | "steppedLine": false, 1282 | "targets": [ 1283 | { 1284 | "datasource": "$datasource", 1285 | "exemplar": true, 1286 | "expr": "sum by (temporal_namespace) (rate(temporal_activity_execution_latency_count[5m]))", 1287 | "interval": "", 1288 | "legendFormat": "latency count", 1289 | "refId": "A" 1290 | } 1291 | ], 1292 | "thresholds": [], 1293 | "timeRegions": [], 1294 | "title": "Activity Throughput By Namespace", 1295 | "tooltip": { 1296 | "shared": true, 1297 | "sort": 0, 1298 | "value_type": "individual" 1299 | }, 1300 | "type": "graph", 1301 | "xaxis": { 1302 | "mode": "time", 1303 | "show": true, 1304 | "values": [] 1305 | }, 1306 | "yaxes": [ 1307 | { 1308 | "format": "short", 1309 | "logBase": 1, 1310 | "show": true 1311 | }, 1312 | { 1313 | "format": "short", 1314 | "logBase": 1, 1315 | "show": true 1316 | } 1317 | ], 1318 | "yaxis": { 1319 | "align": false 1320 | } 1321 | }, 1322 | { 1323 | "aliasColors": {}, 1324 | "bars": false, 1325 | "dashLength": 10, 1326 | "dashes": false, 1327 | "datasource": "$datasource", 1328 | "fieldConfig": { 1329 | "defaults": { 1330 | "links": [] 1331 | }, 1332 | "overrides": [] 1333 | }, 1334 | "fill": 1, 1335 | "fillGradient": 0, 1336 | "gridPos": { 1337 | "h": 8, 1338 | "w": 12, 1339 | "x": 12, 1340 | "y": 47 1341 | }, 1342 | "hiddenSeries": false, 1343 | "id": 40, 1344 | "legend": { 1345 | "avg": false, 1346 | "current": false, 1347 | "max": false, 1348 | "min": false, 1349 | "show": true, 1350 | "total": false, 1351 | "values": false 1352 | }, 1353 | "lines": true, 1354 | "linewidth": 1, 1355 | "nullPointMode": "null", 1356 | "options": { 1357 | "alertThreshold": true 1358 | }, 1359 | "percentage": false, 1360 | "pluginVersion": "8.3.4", 1361 | "pointradius": 2, 1362 | "points": false, 1363 | "renderer": "flot", 1364 | "seriesOverrides": [], 1365 | "spaceLength": 10, 1366 | "stack": false, 1367 | "steppedLine": false, 1368 | "targets": [ 1369 | { 1370 | "datasource": "$datasource", 1371 | "exemplar": true, 1372 | "expr": "sum by (temporal_namespace, activity_type) (rate(temporal_activity_execution_latency_count[5m]))", 1373 | "interval": "", 1374 | "legendFormat": "{{ activity_type }}", 1375 | "refId": "A" 1376 | } 1377 | ], 1378 | "thresholds": [], 1379 | "timeRegions": [], 1380 | "title": "Activity Throughput By Activity Type", 1381 | "tooltip": { 1382 | "shared": true, 1383 | "sort": 0, 1384 | "value_type": "individual" 1385 | }, 1386 | "type": "graph", 1387 | "xaxis": { 1388 | "mode": "time", 1389 | "show": true, 1390 | "values": [] 1391 | }, 1392 | "yaxes": [ 1393 | { 1394 | "format": "short", 1395 | "logBase": 1, 1396 | "show": true 1397 | }, 1398 | { 1399 | "format": "short", 1400 | "logBase": 1, 1401 | "show": true 1402 | } 1403 | ], 1404 | "yaxis": { 1405 | "align": false 1406 | } 1407 | }, 1408 | { 1409 | "aliasColors": {}, 1410 | "bars": false, 1411 | "dashLength": 10, 1412 | "dashes": false, 1413 | "datasource": "$datasource", 1414 | "fieldConfig": { 1415 | "defaults": { 1416 | "links": [] 1417 | }, 1418 | "overrides": [] 1419 | }, 1420 | "fill": 1, 1421 | "fillGradient": 0, 1422 | "gridPos": { 1423 | "h": 9, 1424 | "w": 24, 1425 | "x": 0, 1426 | "y": 55 1427 | }, 1428 | "hiddenSeries": false, 1429 | "id": 44, 1430 | "legend": { 1431 | "avg": false, 1432 | "current": false, 1433 | "max": false, 1434 | "min": false, 1435 | "show": true, 1436 | "total": false, 1437 | "values": false 1438 | }, 1439 | "lines": true, 1440 | "linewidth": 1, 1441 | "nullPointMode": "null", 1442 | "options": { 1443 | "alertThreshold": true 1444 | }, 1445 | "percentage": false, 1446 | "pluginVersion": "8.3.4", 1447 | "pointradius": 2, 1448 | "points": false, 1449 | "renderer": "flot", 1450 | "seriesOverrides": [], 1451 | "spaceLength": 10, 1452 | "stack": false, 1453 | "steppedLine": false, 1454 | "targets": [ 1455 | { 1456 | "datasource": "$datasource", 1457 | "exemplar": true, 1458 | "expr": "sum by (temporal_namespace, activity_type) (rate(temporal_activity_execution_failed[5m]))", 1459 | "interval": "", 1460 | "legendFormat": "{{ temporal_namespace }} - {{ activity_type }}", 1461 | "refId": "A" 1462 | } 1463 | ], 1464 | "thresholds": [], 1465 | "timeRegions": [], 1466 | "title": "Failed Activity by Type", 1467 | "tooltip": { 1468 | "shared": true, 1469 | "sort": 0, 1470 | "value_type": "individual" 1471 | }, 1472 | "type": "graph", 1473 | "xaxis": { 1474 | "mode": "time", 1475 | "show": true, 1476 | "values": [] 1477 | }, 1478 | "yaxes": [ 1479 | { 1480 | "format": "short", 1481 | "logBase": 1, 1482 | "show": true 1483 | }, 1484 | { 1485 | "format": "short", 1486 | "logBase": 1, 1487 | "show": true 1488 | } 1489 | ], 1490 | "yaxis": { 1491 | "align": false 1492 | } 1493 | }, 1494 | { 1495 | "aliasColors": {}, 1496 | "bars": false, 1497 | "dashLength": 10, 1498 | "dashes": false, 1499 | "datasource": "$datasource", 1500 | "fieldConfig": { 1501 | "defaults": { 1502 | "links": [] 1503 | }, 1504 | "overrides": [] 1505 | }, 1506 | "fill": 1, 1507 | "fillGradient": 0, 1508 | "gridPos": { 1509 | "h": 8, 1510 | "w": 12, 1511 | "x": 0, 1512 | "y": 64 1513 | }, 1514 | "hiddenSeries": false, 1515 | "id": 46, 1516 | "legend": { 1517 | "avg": false, 1518 | "current": false, 1519 | "max": false, 1520 | "min": false, 1521 | "show": true, 1522 | "total": false, 1523 | "values": false 1524 | }, 1525 | "lines": true, 1526 | "linewidth": 1, 1527 | "nullPointMode": "null", 1528 | "options": { 1529 | "alertThreshold": true 1530 | }, 1531 | "percentage": false, 1532 | "pluginVersion": "8.3.4", 1533 | "pointradius": 2, 1534 | "points": false, 1535 | "renderer": "flot", 1536 | "seriesOverrides": [], 1537 | "spaceLength": 10, 1538 | "stack": false, 1539 | "steppedLine": false, 1540 | "targets": [ 1541 | { 1542 | "datasource": "$datasource", 1543 | "exemplar": true, 1544 | "expr": "histogram_quantile(0.95, sum(rate(temporal_activity_execution_latency_bucket[5m])) by (temporal_namespace, activity_type, le))", 1545 | "interval": "", 1546 | "legendFormat": "{{ temporal_namespace }} - {{ activity_type }}", 1547 | "refId": "A" 1548 | } 1549 | ], 1550 | "thresholds": [], 1551 | "timeRegions": [], 1552 | "title": "Activity Execution Latencies", 1553 | "tooltip": { 1554 | "shared": true, 1555 | "sort": 0, 1556 | "value_type": "individual" 1557 | }, 1558 | "type": "graph", 1559 | "xaxis": { 1560 | "mode": "time", 1561 | "show": true, 1562 | "values": [] 1563 | }, 1564 | "yaxes": [ 1565 | { 1566 | "format": "dtdurations", 1567 | "logBase": 1, 1568 | "show": true 1569 | }, 1570 | { 1571 | "format": "short", 1572 | "logBase": 1, 1573 | "show": true 1574 | } 1575 | ], 1576 | "yaxis": { 1577 | "align": false 1578 | } 1579 | }, 1580 | { 1581 | "aliasColors": {}, 1582 | "bars": false, 1583 | "dashLength": 10, 1584 | "dashes": false, 1585 | "datasource": "$datasource", 1586 | "fieldConfig": { 1587 | "defaults": { 1588 | "links": [] 1589 | }, 1590 | "overrides": [] 1591 | }, 1592 | "fill": 1, 1593 | "fillGradient": 0, 1594 | "gridPos": { 1595 | "h": 8, 1596 | "w": 12, 1597 | "x": 12, 1598 | "y": 64 1599 | }, 1600 | "hiddenSeries": false, 1601 | "id": 48, 1602 | "legend": { 1603 | "avg": false, 1604 | "current": false, 1605 | "max": false, 1606 | "min": false, 1607 | "show": true, 1608 | "total": false, 1609 | "values": false 1610 | }, 1611 | "lines": true, 1612 | "linewidth": 1, 1613 | "nullPointMode": "null", 1614 | "options": { 1615 | "alertThreshold": true 1616 | }, 1617 | "percentage": false, 1618 | "pluginVersion": "8.3.4", 1619 | "pointradius": 2, 1620 | "points": false, 1621 | "renderer": "flot", 1622 | "seriesOverrides": [], 1623 | "spaceLength": 10, 1624 | "stack": false, 1625 | "steppedLine": false, 1626 | "targets": [ 1627 | { 1628 | "datasource": "$datasource", 1629 | "exemplar": true, 1630 | "expr": "histogram_quantile(0.95, sum(rate(temporal_activity_endtoend_latency_bucket[5m])) by (temporal_namespace, activity_type, le))", 1631 | "interval": "", 1632 | "legendFormat": "", 1633 | "refId": "A" 1634 | } 1635 | ], 1636 | "thresholds": [], 1637 | "timeRegions": [], 1638 | "title": "Activity End-To-End Latencies", 1639 | "tooltip": { 1640 | "shared": true, 1641 | "sort": 0, 1642 | "value_type": "individual" 1643 | }, 1644 | "type": "graph", 1645 | "xaxis": { 1646 | "mode": "time", 1647 | "show": true, 1648 | "values": [] 1649 | }, 1650 | "yaxes": [ 1651 | { 1652 | "format": "dtdurations", 1653 | "logBase": 1, 1654 | "show": true 1655 | }, 1656 | { 1657 | "format": "short", 1658 | "logBase": 1, 1659 | "show": true 1660 | } 1661 | ], 1662 | "yaxis": { 1663 | "align": false 1664 | } 1665 | }, 1666 | { 1667 | "collapsed": false, 1668 | "gridPos": { 1669 | "h": 1, 1670 | "w": 24, 1671 | "x": 0, 1672 | "y": 72 1673 | }, 1674 | "id": 34, 1675 | "panels": [], 1676 | "title": "Activity Task Processing", 1677 | "type": "row" 1678 | }, 1679 | { 1680 | "aliasColors": {}, 1681 | "bars": false, 1682 | "dashLength": 10, 1683 | "dashes": false, 1684 | "datasource": "$datasource", 1685 | "fieldConfig": { 1686 | "defaults": { 1687 | "links": [] 1688 | }, 1689 | "overrides": [] 1690 | }, 1691 | "fill": 1, 1692 | "fillGradient": 0, 1693 | "gridPos": { 1694 | "h": 8, 1695 | "w": 12, 1696 | "x": 0, 1697 | "y": 73 1698 | }, 1699 | "hiddenSeries": false, 1700 | "id": 31, 1701 | "legend": { 1702 | "avg": false, 1703 | "current": false, 1704 | "max": false, 1705 | "min": false, 1706 | "show": true, 1707 | "total": false, 1708 | "values": false 1709 | }, 1710 | "lines": true, 1711 | "linewidth": 1, 1712 | "nullPointMode": "null", 1713 | "options": { 1714 | "alertThreshold": true 1715 | }, 1716 | "percentage": false, 1717 | "pluginVersion": "8.3.4", 1718 | "pointradius": 2, 1719 | "points": false, 1720 | "renderer": "flot", 1721 | "seriesOverrides": [], 1722 | "spaceLength": 10, 1723 | "stack": false, 1724 | "steppedLine": false, 1725 | "targets": [ 1726 | { 1727 | "datasource": "$datasource", 1728 | "exemplar": true, 1729 | "expr": "sum by (temporal_namespace) (rate(temporal_activity_poll_no_task[5m]))", 1730 | "interval": "", 1731 | "legendFormat": "{{ temporal_namespace }}", 1732 | "refId": "A" 1733 | } 1734 | ], 1735 | "thresholds": [], 1736 | "timeRegions": [], 1737 | "title": "Empty Activity Polls By Namespace", 1738 | "tooltip": { 1739 | "shared": true, 1740 | "sort": 0, 1741 | "value_type": "individual" 1742 | }, 1743 | "type": "graph", 1744 | "xaxis": { 1745 | "mode": "time", 1746 | "show": true, 1747 | "values": [] 1748 | }, 1749 | "yaxes": [ 1750 | { 1751 | "format": "short", 1752 | "logBase": 1, 1753 | "show": true 1754 | }, 1755 | { 1756 | "format": "short", 1757 | "logBase": 1, 1758 | "show": true 1759 | } 1760 | ], 1761 | "yaxis": { 1762 | "align": false 1763 | } 1764 | }, 1765 | { 1766 | "aliasColors": {}, 1767 | "bars": false, 1768 | "dashLength": 10, 1769 | "dashes": false, 1770 | "datasource": "$datasource", 1771 | "fieldConfig": { 1772 | "defaults": { 1773 | "links": [] 1774 | }, 1775 | "overrides": [] 1776 | }, 1777 | "fill": 1, 1778 | "fillGradient": 0, 1779 | "gridPos": { 1780 | "h": 8, 1781 | "w": 12, 1782 | "x": 12, 1783 | "y": 73 1784 | }, 1785 | "hiddenSeries": false, 1786 | "id": 32, 1787 | "legend": { 1788 | "avg": false, 1789 | "current": false, 1790 | "max": false, 1791 | "min": false, 1792 | "show": true, 1793 | "total": false, 1794 | "values": false 1795 | }, 1796 | "lines": true, 1797 | "linewidth": 1, 1798 | "nullPointMode": "null", 1799 | "options": { 1800 | "alertThreshold": true 1801 | }, 1802 | "percentage": false, 1803 | "pluginVersion": "8.3.4", 1804 | "pointradius": 2, 1805 | "points": false, 1806 | "renderer": "flot", 1807 | "seriesOverrides": [], 1808 | "spaceLength": 10, 1809 | "stack": false, 1810 | "steppedLine": false, 1811 | "targets": [ 1812 | { 1813 | "datasource": "$datasource", 1814 | "exemplar": true, 1815 | "expr": "histogram_quantile(0.95, sum(rate(temporal_activity_schedule_to_start_latency_bucket[5m])) by (temporal_namespace, le))", 1816 | "interval": "", 1817 | "legendFormat": "", 1818 | "refId": "A" 1819 | } 1820 | ], 1821 | "thresholds": [], 1822 | "timeRegions": [], 1823 | "title": "Activity Task Backlog By Namespace", 1824 | "tooltip": { 1825 | "shared": true, 1826 | "sort": 0, 1827 | "value_type": "individual" 1828 | }, 1829 | "type": "graph", 1830 | "xaxis": { 1831 | "mode": "time", 1832 | "show": true, 1833 | "values": [] 1834 | }, 1835 | "yaxes": [ 1836 | { 1837 | "format": "dtdurationms", 1838 | "logBase": 1, 1839 | "show": true 1840 | }, 1841 | { 1842 | "format": "short", 1843 | "logBase": 1, 1844 | "show": true 1845 | } 1846 | ], 1847 | "yaxis": { 1848 | "align": false 1849 | } 1850 | }, 1851 | { 1852 | "aliasColors": {}, 1853 | "bars": false, 1854 | "dashLength": 10, 1855 | "dashes": false, 1856 | "datasource": "$datasource", 1857 | "fieldConfig": { 1858 | "defaults": { 1859 | "links": [] 1860 | }, 1861 | "overrides": [] 1862 | }, 1863 | "fill": 1, 1864 | "fillGradient": 0, 1865 | "gridPos": { 1866 | "h": 8, 1867 | "w": 12, 1868 | "x": 0, 1869 | "y": 81 1870 | }, 1871 | "hiddenSeries": false, 1872 | "id": 36, 1873 | "legend": { 1874 | "avg": false, 1875 | "current": false, 1876 | "max": false, 1877 | "min": false, 1878 | "show": true, 1879 | "total": false, 1880 | "values": false 1881 | }, 1882 | "lines": true, 1883 | "linewidth": 1, 1884 | "nullPointMode": "null", 1885 | "options": { 1886 | "alertThreshold": true 1887 | }, 1888 | "percentage": false, 1889 | "pluginVersion": "8.3.4", 1890 | "pointradius": 2, 1891 | "points": false, 1892 | "renderer": "flot", 1893 | "seriesOverrides": [], 1894 | "spaceLength": 10, 1895 | "stack": false, 1896 | "steppedLine": false, 1897 | "targets": [ 1898 | { 1899 | "datasource": "$datasource", 1900 | "exemplar": true, 1901 | "expr": "histogram_quantile(0.95, sum(rate(temporal_activity_schedule_to_start_latency_bucket[5m])) by (temporal_namespace, activity_type, le))", 1902 | "interval": "", 1903 | "legendFormat": "{{ activity_type }}", 1904 | "refId": "A" 1905 | } 1906 | ], 1907 | "thresholds": [], 1908 | "timeRegions": [], 1909 | "title": "Activity Task Backlog By Activity Type", 1910 | "tooltip": { 1911 | "shared": true, 1912 | "sort": 0, 1913 | "value_type": "individual" 1914 | }, 1915 | "type": "graph", 1916 | "xaxis": { 1917 | "mode": "time", 1918 | "show": true, 1919 | "values": [] 1920 | }, 1921 | "yaxes": [ 1922 | { 1923 | "format": "dtdurationms", 1924 | "logBase": 1, 1925 | "show": true 1926 | }, 1927 | { 1928 | "format": "short", 1929 | "logBase": 1, 1930 | "show": true 1931 | } 1932 | ], 1933 | "yaxis": { 1934 | "align": false 1935 | } 1936 | } 1937 | ], 1938 | "schemaVersion": 34, 1939 | "style": "dark", 1940 | "tags": [], 1941 | "templating": { 1942 | "list": [ 1943 | { 1944 | "current": { 1945 | "selected": false, 1946 | "text": "default", 1947 | "value": "default" 1948 | }, 1949 | "description": null, 1950 | "error": null, 1951 | "hide": 0, 1952 | "includeAll": false, 1953 | "label": null, 1954 | "multi": false, 1955 | "name": "datasource", 1956 | "options": [], 1957 | "query": "prometheus", 1958 | "queryValue": "", 1959 | "refresh": 1, 1960 | "regex": "", 1961 | "skipUrlSync": false, 1962 | "type": "datasource" 1963 | }, 1964 | { 1965 | "allValue": ".*", 1966 | "current": { 1967 | "selected": true, 1968 | "text": "All", 1969 | "value": "$__all" 1970 | }, 1971 | "hide": 0, 1972 | "includeAll": true, 1973 | "multi": false, 1974 | "name": "Namespace", 1975 | "options": [ 1976 | { 1977 | "selected": true, 1978 | "text": "All", 1979 | "value": "$__all" 1980 | } 1981 | ], 1982 | "query": "", 1983 | "queryValue": "", 1984 | "skipUrlSync": false, 1985 | "type": "custom" 1986 | }, 1987 | { 1988 | "allValue": ".*", 1989 | "current": { 1990 | "selected": true, 1991 | "text": "All", 1992 | "value": "$__all" 1993 | }, 1994 | "hide": 0, 1995 | "includeAll": true, 1996 | "multi": false, 1997 | "name": "WorkflowType", 1998 | "options": [ 1999 | { 2000 | "selected": true, 2001 | "text": "All", 2002 | "value": "$__all" 2003 | } 2004 | ], 2005 | "query": "", 2006 | "skipUrlSync": false, 2007 | "type": "custom" 2008 | } 2009 | ] 2010 | }, 2011 | "time": { 2012 | "from": "now-1h", 2013 | "to": "now" 2014 | }, 2015 | "timepicker": { 2016 | "refresh_intervals": [ 2017 | "30s" 2018 | ] 2019 | }, 2020 | "timezone": "", 2021 | "title": "Background Checks Application SDK Metrics", 2022 | "uid": "XXEJjl17k", 2023 | "version": 1, 2024 | "weekStart": "" 2025 | } -------------------------------------------------------------------------------- /deployment/grafana/provisioning/dashboards/all.yml: -------------------------------------------------------------------------------- 1 | - name: 'default' 2 | org_id: 1 3 | folder: '' 4 | type: 'file' 5 | options: 6 | folder: '/var/lib/grafana/dashboards' -------------------------------------------------------------------------------- /deployment/grafana/provisioning/datasources/all.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: 'Temporal Prometheus' 5 | type: 'prometheus' 6 | org_id: 1 7 | url: 'http://prometheus:9090' 8 | is_default: true 9 | version: 1 10 | editable: true -------------------------------------------------------------------------------- /deployment/prometheus/config.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | external_labels: 4 | monitor: 'temporal-monitor' 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | metrics_path: /metrics 8 | scheme: http 9 | static_configs: 10 | - targets: 11 | - 'temporal:8000' 12 | - 'worker:8001' 13 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 AS build 2 | 3 | WORKDIR /go/src/thirdparty-simulator 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY api ./api 10 | COPY cmd ./cmd 11 | COPY main.go ./main.go 12 | 13 | RUN go install -v ./ 14 | 15 | FROM golang:1.17 16 | 17 | COPY --from=build /go/bin/thirdparty-simulator /go/bin/thirdparty-simulator 18 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "regexp" 11 | 12 | "github.com/github/go-fault" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | const DefaultEndpoint = "0.0.0.0:8082" 17 | 18 | // For convenience this fake third party API happens to take the same shape inputs and return the same 19 | // shape results as our application uses. 20 | 21 | type SSNTraceInput struct { 22 | FullName string 23 | SSN string 24 | } 25 | 26 | type SSNTraceResult struct { 27 | SSNIsValid bool 28 | KnownAddresses []string 29 | } 30 | 31 | type MotorVehicleIncidentSearchInput struct { 32 | FullName string 33 | Address string 34 | } 35 | 36 | type MotorVehicleIncidentSearchResult struct { 37 | LicenseValid bool 38 | MotorVehicleIncidents []string 39 | } 40 | 41 | type FederalCriminalSearchInput struct { 42 | FullName string 43 | Address string 44 | } 45 | 46 | type FederalCriminalSearchResult struct { 47 | Crimes []string 48 | } 49 | 50 | type StateCriminalSearchInput struct { 51 | FullName string 52 | Address string 53 | } 54 | 55 | type StateCriminalSearchResult struct { 56 | FullName string 57 | Address string 58 | Crimes []string 59 | } 60 | 61 | func handleSsnTrace(w http.ResponseWriter, r *http.Request) { 62 | var input SSNTraceInput 63 | 64 | err := json.NewDecoder(r.Body).Decode(&input) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusBadRequest) 67 | return 68 | } 69 | 70 | var result SSNTraceResult 71 | 72 | var validSSN = regexp.MustCompile(`\d{3}-\d{2}-\d{4}$`) 73 | result.SSNIsValid = validSSN.MatchString(input.SSN) 74 | 75 | addressMap := map[string][]string{ 76 | "111-11-1111": {"123 Broadway, New York, NY 10011", "1 E. 161 St, Bronx, NY 10451", "41 Seaver Way, Queens, NY 11368"}, 77 | "222-22-2222": {"456 Oak Street, Springfield, IL 62706", "1060 W. Addison St, Chicago, IL 60613"}, 78 | "333-33-3333": {"4 Jersey St, Boston, MA 02215", "333 W Camden St, Baltimore, MD 21201"}, 79 | "444-44-4444": {"1 Royal Way, Kansas City, MO 64129", "", "700 Clark Ave, St Louis, MO 63102"}} 80 | 81 | result.KnownAddresses = addressMap[input.SSN] 82 | 83 | w.Header().Set("Content-Type", "application/json") 84 | json.NewEncoder(w).Encode(result) 85 | } 86 | 87 | func handleMotorVehicleSearch(w http.ResponseWriter, r *http.Request) { 88 | var input MotorVehicleIncidentSearchInput 89 | 90 | err := json.NewDecoder(r.Body).Decode(&input) 91 | if err != nil { 92 | http.Error(w, err.Error(), http.StatusBadRequest) 93 | return 94 | } 95 | 96 | var result MotorVehicleIncidentSearchResult 97 | var motorVehicleIncidents []string 98 | 99 | possibleMotorVehicleIncidents := []string{ 100 | "Speeding", 101 | "Reckless Driving", 102 | "Driving Without Insurance", 103 | "Driving Under the Influence", 104 | } 105 | 106 | rndnum := rand.Intn(100) 107 | if rndnum > 25 { 108 | motorVehicleIncidents = append(motorVehicleIncidents, possibleMotorVehicleIncidents[rand.Intn(len(possibleMotorVehicleIncidents))]) 109 | result.LicenseValid = false 110 | } else { 111 | result.LicenseValid = true 112 | } 113 | 114 | result.MotorVehicleIncidents = motorVehicleIncidents 115 | 116 | w.Header().Set("Content-Type", "application/json") 117 | json.NewEncoder(w).Encode(result) 118 | } 119 | 120 | func handleFederalCriminalSearch(w http.ResponseWriter, r *http.Request) { 121 | var input FederalCriminalSearchInput 122 | 123 | err := json.NewDecoder(r.Body).Decode(&input) 124 | if err != nil { 125 | http.Error(w, err.Error(), http.StatusBadRequest) 126 | return 127 | } 128 | 129 | var result FederalCriminalSearchResult 130 | var crimes []string 131 | 132 | possibleCrimes := []string{ 133 | "Money Laundering", 134 | "Racketeering", 135 | "Counterfeiting", 136 | "Espionage", 137 | } 138 | 139 | rndnum := rand.Intn(100) 140 | if rndnum > 75 { 141 | crimes = append(crimes, possibleCrimes[rand.Intn(len(possibleCrimes))]) 142 | } 143 | result.Crimes = crimes 144 | 145 | w.Header().Set("Content-Type", "application/json") 146 | json.NewEncoder(w).Encode(result) 147 | } 148 | 149 | func handleStateCriminalSearch(w http.ResponseWriter, r *http.Request) { 150 | var input StateCriminalSearchInput 151 | 152 | err := json.NewDecoder(r.Body).Decode(&input) 153 | if err != nil { 154 | http.Error(w, err.Error(), http.StatusBadRequest) 155 | return 156 | } 157 | 158 | var result StateCriminalSearchResult 159 | var crimes []string 160 | 161 | possibleCrimes := []string{ 162 | "Jaywalking", 163 | "Burglary", 164 | "Foul Play", 165 | "Smoking in a Public Place", 166 | } 167 | 168 | rndnum := rand.Intn(100) 169 | if rndnum > 75 { 170 | crimes = append(crimes, possibleCrimes[rand.Intn(len(possibleCrimes))]) 171 | } 172 | result.FullName = input.FullName 173 | result.Address = input.Address 174 | result.Crimes = crimes 175 | 176 | w.Header().Set("Content-Type", "application/json") 177 | json.NewEncoder(w).Encode(result) 178 | } 179 | 180 | func Router() *mux.Router { 181 | r := mux.NewRouter() 182 | 183 | r.HandleFunc("/ssntrace", handleSsnTrace).Methods("POST") 184 | r.HandleFunc("/motorvehiclesearch", handleMotorVehicleSearch).Methods("POST") 185 | r.HandleFunc("/federalcriminalsearch", handleFederalCriminalSearch).Methods("POST") 186 | r.HandleFunc("/statecriminalsearch", handleStateCriminalSearch).Methods("POST") 187 | 188 | return r 189 | } 190 | 191 | func Run() { 192 | var err error 193 | 194 | errorInjector, _ := fault.NewErrorInjector(http.StatusInternalServerError) 195 | errorFault, _ := fault.NewFault(errorInjector, 196 | fault.WithEnabled(true), 197 | fault.WithParticipation(0.3), 198 | ) 199 | 200 | handlerChain := errorFault.Handler(Router()) 201 | 202 | srv := &http.Server{ 203 | Handler: handlerChain, 204 | Addr: DefaultEndpoint, 205 | } 206 | 207 | errCh := make(chan error, 1) 208 | go func() { errCh <- srv.ListenAndServe() }() 209 | 210 | sigCh := make(chan os.Signal, 1) 211 | signal.Notify(sigCh, os.Interrupt) 212 | 213 | select { 214 | case <-sigCh: 215 | srv.Close() 216 | case err = <-errCh: 217 | log.Fatalf("error: %v", err) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/cmd/api.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/temporalio/background-checks/deployment/thirdparty-simulator/api" 7 | ) 8 | 9 | // apiCmd represents the thirdparty command 10 | var apiCmd = &cobra.Command{ 11 | Use: "api", 12 | Short: "Starts an API server for the third party API simulator", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | api.Run() 15 | }, 16 | } 17 | 18 | func init() { 19 | rootCmd.AddCommand(apiCmd) 20 | } 21 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // rootCmd represents the base command when called without any subcommands 8 | var rootCmd = &cobra.Command{ 9 | Use: "thirdparty-simulator", 10 | Short: "Third Party API Simulator", 11 | CompletionOptions: cobra.CompletionOptions{ 12 | DisableDefaultCmd: true, 13 | }, 14 | } 15 | 16 | func Execute() { 17 | cobra.CheckErr(rootCmd.Execute()) 18 | } 19 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/temporalio/background-checks/deployment/thirdparty-simulator 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/github/go-fault v0.2.0 7 | github.com/gorilla/mux v1.8.0 8 | github.com/spf13/cobra v1.3.0 9 | ) 10 | -------------------------------------------------------------------------------- /deployment/thirdparty-simulator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/temporalio/background-checks/deployment/thirdparty-simulator/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | postgresql: 4 | environment: 5 | POSTGRES_PASSWORD: temporal 6 | POSTGRES_USER: temporal 7 | image: postgres:13 8 | temporal: 9 | depends_on: 10 | - postgresql 11 | environment: 12 | - DB=postgres12 13 | - DB_PORT=5432 14 | - POSTGRES_USER=temporal 15 | - POSTGRES_PWD=temporal 16 | - POSTGRES_SEEDS=postgresql 17 | - PROMETHEUS_ENDPOINT=0.0.0.0:8000 18 | image: temporalio/auto-setup:1.21.1 19 | temporal-admin-tools: 20 | depends_on: 21 | - temporal 22 | environment: 23 | - TEMPORAL_ADDRESS=temporal:7233 24 | - TEMPORAL_CLI_ADDRESS=temporal:7233 25 | image: temporalio/admin-tools:1.21.1 26 | stdin_open: true 27 | tty: true 28 | temporal-ui: 29 | depends_on: 30 | - temporal 31 | environment: 32 | - TEMPORAL_ADDRESS=temporal:7233 33 | - TEMPORAL_CODEC_ENDPOINT=http://localhost:8081 34 | image: temporalio/ui:2.16.2 35 | ports: 36 | - 8080:8080 37 | prometheus: 38 | image: prom/prometheus 39 | ports: 40 | - 9090:9090 41 | volumes: 42 | - type: bind 43 | source: ./deployment/prometheus/config.yml 44 | target: /etc/prometheus/prometheus.yml 45 | grafana: 46 | build: './deployment/grafana' 47 | environment: 48 | - GF_AUTH_DISABLE_LOGIN_FORM=true 49 | - GF_AUTH_ANONYMOUS_ENABLED=true 50 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 51 | ports: 52 | - 8085:3000 53 | volumes: 54 | - type: bind 55 | source: ./deployment/grafana/provisioning/datasources 56 | target: /etc/grafana/provisioning/datasources 57 | mailhog: 58 | image: mailhog/mailhog:v1.0.1 59 | ports: 60 | - 8025:8025 61 | thirdparty: 62 | build: deployment/thirdparty-simulator 63 | command: thirdparty-simulator api 64 | restart: unless-stopped 65 | environment: 66 | - TEMPORAL_GRPC_ENDPOINT=temporal:7233 67 | api: 68 | restart: unless-stopped 69 | environment: 70 | - TEMPORAL_GRPC_ENDPOINT=temporal:7233 71 | - DATACONVERTER_ENCRYPTION_KEY_ID=secret 72 | ui: 73 | restart: unless-stopped 74 | environment: 75 | - TEMPORAL_GRPC_ENDPOINT=temporal:7233 76 | worker: 77 | restart: unless-stopped 78 | environment: 79 | - TEMPORAL_GRPC_ENDPOINT=temporal:7233 80 | - DATACONVERTER_ENCRYPTION_KEY_ID=secret 81 | tools: 82 | environment: 83 | - TEMPORAL_ADDRESS=temporal:7233 84 | - TEMPORAL_CLI_ADDRESS=temporal:7233 85 | - TEMPORAL_CODEC_ENDPOINT=http://dataconverter:8081/ 86 | - TEMPORAL_CLI_CODEC_ENDPOINT=http://dataconverter:8081/ 87 | dataconverter: 88 | build: 89 | context: . 90 | target: app 91 | command: dataconverter-server --ui http://localhost:8080 --port 8081 92 | restart: unless-stopped 93 | ports: 94 | - 8081:8081 95 | environment: 96 | - DATACONVERTER_ENCRYPTION_KEY_ID=secret 97 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | api: 4 | build: 5 | context: . 6 | target: app 7 | command: bgc-backend api 8 | ui: 9 | build: 10 | context: . 11 | target: app 12 | command: bgc-backend ui 13 | ports: 14 | - 8083:8083 15 | worker: 16 | build: 17 | context: . 18 | target: app 19 | command: bgc-backend worker 20 | tools: 21 | build: 22 | context: . 23 | target: app 24 | command: tail -f /dev/null 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/temporalio/background-checks 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/prometheus/client_golang v1.12.1 8 | github.com/spf13/cobra v1.2.1 9 | github.com/stretchr/testify v1.8.3 10 | github.com/uber-go/tally/v4 v4.1.1 11 | github.com/xhit/go-simple-mail/v2 v2.10.0 12 | go.temporal.io/api v1.21.0 13 | go.temporal.io/sdk v1.23.1 14 | go.temporal.io/sdk/contrib/tally v0.1.0 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 22 | github.com/gogo/googleapis v1.4.1 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/gogo/status v1.1.1 // indirect 25 | github.com/golang/mock v1.6.0 // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/google/uuid v1.3.0 // indirect 28 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 29 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 30 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 31 | github.com/pborman/uuid v1.2.1 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/prometheus/client_model v0.3.0 // indirect 35 | github.com/prometheus/common v0.32.1 // indirect 36 | github.com/prometheus/procfs v0.7.3 // indirect 37 | github.com/robfig/cron v1.2.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/stretchr/objx v0.5.0 // indirect 40 | github.com/twmb/murmur3 v1.1.6 // indirect 41 | go.uber.org/atomic v1.9.0 // indirect 42 | golang.org/x/net v0.10.0 // indirect 43 | golang.org/x/sys v0.8.0 // indirect 44 | golang.org/x/text v0.9.0 // indirect 45 | golang.org/x/time v0.3.0 // indirect 46 | google.golang.org/genproto v0.0.0-20230525154841-bd750badd5c6 // indirect 47 | google.golang.org/grpc v1.55.0 // indirect 48 | google.golang.org/protobuf v1.30.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /run-cli: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec docker compose exec tools "$@" 4 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 4 | echo " * Bringing services up, (re)building as required..." 5 | echo 6 | 7 | docker compose up --build -d 8 | 9 | echo 10 | echo " * Checking for custom search attributes..." 11 | echo 12 | 13 | until docker compose exec tools tctl --auto_confirm admin cluster add-search-attributes --name CandidateEmail --type keyword; do 14 | echo "Waiting for Temporal Frontend to be up" 15 | sleep 1 16 | done 17 | 18 | until docker compose exec tools tctl --auto_confirm admin cluster add-search-attributes --name BackgroundCheckStatus --type keyword; do 19 | echo "Waiting for Temporal Frontend to be up" 20 | sleep 1 21 | done 22 | 23 | echo 24 | echo " * All services are up" 25 | echo 26 | 27 | echo 28 | echo " * The Background Check application is now running." 29 | echo 30 | echo "The following URLs are available to explore the sytem:" 31 | echo 32 | echo -e "\tTemporal Web:" 33 | echo -e "\t\thttp://localhost:8080" 34 | echo -e "\tMail Server (mailhog):" 35 | echo -e "\t\thttp://localhost:8025/" 36 | echo -e "\tGrafana:" 37 | echo -e "\t\thttp://localhost:8085/" 38 | echo 39 | echo "You can check the logs for various components:" 40 | echo 41 | echo -e "docker compose logs worker" 42 | echo -e "docker compose logs api" 43 | echo -e "docker compose logs temporal" -------------------------------------------------------------------------------- /temporal/client.go: -------------------------------------------------------------------------------- 1 | package temporal 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/temporalio/background-checks/temporal/dataconverter" 7 | "go.temporal.io/sdk/client" 8 | "go.temporal.io/sdk/converter" 9 | ) 10 | 11 | func NewClient(options client.Options) (client.Client, error) { 12 | if options.HostPort == "" { 13 | options.HostPort = os.Getenv("TEMPORAL_GRPC_ENDPOINT") 14 | } 15 | 16 | options.DataConverter = dataconverter.NewEncryptionDataConverter( 17 | converter.GetDefaultDataConverter(), 18 | dataconverter.DataConverterOptions{KeyID: os.Getenv("DATACONVERTER_ENCRYPTION_KEY_ID")}, 19 | ) 20 | 21 | return client.NewClient(options) 22 | } 23 | -------------------------------------------------------------------------------- /temporal/dataconverter-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "strconv" 10 | 11 | "github.com/temporalio/background-checks/temporal/dataconverter" 12 | 13 | "go.temporal.io/sdk/converter" 14 | ) 15 | 16 | var portFlag int 17 | var uiFlag string 18 | 19 | func init() { 20 | flag.IntVar(&portFlag, "port", 8081, "Port to listen on") 21 | flag.StringVar(&uiFlag, "ui", "", "Temporal UI URL. Enables CORS which is required for access from Temporal UI") 22 | } 23 | 24 | func newCORSHTTPHandler(web string, next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | w.Header().Set("Access-Control-Allow-Origin", web) 27 | w.Header().Set("Access-Control-Allow-Credentials", "true") 28 | w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type,X-Namespace") 29 | 30 | if r.Method == "OPTIONS" { 31 | return 32 | } 33 | 34 | next.ServeHTTP(w, r) 35 | }) 36 | } 37 | 38 | func main() { 39 | flag.Parse() 40 | 41 | if uiFlag == "" { 42 | log.Fatalf("Please specific the -ui flag to enable UI support.\n") 43 | } 44 | 45 | handler := converter.NewPayloadCodecHTTPHandler(&dataconverter.Codec{KeyID: os.Getenv("DATACONVERTER_ENCRYPTION_KEY_ID")}) 46 | handler = newCORSHTTPHandler(uiFlag, handler) 47 | 48 | srv := &http.Server{ 49 | Addr: "0.0.0.0:" + strconv.Itoa(portFlag), 50 | Handler: handler, 51 | } 52 | 53 | errCh := make(chan error, 1) 54 | go func() { errCh <- srv.ListenAndServe() }() 55 | 56 | sigCh := make(chan os.Signal, 1) 57 | signal.Notify(sigCh, os.Interrupt) 58 | 59 | select { 60 | case <-sigCh: 61 | _ = srv.Close() 62 | case err := <-errCh: 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /temporal/dataconverter/crypt.go: -------------------------------------------------------------------------------- 1 | package dataconverter 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | func encrypt(plainData []byte, key []byte) ([]byte, error) { 12 | c, err := aes.NewCipher(key) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | gcm, err := cipher.NewGCM(c) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | nonce := make([]byte, gcm.NonceSize()) 23 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 24 | return nil, err 25 | } 26 | 27 | return gcm.Seal(nonce, nonce, plainData, nil), nil 28 | } 29 | 30 | func decrypt(encryptedData []byte, key []byte) ([]byte, error) { 31 | c, err := aes.NewCipher(key) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | gcm, err := cipher.NewGCM(c) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | nonceSize := gcm.NonceSize() 42 | if len(encryptedData) < nonceSize { 43 | return nil, fmt.Errorf("ciphertext too short: %v", encryptedData) 44 | } 45 | 46 | nonce, encryptedData := encryptedData[:nonceSize], encryptedData[nonceSize:] 47 | return gcm.Open(nil, nonce, encryptedData, nil) 48 | } 49 | -------------------------------------------------------------------------------- /temporal/dataconverter/data_converter.go: -------------------------------------------------------------------------------- 1 | package dataconverter 2 | 3 | import ( 4 | "fmt" 5 | 6 | commonpb "go.temporal.io/api/common/v1" 7 | 8 | "go.temporal.io/sdk/converter" 9 | ) 10 | 11 | const ( 12 | MetadataEncodingEncrypted = "binary/encrypted" 13 | MetadataEncryptionKeyID = "encryption-key-id" 14 | ) 15 | 16 | type DataConverter struct { 17 | parent converter.DataConverter 18 | converter.DataConverter 19 | options DataConverterOptions 20 | } 21 | 22 | type DataConverterOptions struct { 23 | KeyID string 24 | } 25 | 26 | type Codec struct { 27 | KeyID string 28 | } 29 | 30 | func (e *Codec) getKey(keyID string) (key []byte) { 31 | return []byte("test-key-test-key-test-key-test!") 32 | } 33 | 34 | // NewEncryptionDataConverter creates a new instance of EncryptionDataConverter wrapping a DataConverter 35 | func NewEncryptionDataConverter(dataConverter converter.DataConverter, options DataConverterOptions) *DataConverter { 36 | codecs := []converter.PayloadCodec{ 37 | &Codec{KeyID: options.KeyID}, 38 | } 39 | 40 | return &DataConverter{ 41 | parent: dataConverter, 42 | DataConverter: converter.NewCodecDataConverter(dataConverter, codecs...), 43 | options: options, 44 | } 45 | } 46 | 47 | // Encode implements converter.PayloadCodec.Encode. 48 | func (e *Codec) Encode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) { 49 | result := make([]*commonpb.Payload, len(payloads)) 50 | for i, p := range payloads { 51 | origBytes, err := p.Marshal() 52 | if err != nil { 53 | return payloads, err 54 | } 55 | 56 | key := e.getKey(e.KeyID) 57 | 58 | b, err := encrypt(origBytes, key) 59 | if err != nil { 60 | return payloads, err 61 | } 62 | 63 | result[i] = &commonpb.Payload{ 64 | Metadata: map[string][]byte{ 65 | converter.MetadataEncoding: []byte(MetadataEncodingEncrypted), 66 | MetadataEncryptionKeyID: []byte(e.KeyID), 67 | }, 68 | Data: b, 69 | } 70 | } 71 | 72 | return result, nil 73 | } 74 | 75 | // Decode implements converter.PayloadCodec.Decode. 76 | func (e *Codec) Decode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) { 77 | result := make([]*commonpb.Payload, len(payloads)) 78 | for i, p := range payloads { 79 | // Only if it's encrypted 80 | if string(p.Metadata[converter.MetadataEncoding]) != MetadataEncodingEncrypted { 81 | result[i] = p 82 | continue 83 | } 84 | 85 | keyID, ok := p.Metadata[MetadataEncryptionKeyID] 86 | if !ok { 87 | return payloads, fmt.Errorf("no encryption key id") 88 | } 89 | 90 | key := e.getKey(string(keyID)) 91 | 92 | b, err := decrypt(p.Data, key) 93 | if err != nil { 94 | return payloads, err 95 | } 96 | 97 | result[i] = &commonpb.Payload{} 98 | err = result[i].Unmarshal(b) 99 | if err != nil { 100 | return payloads, err 101 | } 102 | } 103 | 104 | return result, nil 105 | } 106 | -------------------------------------------------------------------------------- /temporal/dataconverter/propagator.go: -------------------------------------------------------------------------------- 1 | package dataconverter 2 | 3 | import ( 4 | "context" 5 | 6 | "go.temporal.io/sdk/converter" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | type ( 11 | contextKey struct{} 12 | propagator struct{} 13 | CryptContext struct { 14 | KeyID string `json:"keyId"` 15 | } 16 | ) 17 | 18 | var PropagateKey = contextKey{} 19 | 20 | const propagationKey = "encryption" 21 | 22 | // NewContextPropagator returns a context propagator that propagates a set of 23 | // string key-value pairs across a workflow 24 | func NewContextPropagator() workflow.ContextPropagator { 25 | return &propagator{} 26 | } 27 | 28 | // Inject injects values from context into headers for propagation 29 | func (s *propagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error { 30 | value := ctx.Value(PropagateKey) 31 | payload, err := converter.GetDefaultDataConverter().ToPayload(value) 32 | if err != nil { 33 | return err 34 | } 35 | writer.Set(propagationKey, payload) 36 | return nil 37 | } 38 | 39 | // InjectFromWorkflow injects values from context into headers for propagation 40 | func (s *propagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error { 41 | value := ctx.Value(PropagateKey) 42 | payload, err := converter.GetDefaultDataConverter().ToPayload(value) 43 | if err != nil { 44 | return err 45 | } 46 | writer.Set(propagationKey, payload) 47 | return nil 48 | } 49 | 50 | // Extract extracts values from headers and puts them into context 51 | func (s *propagator) Extract(ctx context.Context, reader workflow.HeaderReader) (context.Context, error) { 52 | if value, ok := reader.Get(propagationKey); ok { 53 | var cryptContext CryptContext 54 | if err := converter.GetDefaultDataConverter().FromPayload(value, &cryptContext); err != nil { 55 | return ctx, nil 56 | } 57 | ctx = context.WithValue(ctx, PropagateKey, cryptContext) 58 | } 59 | 60 | return ctx, nil 61 | } 62 | 63 | // ExtractToWorkflow extracts values from headers and puts them into context 64 | func (s *propagator) ExtractToWorkflow(ctx workflow.Context, reader workflow.HeaderReader) (workflow.Context, error) { 65 | if value, ok := reader.Get(propagationKey); ok { 66 | var cryptContext CryptContext 67 | if err := converter.GetDefaultDataConverter().FromPayload(value, &cryptContext); err != nil { 68 | return ctx, nil 69 | } 70 | ctx = workflow.WithValue(ctx, PropagateKey, cryptContext) 71 | } 72 | 73 | return ctx, nil 74 | } 75 | -------------------------------------------------------------------------------- /ui/accept.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Background Check Request 100 |
  • 101 |
  • Enter Personal Information
  • 102 |
  • Background Check Started
  • 103 |
104 |
105 |
106 |
107 |
108 |
109 |

Personal Information

110 |

The information requested below is required to process your background check.

111 |

If you are happy for the check to proceed, please complete the form and hit 'Accept'.

112 |

If you would rather the background check is not run, please hit 'Decline'.

113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 | 121 |
122 |
123 | 124 | 125 |
126 |
127 | 128 | 129 |
130 | 131 | 132 |
133 |
134 |
135 |
136 |
137 | 138 | -------------------------------------------------------------------------------- /ui/accepted.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Background Check Request 100 |
  • 101 |
  • Enter Personal Information
  • 102 |
  • Background Check Started
  • 103 |
104 |
105 |
106 |
107 |
108 |
109 |

Congratulations!

110 |

Your background check has been started.

111 |

You will be notified of the results once it has finished.

112 |
113 |
114 |
115 |
116 | 117 | -------------------------------------------------------------------------------- /ui/declined.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background Checks: Candidate 5 | 6 | 7 | 8 |
9 | 27 |
28 |
29 |
30 |

Declined

31 |

You have declined the background check.

32 |
33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /ui/employment_verification.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Start Employment Verification 100 |
  • 101 |
  • Confirm Employment Verification
  • 102 |
  • Verification Submitted
  • 103 |
104 |
105 |
106 |
107 |

Employment Confirmation for {{.Candidate.FullName}}

108 |

Please confirm that the candidate is currently employed by {{.Candidate.Employer}}

109 |

110 |

111 |

112 |
113 |
114 | 115 | 116 |
117 |
118 |
119 |

120 |
121 |
122 | 123 | -------------------------------------------------------------------------------- /ui/employment_verified.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Start Employment Verification 100 |
  • 101 |
  • Confirm Employment Verification
  • 102 |
  • Verification Submitted
  • 103 |
104 |
105 |
106 |
107 |

Thank you!

108 |

Your research results have been successfully submitted.

109 |

110 |
111 |
112 | 113 | -------------------------------------------------------------------------------- /ui/report.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 85 | 86 | 87 | 88 | 94 |
95 |
96 |
97 |
    98 |
  • 99 | Candidate Report Confirmation 100 |
  • 101 |
  • View Candidate Report
  • 102 |
103 |
104 |
105 |
106 |

View Candidate Report

107 |

108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
#Check TypeCheck Result
1SSN Verification{{ .SSNTrace.SSNIsValid }}
2Employment Verification{{ .SearchResults.EmploymentVerification.EmployerVerified }}
3Motor Vehicle Incident Search{{ .SearchResults.MotorVehicleIncidentSearch.MotorVehicleIncidents }}
4State Criminal Search{{ .SearchResults.StateCriminalSearch.Crimes }}
5Federal Criminal Search{{ .SearchResults.FederalCriminalSearch.Crimes }}
137 |

138 |
139 |
140 | 141 | -------------------------------------------------------------------------------- /ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "text/template" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/temporalio/background-checks/api" 12 | "github.com/temporalio/background-checks/utils" 13 | "github.com/temporalio/background-checks/workflows" 14 | ) 15 | 16 | const ( 17 | APIEndpoint = "api:8081" 18 | ) 19 | 20 | type handlers struct{} 21 | 22 | //go:embed accept.go.html 23 | var acceptHTML string 24 | var acceptHTMLTemplate = template.Must(template.New("accept").Parse(acceptHTML)) 25 | 26 | func (h *handlers) handleAccept(w http.ResponseWriter, r *http.Request) { 27 | vars := mux.Vars(r) 28 | token := vars["token"] 29 | 30 | err := acceptHTMLTemplate.Execute(w, map[string]string{"Token": token}) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | } 36 | 37 | //go:embed accepted.go.html 38 | var acceptedHTML string 39 | var acceptedHTMLTemplate = template.Must(template.New("accepted").Parse(acceptedHTML)) 40 | 41 | //go:embed declined.go.html 42 | var declinedHTML string 43 | var declinedHTMLTemplate = template.Must(template.New("declined").Parse(declinedHTML)) 44 | 45 | func (h *handlers) handleAcceptSubmission(w http.ResponseWriter, r *http.Request) { 46 | vars := mux.Vars(r) 47 | token := vars["token"] 48 | 49 | router := api.Router(nil) 50 | 51 | if r.FormValue("action") == "decline" { 52 | requestURL, err := router.Get("decline").Host(APIEndpoint).URL("token", token) 53 | if err != nil { 54 | http.Error(w, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | 58 | response, err := utils.PostJSON(requestURL, nil) 59 | if err != nil { 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | return 62 | } 63 | defer response.Body.Close() 64 | 65 | body, _ := io.ReadAll(response.Body) 66 | 67 | if response.StatusCode != http.StatusOK { 68 | message := fmt.Sprintf("%s: %s", http.StatusText(response.StatusCode), body) 69 | http.Error(w, message, http.StatusInternalServerError) 70 | return 71 | } 72 | 73 | err = declinedHTMLTemplate.Execute(w, nil) 74 | if err != nil { 75 | http.Error(w, err.Error(), http.StatusInternalServerError) 76 | return 77 | } 78 | return 79 | } 80 | 81 | requestURL, err := router.Get("accept").Host(APIEndpoint).URL("token", token) 82 | if err != nil { 83 | http.Error(w, err.Error(), http.StatusInternalServerError) 84 | return 85 | } 86 | 87 | candidatedetails := workflows.CandidateDetails{ 88 | FullName: r.FormValue("full_name"), 89 | SSN: r.FormValue("ssn"), 90 | Employer: r.FormValue("employer"), 91 | } 92 | submission := workflows.AcceptSubmissionSignal{ 93 | CandidateDetails: candidatedetails, 94 | } 95 | 96 | response, err := utils.PostJSON(requestURL, submission) 97 | if err != nil { 98 | http.Error(w, err.Error(), http.StatusInternalServerError) 99 | return 100 | } 101 | defer response.Body.Close() 102 | 103 | body, _ := io.ReadAll(response.Body) 104 | 105 | if response.StatusCode != http.StatusOK { 106 | message := fmt.Sprintf("%s: %s", http.StatusText(response.StatusCode), body) 107 | http.Error(w, message, http.StatusInternalServerError) 108 | return 109 | } 110 | 111 | err = acceptedHTMLTemplate.Execute(w, nil) 112 | if err != nil { 113 | http.Error(w, err.Error(), http.StatusInternalServerError) 114 | return 115 | } 116 | } 117 | 118 | //go:embed employment_verification.go.html 119 | var employmentVerificationHTML string 120 | var employmentVerificationHTMLTemplate = template.Must(template.New("employment_verification").Parse(employmentVerificationHTML)) 121 | 122 | func (h *handlers) handleEmploymentVerification(w http.ResponseWriter, r *http.Request) { 123 | vars := mux.Vars(r) 124 | token := vars["token"] 125 | 126 | router := api.Router(nil) 127 | 128 | requestURL, err := router.Get("employmentverify_details").Host(APIEndpoint).URL("token", token) 129 | if err != nil { 130 | http.Error(w, err.Error(), http.StatusInternalServerError) 131 | return 132 | } 133 | 134 | var candidate workflows.CandidateDetails 135 | 136 | _, err = utils.GetJSON(requestURL, &candidate) 137 | if err != nil { 138 | http.Error(w, err.Error(), http.StatusInternalServerError) 139 | return 140 | } 141 | 142 | err = employmentVerificationHTMLTemplate.Execute(w, map[string]interface{}{"Token": token, "Candidate": candidate}) 143 | if err != nil { 144 | http.Error(w, err.Error(), http.StatusInternalServerError) 145 | return 146 | } 147 | } 148 | 149 | //go:embed employment_verified.go.html 150 | var employmentVerifiedHTML string 151 | var employmentVerifiedHTMLTemplate = template.Must(template.New("employment_verification").Parse(employmentVerifiedHTML)) 152 | 153 | func (h *handlers) handleEmploymentVerificationSubmission(w http.ResponseWriter, r *http.Request) { 154 | vars := mux.Vars(r) 155 | token := vars["token"] 156 | 157 | router := api.Router(nil) 158 | 159 | requestURL, err := router.Get("employmentverify").Host(APIEndpoint).URL("token", token) 160 | if err != nil { 161 | http.Error(w, err.Error(), http.StatusInternalServerError) 162 | return 163 | } 164 | 165 | submission := workflows.EmploymentVerificationSubmissionSignal{ 166 | EmploymentVerificationComplete: true, 167 | EmployerVerified: r.FormValue("action") == "yes", 168 | } 169 | 170 | response, err := utils.PostJSON(requestURL, submission) 171 | if err != nil { 172 | http.Error(w, err.Error(), http.StatusInternalServerError) 173 | return 174 | } 175 | defer response.Body.Close() 176 | 177 | body, _ := io.ReadAll(response.Body) 178 | 179 | if response.StatusCode != http.StatusOK { 180 | message := fmt.Sprintf("%s: %s", http.StatusText(response.StatusCode), body) 181 | http.Error(w, message, http.StatusInternalServerError) 182 | return 183 | } 184 | 185 | err = employmentVerifiedHTMLTemplate.Execute(w, nil) 186 | if err != nil { 187 | http.Error(w, err.Error(), http.StatusInternalServerError) 188 | return 189 | } 190 | } 191 | 192 | //go:embed report.go.html 193 | var reportHTML string 194 | var reportHTMLTemplate = template.Must(template.New("report").Parse(reportHTML)) 195 | 196 | func (h *handlers) handleReport(w http.ResponseWriter, r *http.Request) { 197 | vars := mux.Vars(r) 198 | token := vars["token"] 199 | 200 | router := api.Router(nil) 201 | 202 | requestURL, err := router.Get("check_report").Host(APIEndpoint).URL("token", token) 203 | if err != nil { 204 | http.Error(w, err.Error(), http.StatusInternalServerError) 205 | return 206 | } 207 | 208 | var status workflows.BackgroundCheckState 209 | 210 | _, err = utils.GetJSON(requestURL, &status) 211 | if err != nil { 212 | http.Error(w, err.Error(), http.StatusInternalServerError) 213 | return 214 | } 215 | 216 | err = reportHTMLTemplate.Execute(w, status) 217 | if err != nil { 218 | http.Error(w, err.Error(), http.StatusInternalServerError) 219 | return 220 | } 221 | } 222 | 223 | func Router() *mux.Router { 224 | r := mux.NewRouter() 225 | 226 | h := handlers{} 227 | 228 | r.HandleFunc("/candidate/{token}", h.handleAccept).Methods("GET") 229 | r.HandleFunc("/candidate/{token}", h.handleAcceptSubmission).Methods("POST") 230 | 231 | r.HandleFunc("/employment/{token}", h.handleEmploymentVerification).Methods("GET") 232 | r.HandleFunc("/employment/{token}", h.handleEmploymentVerificationSubmission).Methods("POST") 233 | 234 | r.HandleFunc("/report/{token}", h.handleReport).Methods("GET") 235 | 236 | return r 237 | } 238 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | func PostJSON(url *url.URL, input interface{}) (*http.Response, error) { 13 | jsonInput, err := json.Marshal(input) 14 | if err != nil { 15 | return nil, fmt.Errorf("unable to encode input: %w", err) 16 | } 17 | 18 | req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(jsonInput)) 19 | if err != nil { 20 | return nil, fmt.Errorf("unable to build request: %w", err) 21 | } 22 | 23 | req.Header.Set("Content-Type", "application/json") 24 | 25 | client := http.Client{} 26 | return client.Do(req) 27 | } 28 | 29 | func GetJSON(url *url.URL, result interface{}) (*http.Response, error) { 30 | req, err := http.NewRequest(http.MethodGet, url.String(), nil) 31 | if err != nil { 32 | return nil, fmt.Errorf("unable to build request: %w", err) 33 | } 34 | 35 | client := http.Client{} 36 | r, err := client.Do(req) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer r.Body.Close() 41 | 42 | if r.StatusCode >= 200 && r.StatusCode < 300 { 43 | err = json.NewDecoder(r.Body).Decode(result) 44 | return r, err 45 | } 46 | 47 | message, _ := io.ReadAll(r.Body) 48 | 49 | return r, fmt.Errorf("%s: %s", http.StatusText(r.StatusCode), message) 50 | } 51 | -------------------------------------------------------------------------------- /workflows/accept.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | const ( 11 | AcceptSubmissionSignalName = "accept-submission" 12 | AcceptGracePeriod = time.Hour * 24 * 7 13 | ) 14 | 15 | func emailCandidate(ctx workflow.Context, input *AcceptWorkflowInput) error { 16 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 17 | StartToCloseTimeout: time.Minute, 18 | }) 19 | 20 | i := activities.SendAcceptEmailInput{ 21 | Email: input.Email, 22 | Token: TokenForWorkflow(ctx), 23 | } 24 | f := workflow.ExecuteActivity(ctx, a.SendAcceptEmail, i) 25 | return f.Get(ctx, nil) 26 | } 27 | 28 | func waitForSubmission(ctx workflow.Context) (*AcceptSubmission, error) { 29 | var response AcceptSubmission 30 | var err error 31 | 32 | s := workflow.NewSelector(ctx) 33 | 34 | ch := workflow.GetSignalChannel(ctx, AcceptSubmissionSignalName) 35 | s.AddReceive(ch, func(c workflow.ReceiveChannel, more bool) { 36 | var submission AcceptSubmissionSignal 37 | c.Receive(ctx, &submission) 38 | 39 | response = AcceptSubmission(submission) 40 | }) 41 | s.AddFuture(workflow.NewTimer(ctx, AcceptGracePeriod), func(f workflow.Future) { 42 | err = f.Get(ctx, nil) 43 | 44 | // Treat failure to accept in time as declining. 45 | response.Accepted = false 46 | }) 47 | 48 | s.Select(ctx) 49 | 50 | return &response, err 51 | } 52 | 53 | type AcceptWorkflowInput struct { 54 | Email string 55 | } 56 | 57 | type AcceptWorkflowResult struct { 58 | Accepted bool 59 | CandidateDetails CandidateDetails 60 | } 61 | 62 | // @@@SNIPSTART background-checks-accept-workflow-definition 63 | func Accept(ctx workflow.Context, input *AcceptWorkflowInput) (*AcceptWorkflowResult, error) { 64 | err := emailCandidate(ctx, input) 65 | if err != nil { 66 | return &AcceptWorkflowResult{}, err 67 | } 68 | 69 | submission, err := waitForSubmission(ctx) 70 | 71 | result := AcceptWorkflowResult(*submission) 72 | return &result, err 73 | } 74 | 75 | // @@@SNIPEND 76 | -------------------------------------------------------------------------------- /workflows/accept_test.go: -------------------------------------------------------------------------------- 1 | package workflows_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/temporalio/background-checks/activities" 8 | "github.com/temporalio/background-checks/workflows" 9 | "go.temporal.io/sdk/testsuite" 10 | ) 11 | 12 | func TestReturnsAcceptWorkflow(t *testing.T) { 13 | s := testsuite.WorkflowTestSuite{} 14 | env := s.NewTestWorkflowEnvironment() 15 | a := activities.Activities{SMTPStub: true} 16 | 17 | env.RegisterActivity(a.SendAcceptEmail) 18 | 19 | details := workflows.CandidateDetails{ 20 | FullName: "John Smith", 21 | SSN: "111-11-1111", 22 | DOB: "1981-01-01", 23 | Address: "1 Chestnut Avenue", 24 | } 25 | 26 | env.RegisterDelayedCallback( 27 | func() { 28 | env.SignalWorkflow( 29 | workflows.AcceptSubmissionSignalName, 30 | workflows.AcceptSubmissionSignal{Accepted: true, CandidateDetails: details}, 31 | ) 32 | }, 33 | 0, 34 | ) 35 | 36 | env.ExecuteWorkflow(workflows.Accept, &workflows.AcceptWorkflowInput{}) 37 | 38 | var result workflows.AcceptWorkflowResult 39 | err := env.GetWorkflowResult(&result) 40 | assert.NoError(t, err) 41 | 42 | assert.Equal(t, workflows.AcceptWorkflowResult{Accepted: true, CandidateDetails: details}, result) 43 | } 44 | 45 | func TestReturnsAcceptWorkflowTimeout(t *testing.T) { 46 | s := testsuite.WorkflowTestSuite{} 47 | env := s.NewTestWorkflowEnvironment() 48 | a := activities.Activities{SMTPStub: true} 49 | 50 | env.RegisterActivity(a.SendAcceptEmail) 51 | 52 | env.ExecuteWorkflow(workflows.Accept, &workflows.AcceptWorkflowInput{}) 53 | 54 | var result workflows.AcceptWorkflowResult 55 | err := env.GetWorkflowResult(&result) 56 | assert.NoError(t, err) 57 | 58 | assert.Equal(t, workflows.AcceptWorkflowResult{Accepted: false, CandidateDetails: workflows.CandidateDetails{}}, result) 59 | } 60 | -------------------------------------------------------------------------------- /workflows/activities.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import "github.com/temporalio/background-checks/activities" 4 | 5 | var a *activities.Activities 6 | -------------------------------------------------------------------------------- /workflows/background_check.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/log" 8 | "go.temporal.io/sdk/workflow" 9 | ) 10 | 11 | const ( 12 | BackgroundCheckStatusQuery = "background-check-status" 13 | ) 14 | 15 | type BackgroundCheckWorkflowInput struct { 16 | Email string 17 | Tier string 18 | } 19 | 20 | type BackgroundCheckState struct { 21 | Email string 22 | Tier string 23 | Accepted bool 24 | CandidateDetails CandidateDetails 25 | SSNTrace *SSNTraceWorkflowResult 26 | SearchResults map[string]interface{} 27 | SearchErrors map[string]string 28 | } 29 | 30 | type BackgroundCheckWorkflowResult = BackgroundCheckState 31 | 32 | // backgroundCheckWorkflow represents the state for a background check workflow execution. 33 | type backgroundCheckWorkflow struct { 34 | BackgroundCheckState 35 | checkID string 36 | searchFutures map[string]workflow.Future 37 | logger log.Logger 38 | } 39 | 40 | // newBackgroundCheckWorkflow initializes a backgroundCheckWorkflow struct. 41 | func newBackgroundCheckWorkflow(ctx workflow.Context, state *BackgroundCheckState) *backgroundCheckWorkflow { 42 | return &backgroundCheckWorkflow{ 43 | BackgroundCheckState: *state, 44 | checkID: workflow.GetInfo(ctx).WorkflowExecution.RunID, 45 | searchFutures: make(map[string]workflow.Future), 46 | logger: workflow.GetLogger(ctx), 47 | } 48 | } 49 | 50 | // pushStatus updates the BackgroundCheckStatus search attribute for a background check workflow execution. 51 | func (w *backgroundCheckWorkflow) pushStatus(ctx workflow.Context, status string) error { 52 | return workflow.UpsertSearchAttributes( 53 | ctx, 54 | map[string]interface{}{ 55 | "BackgroundCheckStatus": status, 56 | }, 57 | ) 58 | } 59 | 60 | // waitForAccept waits for the candidate to accept or decline the background check. 61 | // If the candidate accepted, the response will include their personal information. 62 | func (w *backgroundCheckWorkflow) waitForAccept(ctx workflow.Context, email string) (*AcceptSubmission, error) { 63 | var r AcceptSubmission 64 | 65 | err := w.pushStatus(ctx, "pending_accept") 66 | if err != nil { 67 | return &r, err 68 | } 69 | 70 | ctx = workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ 71 | WorkflowID: AcceptWorkflowID(email), 72 | }) 73 | consentWF := workflow.ExecuteChildWorkflow(ctx, Accept, AcceptWorkflowInput{ 74 | Email: email, 75 | }) 76 | err = consentWF.Get(ctx, &r) 77 | 78 | return &r, err 79 | } 80 | 81 | // ssnTrace runs an SSN trace. 82 | // This will tell us if the SSN the candidate gave us is valid. 83 | // It also provides us with a list of addresses that the candidate is linked to in the SSN system. 84 | func (w *backgroundCheckWorkflow) ssnTrace(ctx workflow.Context) (*SSNTraceWorkflowResult, error) { 85 | var r SSNTraceWorkflowResult 86 | 87 | ssnTrace := workflow.ExecuteChildWorkflow( 88 | ctx, 89 | SSNTrace, 90 | SSNTraceWorkflowInput{FullName: w.CandidateDetails.FullName, SSN: w.CandidateDetails.SSN}, 91 | ) 92 | 93 | err := ssnTrace.Get(ctx, &r) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return &r, err 99 | } 100 | 101 | // sendDeclineEmail sends an email to the Hiring Manager informing them the candidate declined the background check. 102 | func (w *backgroundCheckWorkflow) sendDeclineEmail(ctx workflow.Context, email string) error { 103 | w.pushStatus(ctx, "declined") 104 | 105 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 106 | StartToCloseTimeout: time.Minute, 107 | }) 108 | 109 | f := workflow.ExecuteActivity(ctx, a.SendDeclineEmail, activities.SendDeclineEmailInput{Email: w.Email}) 110 | return f.Get(ctx, nil) 111 | } 112 | 113 | // sendReportEmail sends an email to the Hiring Manager with a link to the report page for the background check. 114 | func (w *backgroundCheckWorkflow) sendReportEmail(ctx workflow.Context, email string) error { 115 | w.pushStatus(ctx, "completed") 116 | 117 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 118 | StartToCloseTimeout: time.Minute, 119 | }) 120 | 121 | f := workflow.ExecuteActivity(ctx, a.SendReportEmail, activities.SendReportEmailInput{Email: w.Email, Token: TokenForWorkflow(ctx)}) 122 | return f.Get(ctx, nil) 123 | } 124 | 125 | // startSearch starts a child workflow to perform one of the searches that make up the background check. 126 | func (w *backgroundCheckWorkflow) startSearch(ctx workflow.Context, name string, searchWorkflow interface{}, searchInputs ...interface{}) { 127 | f := workflow.ExecuteChildWorkflow( 128 | workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ 129 | WorkflowID: SearchWorkflowID(w.Email, name), 130 | }), 131 | searchWorkflow, 132 | searchInputs..., 133 | ) 134 | // Record the future for the search so we can collect the results later 135 | w.searchFutures[name] = f 136 | } 137 | 138 | // waitForSearches waits for all of our searches to complete and collects the results. 139 | func (w *backgroundCheckWorkflow) waitForSearches(ctx workflow.Context) { 140 | for name, f := range w.searchFutures { 141 | var r interface{} 142 | 143 | err := f.Get(ctx, &r) 144 | if err != nil { 145 | w.logger.Error("Search failed", "name", name, "error", err) 146 | // Record an error for the search so we can include it in the report. 147 | w.SearchErrors[name] = err.Error() 148 | continue 149 | } 150 | // Record the result of the search so we can use it in the report. 151 | w.SearchResults[name] = r 152 | } 153 | } 154 | 155 | // @@@SNIPSTART background-checks-main-workflow-definition 156 | 157 | // BackgroundCheck is a Workflow Definition that calls for the execution of a variable set of Activities and Child Workflows. 158 | // This is the main entry point of the application. 159 | // It accepts an email address as the input. 160 | // All other personal information for the Candidate is provided when they accept the Background Check. 161 | func BackgroundCheck(ctx workflow.Context, input *BackgroundCheckWorkflowInput) (*BackgroundCheckWorkflowResult, error) { 162 | w := newBackgroundCheckWorkflow( 163 | ctx, 164 | &BackgroundCheckState{ 165 | Email: input.Email, 166 | Tier: input.Tier, 167 | SearchResults: make(map[string]interface{}), 168 | SearchErrors: make(map[string]string), 169 | }, 170 | ) 171 | 172 | // The query returns the status of a background check and is used by the API to build the report at the end. 173 | err := workflow.SetQueryHandler(ctx, BackgroundCheckStatusQuery, func() (BackgroundCheckState, error) { 174 | return w.BackgroundCheckState, nil 175 | }) 176 | if err != nil { 177 | return &w.BackgroundCheckState, err 178 | } 179 | 180 | // Send the candidate an email asking them to accept or decline the background check. 181 | response, err := w.waitForAccept(ctx, w.Email) 182 | if err != nil { 183 | return &w.BackgroundCheckState, err 184 | } 185 | 186 | w.Accepted = response.Accepted 187 | 188 | // If the candidate declined the check, let the hiring manager know and then end the workflow. 189 | if !w.Accepted { 190 | return &w.BackgroundCheckState, w.sendDeclineEmail(ctx, activities.HiringManagerEmail) 191 | } 192 | 193 | w.CandidateDetails = response.CandidateDetails 194 | 195 | // Update our status search attribute. This is used by our API to filter the background check list if requested. 196 | err = w.pushStatus(ctx, "running") 197 | if err != nil { 198 | return &w.BackgroundCheckState, err 199 | } 200 | 201 | // Run an SSN trace on the SSN the candidate provided when accepting the background check. 202 | w.SSNTrace, err = w.ssnTrace(ctx) 203 | if err != nil { 204 | return &w.BackgroundCheckState, err 205 | } 206 | 207 | // If the SSN the candidate gave us was not valid then send a report email to the Hiring Manager and end the workflow. 208 | // In this case all the searches are skipped. 209 | if !w.SSNTrace.SSNIsValid { 210 | return &w.BackgroundCheckState, w.sendReportEmail(ctx, activities.HiringManagerEmail) 211 | } 212 | 213 | // Start the main searches, these are run in parallel as they do not depend on each other. 214 | 215 | var primaryAddress string 216 | if len(w.SSNTrace.KnownAddresses) > 0 { 217 | primaryAddress = w.SSNTrace.KnownAddresses[0] 218 | } 219 | 220 | // We always run the FederalCriminalSearch 221 | w.startSearch( 222 | ctx, 223 | "FederalCriminalSearch", 224 | FederalCriminalSearch, 225 | FederalCriminalSearchWorkflowInput{FullName: w.CandidateDetails.FullName, KnownAddresses: w.SSNTrace.KnownAddresses}, 226 | ) 227 | 228 | // If the background check is on the full tier we run more searches 229 | if w.Tier == "full" { 230 | w.startSearch( 231 | ctx, 232 | "StateCriminalSearch", 233 | StateCriminalSearch, 234 | StateCriminalSearchWorkflowInput{FullName: w.CandidateDetails.FullName, KnownAddresses: w.SSNTrace.KnownAddresses}, 235 | ) 236 | w.startSearch( 237 | ctx, 238 | "MotorVehicleIncidentSearch", 239 | MotorVehicleIncidentSearch, 240 | MotorVehicleIncidentSearchWorkflowInput{FullName: w.CandidateDetails.FullName, Address: primaryAddress}, 241 | ) 242 | 243 | // Verify their employment if they provided an employer 244 | if w.CandidateDetails.Employer != "" { 245 | w.startSearch( 246 | ctx, 247 | "EmploymentVerification", 248 | EmploymentVerification, 249 | EmploymentVerificationWorkflowInput{CandidateDetails: w.CandidateDetails}, 250 | ) 251 | } 252 | } 253 | 254 | // Wait for all of our searches to complete. 255 | w.waitForSearches(ctx) 256 | 257 | // Send the report email to the Hiring Manager. 258 | return &w.BackgroundCheckState, w.sendReportEmail(ctx, activities.HiringManagerEmail) 259 | } 260 | 261 | // @@@SNIPEND 262 | -------------------------------------------------------------------------------- /workflows/background_check_test.go: -------------------------------------------------------------------------------- 1 | package workflows_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/temporalio/background-checks/activities" 8 | "github.com/temporalio/background-checks/workflows" 9 | "go.temporal.io/sdk/converter" 10 | "go.temporal.io/sdk/testsuite" 11 | "go.temporal.io/sdk/workflow" 12 | ) 13 | 14 | func TestBackgroundCheckWorkflowStandard(t *testing.T) { 15 | s := testsuite.WorkflowTestSuite{} 16 | env := s.NewTestWorkflowEnvironment() 17 | a := activities.Activities{SMTPStub: true, HTTPStub: true} 18 | 19 | env.RegisterWorkflow(workflows.Accept) 20 | env.RegisterActivity(a.SendAcceptEmail) 21 | env.RegisterWorkflow(workflows.SSNTrace) 22 | env.RegisterActivity(a.SSNTrace) 23 | env.RegisterWorkflow(workflows.FederalCriminalSearch) 24 | env.RegisterActivity(a.FederalCriminalSearch) 25 | env.RegisterActivity(a.SendReportEmail) 26 | 27 | details := workflows.CandidateDetails{ 28 | FullName: "John Smith", 29 | SSN: "111-11-1111", 30 | DOB: "1981-01-01", 31 | Address: "1 Chestnut Avenue", 32 | } 33 | 34 | env.SetOnChildWorkflowStartedListener(func(workflowInfo *workflow.Info, ctx workflow.Context, args converter.EncodedValues) { 35 | if workflowInfo.WorkflowExecution.ID == workflows.AcceptWorkflowID("john@example.com") { 36 | env.SignalWorkflowByID( 37 | workflows.AcceptWorkflowID("john@example.com"), 38 | workflows.AcceptSubmissionSignalName, 39 | workflows.AcceptSubmissionSignal{Accepted: true, CandidateDetails: details}, 40 | ) 41 | } 42 | }) 43 | 44 | env.ExecuteWorkflow(workflows.BackgroundCheck, &workflows.BackgroundCheckWorkflowInput{Email: "john@example.com", Tier: "standard"}) 45 | 46 | var result workflows.BackgroundCheckWorkflowResult 47 | err := env.GetWorkflowResult(&result) 48 | assert.NoError(t, err) 49 | assert.Empty(t, result.SearchErrors) 50 | } 51 | 52 | func TestBackgroundCheckWorkflowFull(t *testing.T) { 53 | s := testsuite.WorkflowTestSuite{} 54 | env := s.NewTestWorkflowEnvironment() 55 | a := activities.Activities{SMTPStub: true, HTTPStub: true} 56 | 57 | env.RegisterWorkflow(workflows.Accept) 58 | env.RegisterActivity(a.SendAcceptEmail) 59 | env.RegisterWorkflow(workflows.SSNTrace) 60 | env.RegisterActivity(a.SSNTrace) 61 | env.RegisterWorkflow(workflows.FederalCriminalSearch) 62 | env.RegisterActivity(a.FederalCriminalSearch) 63 | env.RegisterWorkflow(workflows.StateCriminalSearch) 64 | env.RegisterActivity(a.StateCriminalSearch) 65 | env.RegisterWorkflow(workflows.MotorVehicleIncidentSearch) 66 | env.RegisterWorkflow(workflows.EmploymentVerification) 67 | env.RegisterActivity(a.SendEmploymentVerificationRequestEmail) 68 | env.RegisterActivity(a.SendReportEmail) 69 | 70 | details := workflows.CandidateDetails{ 71 | FullName: "John Smith", 72 | SSN: "111-11-1111", 73 | DOB: "1981-01-01", 74 | Address: "1 Chestnut Avenue", 75 | } 76 | 77 | env.SetOnChildWorkflowStartedListener(func(workflowInfo *workflow.Info, ctx workflow.Context, args converter.EncodedValues) { 78 | if workflowInfo.WorkflowExecution.ID == workflows.AcceptWorkflowID("john@example.com") { 79 | env.SignalWorkflowByID( 80 | workflows.AcceptWorkflowID("john@example.com"), 81 | workflows.AcceptSubmissionSignalName, 82 | workflows.AcceptSubmissionSignal{Accepted: true, CandidateDetails: details}, 83 | ) 84 | } 85 | }) 86 | 87 | env.ExecuteWorkflow(workflows.BackgroundCheck, &workflows.BackgroundCheckWorkflowInput{Email: "john@example.com", Tier: "full"}) 88 | 89 | var result workflows.BackgroundCheckWorkflowResult 90 | err := env.GetWorkflowResult(&result) 91 | assert.NoError(t, err) 92 | assert.Empty(t, result.SearchErrors) 93 | } 94 | -------------------------------------------------------------------------------- /workflows/employment_verification.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/temporalio/background-checks/activities" 8 | "go.temporal.io/sdk/workflow" 9 | ) 10 | 11 | const ( 12 | EmploymentVerificationDetailsQuery = "employment-verification-details" 13 | EmploymentVerificationSubmissionSignalName = "employment-verification-submission" 14 | ResearchDeadline = time.Hour * 24 * 7 15 | ) 16 | 17 | // chooseResearcher encapsulates the logic that randomly chooses a Researcher using a Side Effect. 18 | func chooseResearcher(ctx workflow.Context, input *EmploymentVerificationWorkflowInput) (string, error) { 19 | researchers := []string{ 20 | "researcher1@example.com", 21 | "researcher2@example.com", 22 | "researcher3@example.com", 23 | } 24 | 25 | // Here we just pick a random researcher. 26 | // In a real use case you may round-robin, decide based on price or current workload, 27 | // or fetch a researcher from a third party API. 28 | var researcher string 29 | r := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} { 30 | return researchers[rand.Intn(len(researchers))] 31 | }) 32 | err := r.Get(&researcher) 33 | 34 | return researcher, err 35 | } 36 | 37 | // emailEmploymentVerificationRequest encapsulates the logic that calls for the execution an Activity. 38 | func emailEmploymentVerificationRequest(ctx workflow.Context, input *EmploymentVerificationWorkflowInput, email string) error { 39 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 40 | StartToCloseTimeout: time.Minute, 41 | }) 42 | 43 | evsend := workflow.ExecuteActivity(ctx, a.SendEmploymentVerificationRequestEmail, activities.SendEmploymentVerificationEmailInput{ 44 | Email: email, 45 | Token: TokenForWorkflow(ctx), 46 | }) 47 | return evsend.Get(ctx, nil) 48 | } 49 | 50 | // waitForEmploymentVerificationSubmission encapsulates the logic that waits on and handles a Signal. 51 | func waitForEmploymentVerificationSubmission(ctx workflow.Context) (*EmploymentVerificationSubmission, error) { 52 | var response EmploymentVerificationSubmission 53 | var err error 54 | 55 | s := workflow.NewSelector(ctx) 56 | 57 | ch := workflow.GetSignalChannel(ctx, EmploymentVerificationSubmissionSignalName) 58 | s.AddReceive(ch, func(c workflow.ReceiveChannel, more bool) { 59 | var submission EmploymentVerificationSubmissionSignal 60 | c.Receive(ctx, &submission) 61 | 62 | response = EmploymentVerificationSubmission(submission) 63 | }) 64 | s.AddFuture(workflow.NewTimer(ctx, ResearchDeadline), func(f workflow.Future) { 65 | err = f.Get(ctx, nil) 66 | 67 | // We should probably fail the (child) workflow here. 68 | response.EmploymentVerificationComplete = false 69 | response.EmployerVerified = false 70 | }) 71 | 72 | s.Select(ctx) 73 | 74 | return &response, err 75 | } 76 | 77 | type EmploymentVerificationWorkflowInput struct { 78 | CandidateDetails CandidateDetails 79 | } 80 | 81 | type EmploymentVerificationWorkflowResult struct { 82 | EmploymentVerificationComplete bool 83 | EmployerVerified bool 84 | } 85 | 86 | // @@@SNIPSTART background-checks-employment-verification-workflow-definition 87 | 88 | // EmploymentVerification is a Workflow Definition that calls for the execution of a Side Effect, and an Activity, 89 | // but then waits on and handles a Signal. It is also capable of handling a Query to get Candidate Details. 90 | // This is executed as a Child Workflow by the main Background Check. 91 | func EmploymentVerification(ctx workflow.Context, input *EmploymentVerificationWorkflowInput) (*EmploymentVerificationWorkflowResult, error) { 92 | var result EmploymentVerificationWorkflowResult 93 | 94 | err := workflow.SetQueryHandler(ctx, EmploymentVerificationDetailsQuery, func() (CandidateDetails, error) { 95 | return input.CandidateDetails, nil 96 | }) 97 | if err != nil { 98 | return &result, err 99 | } 100 | 101 | researcher, err := chooseResearcher(ctx, input) 102 | if err != nil { 103 | return &result, err 104 | } 105 | 106 | err = emailEmploymentVerificationRequest(ctx, input, researcher) 107 | if err != nil { 108 | return &result, err 109 | } 110 | submission, err := waitForEmploymentVerificationSubmission(ctx) 111 | 112 | result = EmploymentVerificationWorkflowResult(*submission) 113 | return &result, err 114 | } 115 | 116 | // @@@SNIPEND 117 | -------------------------------------------------------------------------------- /workflows/employment_verification_test.go: -------------------------------------------------------------------------------- 1 | package workflows_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/temporalio/background-checks/activities" 10 | "github.com/temporalio/background-checks/workflows" 11 | "go.temporal.io/sdk/testsuite" 12 | ) 13 | 14 | func TestEmploymentVerificationWorkflow(t *testing.T) { 15 | s := testsuite.WorkflowTestSuite{} 16 | env := s.NewTestWorkflowEnvironment() 17 | var a *activities.Activities 18 | 19 | details := workflows.CandidateDetails{ 20 | FullName: "John Smith", 21 | SSN: "111-11-1111", 22 | DOB: "1981-01-01", 23 | Address: "1 Chestnut Avenue", 24 | } 25 | 26 | env.OnActivity(a.SendEmploymentVerificationRequestEmail, mock.Anything, mock.Anything).Return( 27 | func(ctx context.Context, input *activities.SendEmploymentVerificationEmailInput) (*activities.SendEmploymentVerificationEmailResult, error) { 28 | return &activities.SendEmploymentVerificationEmailResult{}, nil 29 | }, 30 | ) 31 | 32 | env.RegisterDelayedCallback( 33 | func() { 34 | env.SignalWorkflow( 35 | workflows.EmploymentVerificationSubmissionSignalName, 36 | workflows.EmploymentVerificationSubmissionSignal{EmploymentVerificationComplete: true, EmployerVerified: true}, 37 | ) 38 | }, 39 | 0, 40 | ) 41 | 42 | env.ExecuteWorkflow(workflows.EmploymentVerification, &workflows.EmploymentVerificationWorkflowInput{CandidateDetails: details}) 43 | 44 | var result workflows.EmploymentVerificationWorkflowResult 45 | err := env.GetWorkflowResult(&result) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, workflows.EmploymentVerificationWorkflowResult{EmploymentVerificationComplete: true, EmployerVerified: true}, result) 49 | } 50 | 51 | func TestEmploymentVerificationWorkflowTimeout(t *testing.T) { 52 | s := testsuite.WorkflowTestSuite{} 53 | env := s.NewTestWorkflowEnvironment() 54 | var a *activities.Activities 55 | 56 | details := workflows.CandidateDetails{ 57 | FullName: "John Smith", 58 | SSN: "111-11-1111", 59 | DOB: "1981-01-01", 60 | Address: "1 Chestnut Avenue", 61 | } 62 | 63 | env.OnActivity(a.SendEmploymentVerificationRequestEmail, mock.Anything, mock.Anything).Return( 64 | func(ctx context.Context, input *activities.SendEmploymentVerificationEmailInput) (*activities.SendEmploymentVerificationEmailResult, error) { 65 | return &activities.SendEmploymentVerificationEmailResult{}, nil 66 | }, 67 | ) 68 | 69 | env.ExecuteWorkflow(workflows.EmploymentVerification, &workflows.EmploymentVerificationWorkflowInput{CandidateDetails: details}) 70 | 71 | var result workflows.EmploymentVerificationWorkflowResult 72 | err := env.GetWorkflowResult(&result) 73 | assert.NoError(t, err) 74 | 75 | assert.Equal(t, workflows.EmploymentVerificationWorkflowResult{EmploymentVerificationComplete: false, EmployerVerified: false}, result) 76 | } 77 | -------------------------------------------------------------------------------- /workflows/federal_criminal_search.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | type FederalCriminalSearchWorkflowInput struct { 11 | FullName string 12 | KnownAddresses []string 13 | } 14 | 15 | type FederalCriminalSearchWorkflowResult struct { 16 | Crimes []string 17 | } 18 | 19 | // @@@SNIPSTART background-checks-federal-criminal-workflow-definition 20 | func FederalCriminalSearch(ctx workflow.Context, input *FederalCriminalSearchWorkflowInput) (*FederalCriminalSearchWorkflowResult, error) { 21 | var result activities.FederalCriminalSearchResult 22 | 23 | name := input.FullName 24 | var address string 25 | if len(input.KnownAddresses) > 0 { 26 | address = input.KnownAddresses[0] 27 | } 28 | var crimes []string 29 | 30 | activityInput := activities.FederalCriminalSearchInput{ 31 | FullName: name, 32 | Address: address, 33 | } 34 | var activityResult activities.FederalCriminalSearchResult 35 | 36 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 37 | StartToCloseTimeout: time.Minute, 38 | }) 39 | 40 | federalcheck := workflow.ExecuteActivity(ctx, a.FederalCriminalSearch, activityInput) 41 | 42 | err := federalcheck.Get(ctx, &activityResult) 43 | if err == nil { 44 | crimes = append(crimes, activityResult.Crimes...) 45 | } 46 | result.Crimes = crimes 47 | 48 | r := FederalCriminalSearchWorkflowResult(result) 49 | return &r, nil 50 | } 51 | 52 | // @@@SNIPEND 53 | -------------------------------------------------------------------------------- /workflows/motor_vehicle_incident_search.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | type MotorVehicleIncidentSearchWorkflowInput struct { 11 | FullName string 12 | Address string 13 | } 14 | 15 | type MotorVehicleIncidentSearchWorkflowResult struct { 16 | LicenseValid bool 17 | MotorVehicleIncidents []string 18 | } 19 | 20 | // @@@SNIPSTART background-checks-motor-vehicle-workflow-definition 21 | func MotorVehicleIncidentSearch(ctx workflow.Context, input *MotorVehicleIncidentSearchWorkflowInput) (*MotorVehicleIncidentSearchWorkflowResult, error) { 22 | var result MotorVehicleIncidentSearchWorkflowResult 23 | 24 | name := input.FullName 25 | address := input.Address 26 | var motorvehicleIncidents []string 27 | 28 | activityInput := activities.MotorVehicleIncidentSearchInput{ 29 | FullName: name, 30 | Address: address, 31 | } 32 | var activityResult activities.MotorVehicleIncidentSearchResult 33 | 34 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 35 | StartToCloseTimeout: time.Minute, 36 | }) 37 | 38 | motorvehicleIncidentSearch := workflow.ExecuteActivity(ctx, a.MotorVehicleIncidentSearch, activityInput) 39 | 40 | err := motorvehicleIncidentSearch.Get(ctx, &activityResult) 41 | if err == nil { 42 | motorvehicleIncidents = append(motorvehicleIncidents, activityResult.MotorVehicleIncidents...) 43 | } 44 | result.MotorVehicleIncidents = motorvehicleIncidents 45 | 46 | r := MotorVehicleIncidentSearchWorkflowResult(result) 47 | return &r, nil 48 | } 49 | 50 | // @@@SNIPEND 51 | -------------------------------------------------------------------------------- /workflows/ssn_trace.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | type SSNTraceWorkflowInput struct { 11 | FullName string 12 | SSN string 13 | } 14 | 15 | type SSNTraceWorkflowResult struct { 16 | SSNIsValid bool 17 | KnownAddresses []string 18 | } 19 | 20 | // @@@SNIPSTART background-checks-ssn-trace-workflow-definition 21 | 22 | // SSNTrace is a Workflow Definition that calls for the execution of a single Activity. 23 | // This is executed as a Child Workflow by the main Background Check. 24 | func SSNTrace(ctx workflow.Context, input *SSNTraceWorkflowInput) (*SSNTraceWorkflowResult, error) { 25 | var result activities.SSNTraceResult 26 | 27 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 28 | StartToCloseTimeout: time.Minute, 29 | }) 30 | 31 | f := workflow.ExecuteActivity(ctx, a.SSNTrace, SSNTraceWorkflowInput(*input)) 32 | 33 | err := f.Get(ctx, &result) 34 | r := SSNTraceWorkflowResult(result) 35 | return &r, err 36 | } 37 | 38 | // @@@SNIPEND 39 | -------------------------------------------------------------------------------- /workflows/state_criminal_search.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/background-checks/activities" 7 | "go.temporal.io/sdk/workflow" 8 | ) 9 | 10 | type StateCriminalSearchWorkflowInput struct { 11 | FullName string 12 | KnownAddresses []string 13 | } 14 | 15 | type StateCriminalSearchWorkflowResult struct { 16 | Crimes []string 17 | } 18 | 19 | // @@@SNIPSTART background-checks-state-criminal-workflow-definition 20 | 21 | // StateCriminalSearch is a Workflow Definition that calls for the execution an Activity for 22 | // each address associated with the Candidate. 23 | // This is executed as a Child Workflow by the main Background Check. 24 | func StateCriminalSearch(ctx workflow.Context, input *StateCriminalSearchWorkflowInput) (*StateCriminalSearchWorkflowResult, error) { 25 | var result StateCriminalSearchWorkflowResult 26 | 27 | name := input.FullName 28 | knownaddresses := input.KnownAddresses 29 | var crimes []string 30 | 31 | for _, address := range knownaddresses { 32 | activityInput := activities.StateCriminalSearchInput{ 33 | FullName: name, 34 | Address: address, 35 | } 36 | var activityResult activities.StateCriminalSearchResult 37 | 38 | ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 39 | StartToCloseTimeout: time.Minute, 40 | }) 41 | 42 | statecheck := workflow.ExecuteActivity(ctx, a.StateCriminalSearch, activityInput) 43 | 44 | err := statecheck.Get(ctx, &activityResult) 45 | if err == nil { 46 | crimes = append(crimes, activityResult.Crimes...) 47 | } 48 | } 49 | result.Crimes = crimes 50 | 51 | r := StateCriminalSearchWorkflowResult(result) 52 | return &r, nil 53 | } 54 | 55 | // @@@SNIPEND 56 | -------------------------------------------------------------------------------- /workflows/workflows.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "path" 7 | 8 | "go.temporal.io/sdk/workflow" 9 | ) 10 | 11 | type CandidateDetails struct { 12 | FullName string 13 | Address string 14 | SSN string 15 | DOB string 16 | Employer string 17 | } 18 | 19 | type AcceptSubmission struct { 20 | Accepted bool 21 | CandidateDetails CandidateDetails 22 | } 23 | 24 | type AcceptSubmissionSignal struct { 25 | Accepted bool 26 | CandidateDetails CandidateDetails 27 | } 28 | 29 | type EmploymentVerificationSubmission struct { 30 | EmploymentVerificationComplete bool 31 | EmployerVerified bool 32 | } 33 | 34 | type EmploymentVerificationSubmissionSignal struct { 35 | EmploymentVerificationComplete bool 36 | EmployerVerified bool 37 | } 38 | 39 | type KnownAddress struct { 40 | Address string 41 | City string 42 | State string 43 | ZipCode string 44 | } 45 | 46 | func BackgroundCheckWorkflowID(email string) string { 47 | return fmt.Sprintf("BackgroundCheck:%s", email) 48 | } 49 | 50 | func AcceptWorkflowID(email string) string { 51 | return fmt.Sprintf("Accept:%s", email) 52 | } 53 | 54 | func EmploymentVerificationWorkflowID(email string) string { 55 | return fmt.Sprintf("EmploymentVerification:%s", email) 56 | } 57 | 58 | func SearchWorkflowID(email string, name string) string { 59 | return fmt.Sprintf("%s:%s", name, email) 60 | } 61 | 62 | func TokenForWorkflow(ctx workflow.Context) string { 63 | info := workflow.GetInfo(ctx) 64 | 65 | rawToken := path.Join(info.WorkflowExecution.ID, info.WorkflowExecution.RunID) 66 | 67 | return base64.URLEncoding.EncodeToString([]byte(rawToken)) 68 | } 69 | 70 | func WorkflowFromToken(token string) (string, string, error) { 71 | var rawToken []byte 72 | 73 | rawToken, err := base64.URLEncoding.DecodeString(token) 74 | if err != nil { 75 | return "", "", err 76 | } 77 | 78 | wfid := path.Dir(string(rawToken)) 79 | runid := path.Base(string(rawToken)) 80 | 81 | return wfid, runid, nil 82 | } 83 | --------------------------------------------------------------------------------