├── .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 |
89 |
90 |
91 | Background Check Request - Candidate
92 |
93 |
94 |
95 |
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 |
89 |
90 |
91 | Background Check Request - Researcher
92 |
93 |
94 |
95 |
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 |
89 |
90 |
91 | Background Check Request - Hiring Manager
92 |
93 |
94 |
95 |
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 |
89 |
90 |
91 | Background Check Request - Candidate
92 |
93 |
94 |
95 |
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 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/ui/accepted.go.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | Background Check Request - Candidate
92 |
93 |
94 |
95 |
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 |
10 |
26 |
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 |
89 |
90 |
91 | Background Check Request - Researcher
92 |
93 |
94 |
95 |
106 |
107 |
Employment Confirmation for {{.Candidate.FullName}}
108 |
Please confirm that the candidate is currently employed by {{.Candidate.Employer}}
109 |
110 |
111 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/ui/employment_verified.go.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | Background Check Request - Researcher
92 |
93 |
94 |
95 |
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 |
89 |
90 |
91 | Background Check Request - Hiring Manager
92 |
93 |
94 |
95 |
105 |
106 |
View Candidate Report
107 |
108 |
109 |
110 |
111 | #
112 | Check Type
113 | Check Result
114 |
115 |
116 |
117 | 1
118 | SSN Verification {{ .SSNTrace.SSNIsValid }}
119 |
120 |
121 | 2
122 | Employment Verification {{ .SearchResults.EmploymentVerification.EmployerVerified }}
123 |
124 |
125 | 3
126 | Motor Vehicle Incident Search {{ .SearchResults.MotorVehicleIncidentSearch.MotorVehicleIncidents }}
127 |
128 |
129 | 4
130 | State Criminal Search {{ .SearchResults.StateCriminalSearch.Crimes }}
131 |
132 |
133 | 5
134 | Federal Criminal Search {{ .SearchResults.FederalCriminalSearch.Crimes }}
135 |
136 |
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 |
--------------------------------------------------------------------------------