From d5dd6ceaf77edb5a43235e9c44c63678b8137c2c Mon Sep 17 00:00:00 2001 From: Lauren Krugen Date: Thu, 25 Jan 2024 11:06:34 -0700 Subject: [PATCH 1/4] adding ctxlogger to writer.go file logs --- bcda/api/alr.go | 6 +-- bcda/api/requests.go | 65 ++++++++++++++-------------- bcda/auth/middleware.go | 36 +++++++-------- bcda/responseutils/v2/writer.go | 32 +++++++------- bcda/responseutils/v2/writer_test.go | 24 +++++++--- bcda/responseutils/writer.go | 26 ++++++----- bcda/responseutils/writer_test.go | 24 +++++++--- bcda/web/middleware/middleware.go | 2 +- bcda/web/middleware/ratelimit.go | 2 +- bcda/web/middleware/validation.go | 24 +++++----- 10 files changed, 137 insertions(+), 104 deletions(-) diff --git a/bcda/api/alr.go b/bcda/api/alr.go index 4c4602964..bdc94411c 100644 --- a/bcda/api/alr.go +++ b/bcda/api/alr.go @@ -63,7 +63,7 @@ func (h *Handler) alrRequest(w http.ResponseWriter, r *http.Request) { log.API.Error(err.Error()) oo := responseutils.CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, responseutils.InternalErr, err.Error()) - responseutils.WriteError(oo, w, http.StatusInternalServerError) + responseutils.WriteError(ctx, oo, w, http.StatusInternalServerError) return } @@ -74,14 +74,14 @@ func (h *Handler) alrRequest(w http.ResponseWriter, r *http.Request) { } log.API.Errorf("Could not handle ALR request %s", err.Error()) oo := responseutils.CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, responseutils.DbErr, "") - responseutils.WriteError(oo, w, http.StatusInternalServerError) + responseutils.WriteError(ctx, oo, w, http.StatusInternalServerError) return } if err = tx.Commit(); err != nil { log.API.Errorf("Failed to commit transaction %s", err.Error()) oo := responseutils.CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, responseutils.DbErr, "") - responseutils.WriteError(oo, w, http.StatusInternalServerError) + responseutils.WriteError(ctx, oo, w, http.StatusInternalServerError) return } diff --git a/bcda/api/requests.go b/bcda/api/requests.go index b5c62cd84..e4f518b32 100644 --- a/bcda/api/requests.go +++ b/bcda/api/requests.go @@ -63,9 +63,9 @@ type Handler struct { } type fhirResponseWriter interface { - Exception(http.ResponseWriter, int, string, string) - NotFound(http.ResponseWriter, int, string, string) - JobsBundle(http.ResponseWriter, []*models.Job, string) + Exception(context.Context, http.ResponseWriter, int, string, string) + NotFound(context.Context, http.ResponseWriter, int, string, string) + JobsBundle(context.Context, http.ResponseWriter, []*models.Job, string) } func NewHandler(dataTypes map[string]service.DataType, basePath string, apiVersion string) *Handler { @@ -147,7 +147,7 @@ func (h *Handler) BulkGroupRequest(w http.ResponseWriter, r *http.Request) { } fallthrough default: - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, "Invalid group ID") + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, "Invalid group ID") return } h.bulkRequest(w, r, reqType) @@ -174,7 +174,7 @@ func (h *Handler) JobsStatus(w http.ResponseWriter, r *http.Request) { statusTypes = append(statusTypes, models.JobStatus(status)) } else { errMsg := fmt.Sprintf("Repeated status type %s", status) - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, errMsg) + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg) return } } @@ -182,14 +182,14 @@ func (h *Handler) JobsStatus(w http.ResponseWriter, r *http.Request) { // validate status types provided match our valid list of statuses if err = h.validateStatuses(statusTypes); err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) return } } if ad, err = readAuthData(r); err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusUnauthorized, responseutils.TokenErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusUnauthorized, responseutils.TokenErr, "") return } @@ -198,9 +198,9 @@ func (h *Handler) JobsStatus(w http.ResponseWriter, r *http.Request) { logger.Error(err) if ok := goerrors.As(err, &service.JobsNotFoundError{}); ok { - h.RespWriter.Exception(w, http.StatusNotFound, responseutils.DbErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusNotFound, responseutils.DbErr, err.Error()) } else { - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") } } @@ -210,7 +210,8 @@ func (h *Handler) JobsStatus(w http.ResponseWriter, r *http.Request) { } host := fmt.Sprintf("%s://%s", scheme, r.Host) - h.RespWriter.JobsBundle(w, jobs, host) + // pass in the ctx here and log with the ctx logger + h.RespWriter.JobsBundle(r.Context(), w, jobs, host) } func (h *Handler) validateStatuses(statusTypes []models.JobStatus) error { @@ -233,7 +234,7 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { logger.Error(err) //We don't need to return the full error to a consumer. //We pass a bad request header (400) for this exception due to the inputs always being invalid for our purposes - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, "") return } @@ -244,7 +245,7 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { // NOTE: This is a catch all and may not necessarily mean that the job was not found. // So returning a StatusNotFound may be a misnomer //In contrast to above, if the input COULD be valid, we return a not found header (404) - h.RespWriter.Exception(w, http.StatusNotFound, responseutils.DbErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusNotFound, responseutils.DbErr, "") return } @@ -253,7 +254,7 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { case models.JobStatusFailed, models.JobStatusFailedExpired: logger.Error(job.Status) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.JobFailed, responseutils.DetailJobFailed) + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.JobFailed, responseutils.DetailJobFailed) case models.JobStatusPending, models.JobStatusInProgress: w.Header().Set("X-Progress", job.StatusMessage()) w.WriteHeader(http.StatusAccepted) @@ -262,7 +263,7 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { // If the job should be expired, but the cleanup job hasn't run for some reason, still respond with 410 if job.UpdatedAt.Add(h.JobTimeout).Before(time.Now()) { w.Header().Set("Expires", job.UpdatedAt.Add(h.JobTimeout).String()) - h.RespWriter.Exception(w, http.StatusGone, responseutils.NotFoundErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusGone, responseutils.NotFoundErr, "") return } w.Header().Set("Content-Type", constants.JsonContentType) @@ -304,23 +305,23 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { jsonData, err := json.Marshal(rb) if err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } _, err = w.Write([]byte(jsonData)) if err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } w.WriteHeader(http.StatusOK) case models.JobStatusArchived, models.JobStatusExpired: w.Header().Set("Expires", job.UpdatedAt.Add(h.JobTimeout).String()) - h.RespWriter.Exception(w, http.StatusGone, responseutils.NotFoundErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusGone, responseutils.NotFoundErr, "") case models.JobStatusCancelled, models.JobStatusCancelledExpired: - h.RespWriter.NotFound(w, http.StatusNotFound, responseutils.NotFoundErr, "Job has been cancelled.") + h.RespWriter.NotFound(r.Context(), w, http.StatusNotFound, responseutils.NotFoundErr, "Job has been cancelled.") } } @@ -333,7 +334,7 @@ func (h *Handler) DeleteJob(w http.ResponseWriter, r *http.Request) { if err != nil { err = errors.Wrap(err, "cannot convert jobID to uint") logger.Error(err) - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) return } @@ -342,11 +343,11 @@ func (h *Handler) DeleteJob(w http.ResponseWriter, r *http.Request) { switch err { case service.ErrJobNotCancellable: logger.Info(errors.Wrap(err, "Job is not cancellable")) - h.RespWriter.Exception(w, http.StatusGone, responseutils.DeletedErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusGone, responseutils.DeletedErr, err.Error()) return default: logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.DbErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.DbErr, err.Error()) return } } @@ -402,7 +403,7 @@ func (h *Handler) AttributionStatus(w http.ResponseWriter, r *http.Request) { if resp.Data == nil { logger.Error(errors.New("Could not find any CCLF8 files")) - h.RespWriter.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusNotFound, responseutils.NotFoundErr, "") return } @@ -456,7 +457,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se if ad, err = readAuthData(r); err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusUnauthorized, responseutils.TokenErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusUnauthorized, responseutils.TokenErr, "") return } @@ -469,14 +470,14 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se if err = h.validateResources(resourceTypes, ad.CMSID); err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) + h.RespWriter.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, err.Error()) return } bb, err := client.NewBlueButtonClient(client.NewConfig(h.bbBasePath)) if err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } @@ -500,7 +501,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se if err != nil { err = fmt.Errorf("failed to start transaction: %w", err) logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } // Use a transaction backed repository to ensure all of our upserts are encapsulated into a single transaction @@ -527,7 +528,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se // We've added logic into the worker to handle this situation. if err = tx.Commit(); err != nil { logger.Error(err.Error()) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.DbErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.DbErr, "") return } @@ -539,7 +540,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se newJob.ID, err = rtx.CreateJob(ctx, newJob) if err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.DbErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.DbErr, "") return } @@ -560,7 +561,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se b, err := bb.GetPatient(jobData, "0") if err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.FormatErr, "Failure to retrieve transactionTime metadata from FHIR Data Server.") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.FormatErr, "Failure to retrieve transactionTime metadata from FHIR Data Server.") return } newJob.TransactionTime = b.Meta.LastUpdated @@ -593,7 +594,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se respCode = http.StatusInternalServerError errType = responseutils.InternalErr } - h.RespWriter.Exception(w, respCode, errType, err.Error()) + h.RespWriter.Exception(r.Context(), w, respCode, errType, err.Error()) return } newJob.JobCount = len(queJobs) @@ -601,7 +602,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se // We've now computed all of the fields necessary to populate a fully defined job if err = rtx.UpdateJob(ctx, newJob); err != nil { logger.Error(err.Error()) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.DbErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.DbErr, "") return } @@ -614,7 +615,7 @@ func (h *Handler) bulkRequest(w http.ResponseWriter, r *http.Request, reqType se if err = h.Enq.AddJob(*j, int(jobPriority)); err != nil { logger.Error(err) - h.RespWriter.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } } diff --git a/bcda/auth/middleware.go b/bcda/auth/middleware.go index 478f81026..182f28a7e 100644 --- a/bcda/auth/middleware.go +++ b/bcda/auth/middleware.go @@ -53,7 +53,7 @@ func ParseToken(next http.Handler) http.Handler { authSubmatches := authRegexp.FindStringSubmatch(authHeader) if len(authSubmatches) < 2 { log.Auth.Warn("Invalid Authorization header value") - rw.Exception(w, http.StatusUnauthorized, responseutils.TokenErr, "") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusUnauthorized, responseutils.TokenErr, "") return } @@ -61,7 +61,7 @@ func ParseToken(next http.Handler) http.Handler { token, ad, err := AuthorizeAccess(tokenString) if err != nil { - handleTokenVerificationError(w, rw, err) + handleTokenVerificationError(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, rw, err) return } @@ -102,23 +102,23 @@ func AuthorizeAccess(tokenString string) (*jwt.Token, AuthData, error) { return token, ad, nil } -func handleTokenVerificationError(w http.ResponseWriter, rw fhirResponseWriter, err error) { +func handleTokenVerificationError(ctx context.Context, w http.ResponseWriter, rw fhirResponseWriter, err error) { if err != nil { log.Auth.Error(err) switch err.(type) { case *customErrors.ExpiredTokenError: - rw.Exception(w, http.StatusUnauthorized, responseutils.ExpiredErr, "") + rw.Exception(ctx, w, http.StatusUnauthorized, responseutils.ExpiredErr, "") case *customErrors.EntityNotFoundError: - rw.Exception(w, http.StatusForbidden, responseutils.UnauthorizedErr, responseutils.UnknownEntityErr) + rw.Exception(ctx, w, http.StatusForbidden, responseutils.UnauthorizedErr, responseutils.UnknownEntityErr) case *customErrors.RequestorDataError: - rw.Exception(w, http.StatusBadRequest, responseutils.InternalErr, "") + rw.Exception(ctx, w, http.StatusBadRequest, responseutils.InternalErr, "") case *customErrors.RequestTimeoutError: - rw.Exception(w, http.StatusServiceUnavailable, responseutils.InternalErr, "") + rw.Exception(ctx, w, http.StatusServiceUnavailable, responseutils.InternalErr, "") case *customErrors.ConfigError, *customErrors.InternalParsingError, *customErrors.UnexpectedSSASError: - rw.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + rw.Exception(ctx, w, http.StatusInternalServerError, responseutils.InternalErr, "") default: - rw.Exception(w, http.StatusUnauthorized, responseutils.TokenErr, "") + rw.Exception(ctx, w, http.StatusUnauthorized, responseutils.TokenErr, "") } } } @@ -132,7 +132,7 @@ func RequireTokenAuth(next http.Handler) http.Handler { token := r.Context().Value(TokenContextKey) if token == nil { log.Auth.Error("No token found") - rw.Exception(w, http.StatusUnauthorized, responseutils.TokenErr, "") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusUnauthorized, responseutils.TokenErr, "") return } @@ -150,12 +150,12 @@ func CheckBlacklist(next http.Handler) http.Handler { ad, ok := r.Context().Value(AuthDataContextKey).(AuthData) if !ok { log.Auth.Error() - rw.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, "AuthData not found") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusNotFound, responseutils.NotFoundErr, "AuthData not found") return } if ad.Blacklisted { - rw.Exception(w, http.StatusForbidden, responseutils.UnauthorizedErr, fmt.Sprintf("ACO (CMS_ID: %s) is unauthorized", ad.CMSID)) + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusForbidden, responseutils.UnauthorizedErr, fmt.Sprintf("ACO (CMS_ID: %s) is unauthorized", ad.CMSID)) return } next.ServeHTTP(w, r) @@ -169,14 +169,14 @@ func RequireTokenJobMatch(next http.Handler) http.Handler { ad, ok := r.Context().Value(AuthDataContextKey).(AuthData) if !ok { log.Auth.Error() - rw.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, "AuthData not found") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusNotFound, responseutils.NotFoundErr, "AuthData not found") return } jobID, err := strconv.ParseUint(chi.URLParam(r, "jobID"), 10, 64) if err != nil { log.Auth.Error(err) - rw.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, err.Error()) + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusNotFound, responseutils.NotFoundErr, err.Error()) return } @@ -185,7 +185,7 @@ func RequireTokenJobMatch(next http.Handler) http.Handler { job, err := repository.GetJobByID(r.Context(), uint(jobID)) if err != nil { log.Auth.Error(err) - rw.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, "") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusNotFound, responseutils.NotFoundErr, "") return } @@ -193,7 +193,7 @@ func RequireTokenJobMatch(next http.Handler) http.Handler { if !strings.EqualFold(ad.ACOID, job.ACOID.String()) { log.Auth.Errorf("ACO %s does not have access to job ID %d %s", ad.ACOID, job.ID, job.ACOID) - rw.Exception(w, http.StatusNotFound, responseutils.NotFoundErr, "") + rw.Exception(log.NewStructuredLoggerEntry(log.Auth, r.Context()), w, http.StatusNotFound, responseutils.NotFoundErr, "") return } next.ServeHTTP(w, r) @@ -201,8 +201,8 @@ func RequireTokenJobMatch(next http.Handler) http.Handler { } type fhirResponseWriter interface { - Exception(http.ResponseWriter, int, string, string) - NotFound(http.ResponseWriter, int, string, string) + Exception(context.Context, http.ResponseWriter, int, string, string) + NotFound(context.Context, http.ResponseWriter, int, string, string) } func getRespWriter(path string) fhirResponseWriter { diff --git a/bcda/responseutils/v2/writer.go b/bcda/responseutils/v2/writer.go index 23ca8fb33..e76739be7 100644 --- a/bcda/responseutils/v2/writer.go +++ b/bcda/responseutils/v2/writer.go @@ -1,16 +1,17 @@ package responseutils import ( + "context" "fmt" "io" - "log" + "net/http" "time" "github.com/CMSgov/bcda-app/bcda/constants" "github.com/CMSgov/bcda-app/bcda/models" "github.com/CMSgov/bcda-app/conf" - logAPI "github.com/CMSgov/bcda-app/log" + "github.com/CMSgov/bcda-app/log" "github.com/google/fhir/go/fhirversion" "github.com/google/fhir/go/jsonformat" @@ -33,7 +34,7 @@ func init() { // Needed to comply with the NDJSON format that we are using. marshaller, err = jsonformat.NewMarshaller(false, "", "", fhirversion.R4) if err != nil { - log.Fatalf("Failed to create marshaller %s", err) + log.API.Fatalf("Failed to create marshaller %s", err) } } @@ -43,17 +44,17 @@ func NewResponseWriter() ResponseWriter { return ResponseWriter{} } -func (r ResponseWriter) Exception(w http.ResponseWriter, statusCode int, errType, errMsg string) { +func (r ResponseWriter) Exception(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) { oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, errType, errMsg) - WriteError(oo, w, statusCode) + WriteError(ctx, oo, w, statusCode) } -func (r ResponseWriter) NotFound(w http.ResponseWriter, statusCode int, errType, errMsg string) { +func (r ResponseWriter) NotFound(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) { oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_NOT_FOUND, errType, errMsg) - WriteError(oo, w, statusCode) + WriteError(ctx, oo, w, statusCode) } -func (r ResponseWriter) JobsBundle(w http.ResponseWriter, jobs []*models.Job, host string) { +func (r ResponseWriter) JobsBundle(ctx context.Context, w http.ResponseWriter, jobs []*models.Job, host string) { jb := CreateJobsBundle(jobs, host) WriteBundleResponse(jb, w) } @@ -161,14 +162,15 @@ func CreateOpOutcome(severity fhircodes.IssueSeverityCode_Value, code fhircodes. } } -func WriteError(outcome *fhirmodelOO.OperationOutcome, w http.ResponseWriter, code int) { +func WriteError(ctx context.Context, outcome *fhirmodelOO.OperationOutcome, w http.ResponseWriter, code int) { //Write application/fhir+json header on OperationOutcome responses //https://build.fhir.org/ig/HL7/bulk-data/export.html#response---error-status-1 + logger := log.GetCtxLogger(ctx) w.Header().Set(constants.ContentType, constants.FHIRJsonContentType) w.WriteHeader(code) _, err := WriteOperationOutcome(w, outcome) if err != nil { - logAPI.API.Error(err) + logger.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -278,13 +280,13 @@ func CreateCapabilityStatement(reldate time.Time, relversion, baseurl string) *f } return statement } -func WriteCapabilityStatement(statement *fhirmodelCS.CapabilityStatement, w http.ResponseWriter) { +func WriteCapabilityStatement(ctx context.Context, statement *fhirmodelCS.CapabilityStatement, w http.ResponseWriter) { resource := &fhirmodelCR.ContainedResource{ OneofResource: &fhirmodelCR.ContainedResource_CapabilityStatement{CapabilityStatement: statement}, } statementJSON, err := marshaller.Marshal(resource) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -293,7 +295,7 @@ func WriteCapabilityStatement(statement *fhirmodelCS.CapabilityStatement, w http w.WriteHeader(http.StatusOK) _, err = w.Write(statementJSON) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -305,7 +307,7 @@ func WriteBundleResponse(bundle *fhirmodelCR.Bundle, w http.ResponseWriter) { } bundleJSON, err := marshaller.Marshal(resource) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -314,7 +316,7 @@ func WriteBundleResponse(bundle *fhirmodelCR.Bundle, w http.ResponseWriter) { w.WriteHeader(http.StatusOK) _, err = w.Write(bundleJSON) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/bcda/responseutils/v2/writer_test.go b/bcda/responseutils/v2/writer_test.go index f32098ede..eca3ec7cc 100644 --- a/bcda/responseutils/v2/writer_test.go +++ b/bcda/responseutils/v2/writer_test.go @@ -1,6 +1,7 @@ package responseutils import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -10,6 +11,7 @@ import ( "github.com/CMSgov/bcda-app/bcda/constants" "github.com/CMSgov/bcda-app/bcda/models" responseutils "github.com/CMSgov/bcda-app/bcda/responseutils" + "github.com/CMSgov/bcda-app/log" "github.com/google/fhir/go/fhirversion" "github.com/google/fhir/go/jsonformat" fhircodes "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" @@ -17,6 +19,7 @@ import ( fhirmodelCS "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/capability_statement_go_proto" fhirvaluesets "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/valuesets_go_proto" "github.com/pborman/uuid" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -39,7 +42,9 @@ func TestResponseUtilsWriterTestSuite(t *testing.T) { } func (s *ResponseUtilsWriterTestSuite) TestResponseWriterException() { rw := NewResponseWriter() - rw.Exception(s.rr, http.StatusAccepted, responseutils.RequestErr, "TestResponseWriterExcepton") + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) + rw.Exception(ctx, s.rr, http.StatusAccepted, responseutils.RequestErr, "TestResponseWriterExcepton") res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -58,8 +63,9 @@ func (s *ResponseUtilsWriterTestSuite) TestResponseWriterException() { func (s *ResponseUtilsWriterTestSuite) TestResponseWriterNotFound() { rw := NewResponseWriter() - - rw.NotFound(s.rr, http.StatusAccepted, responseutils.RequestErr, "TestResponseWriterNotFound") + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) + rw.NotFound(ctx, s.rr, http.StatusAccepted, responseutils.RequestErr, "TestResponseWriterNotFound") res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -86,8 +92,10 @@ func (s *ResponseUtilsWriterTestSuite) TestCreateOpOutcome() { } func (s *ResponseUtilsWriterTestSuite) TestWriteError() { + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, responseutils.RequestErr, "TestCreateOpOutcome") - WriteError(oo, s.rr, http.StatusAccepted) + WriteError(ctx, oo, s.rr, http.StatusAccepted) res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -121,7 +129,7 @@ func (s *ResponseUtilsWriterTestSuite) TestWriteCapabilityStatement() { relversion := "r1" baseurl := "bcda.cms.gov" cs := CreateCapabilityStatement(time.Now(), relversion, baseurl) - WriteCapabilityStatement(cs, s.rr) + WriteCapabilityStatement(context.Background(), cs, s.rr) var respCS *fhirmodelCS.CapabilityStatement res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) @@ -231,3 +239,9 @@ func (s *ResponseUtilsWriterTestSuite) TestGetFhirStatusCode() { }) } } + +func MakeTestStructuredLoggerEntry(logFields logrus.Fields) *log.StructuredLoggerEntry { + var lggr logrus.Logger + newLogEntry := &log.StructuredLoggerEntry{Logger: lggr.WithFields(logFields)} + return newLogEntry +} diff --git a/bcda/responseutils/writer.go b/bcda/responseutils/writer.go index 443c639f4..2a1241409 100644 --- a/bcda/responseutils/writer.go +++ b/bcda/responseutils/writer.go @@ -1,9 +1,9 @@ package responseutils import ( + "context" "fmt" "io" - "log" "net/http" "strconv" "time" @@ -11,6 +11,7 @@ import ( "github.com/CMSgov/bcda-app/bcda/constants" "github.com/CMSgov/bcda-app/bcda/models" "github.com/CMSgov/bcda-app/conf" + "github.com/CMSgov/bcda-app/log" logAPI "github.com/CMSgov/bcda-app/log" "github.com/google/fhir/go/fhirversion" @@ -29,7 +30,7 @@ func init() { // Needed to comply with the NDJSON format that we are using. marshaller, err = jsonformat.NewMarshaller(false, "", "", fhirversion.STU3) if err != nil { - log.Fatalf("Failed to create marshaller %s", err) + log.API.Fatalf("Failed to create marshaller %s", err) } } @@ -39,17 +40,17 @@ func NewResponseWriter() ResponseWriter { return ResponseWriter{} } -func (r ResponseWriter) Exception(w http.ResponseWriter, statusCode int, errType, errMsg string) { +func (r ResponseWriter) Exception(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) { oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, errType, errMsg) - WriteError(oo, w, statusCode) + WriteError(ctx, oo, w, statusCode) } -func (r ResponseWriter) NotFound(w http.ResponseWriter, statusCode int, errType, errMsg string) { +func (r ResponseWriter) NotFound(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) { oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_NOT_FOUND, errType, errMsg) - WriteError(oo, w, statusCode) + WriteError(ctx, oo, w, statusCode) } -func (r ResponseWriter) JobsBundle(w http.ResponseWriter, jobs []*models.Job, host string) { +func (r ResponseWriter) JobsBundle(ctx context.Context, w http.ResponseWriter, jobs []*models.Job, host string) { jb := CreateJobsBundle(jobs, host) WriteBundleResponse(jb, w) } @@ -157,7 +158,8 @@ func CreateOpOutcome(severity fhircodes.IssueSeverityCode_Value, code fhircodes. } } -func WriteError(outcome *fhirmodels.OperationOutcome, w http.ResponseWriter, code int) { +func WriteError(ctx context.Context, outcome *fhirmodels.OperationOutcome, w http.ResponseWriter, code int) { + logger := log.GetCtxLogger(ctx) w.Header().Set(constants.ContentType, constants.FHIRJsonContentType) if code == http.StatusServiceUnavailable { includeRetryAfterHeader(w) @@ -165,7 +167,7 @@ func WriteError(outcome *fhirmodels.OperationOutcome, w http.ResponseWriter, cod w.WriteHeader(code) _, err := WriteOperationOutcome(w, outcome) if err != nil { - logAPI.API.Error(err) + logger.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -286,13 +288,13 @@ func CreateCapabilityStatement(reldate time.Time, relversion, baseurl string) *f } return statement } -func WriteCapabilityStatement(statement *fhirmodels.CapabilityStatement, w http.ResponseWriter) { +func WriteCapabilityStatement(ctx context.Context, statement *fhirmodels.CapabilityStatement, w http.ResponseWriter) { resource := &fhirmodels.ContainedResource{ OneofResource: &fhirmodels.ContainedResource_CapabilityStatement{CapabilityStatement: statement}, } statementJSON, err := marshaller.Marshal(resource) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -301,7 +303,7 @@ func WriteCapabilityStatement(statement *fhirmodels.CapabilityStatement, w http. w.WriteHeader(http.StatusOK) _, err = w.Write(statementJSON) if err != nil { - logAPI.API.Error(err) + log.API.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/bcda/responseutils/writer_test.go b/bcda/responseutils/writer_test.go index 3b6515fb9..009e01a9a 100644 --- a/bcda/responseutils/writer_test.go +++ b/bcda/responseutils/writer_test.go @@ -1,6 +1,7 @@ package responseutils import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -9,12 +10,14 @@ import ( "github.com/CMSgov/bcda-app/bcda/constants" "github.com/CMSgov/bcda-app/bcda/models" + "github.com/CMSgov/bcda-app/log" "github.com/google/fhir/go/fhirversion" "github.com/google/fhir/go/jsonformat" fhircodes "github.com/google/fhir/go/proto/google/fhir/proto/stu3/codes_go_proto" fhirdatatypes "github.com/google/fhir/go/proto/google/fhir/proto/stu3/datatypes_go_proto" fhirmodels "github.com/google/fhir/go/proto/google/fhir/proto/stu3/resources_go_proto" "github.com/pborman/uuid" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -38,7 +41,9 @@ func TestResponseUtilsWriterTestSuite(t *testing.T) { func (s *ResponseUtilsWriterTestSuite) TestResponseWriterException() { rw := NewResponseWriter() - rw.Exception(s.rr, http.StatusAccepted, RequestErr, "TestResponseWriterExcepton") + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) + rw.Exception(ctx, s.rr, http.StatusAccepted, RequestErr, "TestResponseWriterExcepton") res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -57,8 +62,9 @@ func (s *ResponseUtilsWriterTestSuite) TestResponseWriterException() { func (s *ResponseUtilsWriterTestSuite) TestResponseWriterNotFound() { rw := NewResponseWriter() - - rw.NotFound(s.rr, http.StatusAccepted, RequestErr, "TestResponseWriterNotFound") + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) + rw.NotFound(ctx, s.rr, http.StatusAccepted, RequestErr, "TestResponseWriterNotFound") res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -84,8 +90,10 @@ func (s *ResponseUtilsWriterTestSuite) TestCreateOpOutcome() { } func (s *ResponseUtilsWriterTestSuite) TestWriteError() { + newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"foo": "bar"}) + ctx := context.WithValue(context.Background(), log.CtxLoggerKey, newLogEntry) oo := CreateOpOutcome(fhircodes.IssueSeverityCode_ERROR, fhircodes.IssueTypeCode_EXCEPTION, RequestErr, "TestCreateOpOutcome") - WriteError(oo, s.rr, http.StatusAccepted) + WriteError(ctx, oo, s.rr, http.StatusAccepted) res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) assert.NoError(s.T(), err) @@ -119,7 +127,7 @@ func (s *ResponseUtilsWriterTestSuite) TestWriteCapabilityStatement() { relversion := "r1" baseurl := "bcda.cms.gov" cs := CreateCapabilityStatement(time.Now(), relversion, baseurl) - WriteCapabilityStatement(cs, s.rr) + WriteCapabilityStatement(context.Background(), cs, s.rr) var respCS *fhirmodels.CapabilityStatement res, err := s.unmarshaller.Unmarshal(s.rr.Body.Bytes()) @@ -229,3 +237,9 @@ func (s *ResponseUtilsWriterTestSuite) TestGetFhirStatusCode() { }) } } + +func MakeTestStructuredLoggerEntry(logFields logrus.Fields) *log.StructuredLoggerEntry { + var lggr logrus.Logger + newLogEntry := &log.StructuredLoggerEntry{Logger: lggr.WithFields(logFields)} + return newLogEntry +} diff --git a/bcda/web/middleware/middleware.go b/bcda/web/middleware/middleware.go index 725165135..34e7a1753 100644 --- a/bcda/web/middleware/middleware.go +++ b/bcda/web/middleware/middleware.go @@ -50,7 +50,7 @@ func ACOEnabled(cfg *service.Config) func(next http.Handler) http.Handler { if cfg.IsACODisabled(ad.CMSID) { logger := log.GetCtxLogger(r.Context()) logger.Error(fmt.Sprintf("failed to complete request, CMSID %s is not enabled", ad.CMSID)) - rw.Exception(w, http.StatusUnauthorized, responseutils.InternalErr, "") + rw.Exception(r.Context(), w, http.StatusUnauthorized, responseutils.InternalErr, "") return } next.ServeHTTP(w, r) diff --git a/bcda/web/middleware/ratelimit.go b/bcda/web/middleware/ratelimit.go index 3b6bb6808..ff48b0ad5 100644 --- a/bcda/web/middleware/ratelimit.go +++ b/bcda/web/middleware/ratelimit.go @@ -53,7 +53,7 @@ func CheckConcurrentJobs(next http.Handler) http.Handler { if err != nil { logger := log.GetCtxLogger(r.Context()) logger.Error(fmt.Errorf("failed to lookup pending and in-progress jobs: %w", err)) - rw.Exception(w, http.StatusInternalServerError, responseutils.InternalErr, "") + rw.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.InternalErr, "") return } if len(pendingAndInProgressJobs) > 0 { diff --git a/bcda/web/middleware/validation.go b/bcda/web/middleware/validation.go index 18091ab43..16a7e9e77 100644 --- a/bcda/web/middleware/validation.go +++ b/bcda/web/middleware/validation.go @@ -58,7 +58,7 @@ func ValidateRequestURL(next http.Handler) http.Handler { if _, found := supportedOutputFormats[params[0]]; !found { errMsg := fmt.Sprintf("_outputFormat parameter must be one of %v", getKeys(supportedOutputFormats)) log.API.Error(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, errMsg) return } } @@ -68,7 +68,7 @@ func ValidateRequestURL(next http.Handler) http.Handler { if ok { errMsg := "Invalid parameter: this server does not support the _elements parameter." log.API.Warn(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.RequestErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg) return } @@ -78,7 +78,7 @@ func ValidateRequestURL(next http.Handler) http.Handler { if strings.HasPrefix(key, "?") { errMsg := "Invalid parameter: query parameters cannot start with ?" log.API.Warn(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, errMsg) return } } @@ -90,12 +90,12 @@ func ValidateRequestURL(next http.Handler) http.Handler { if err != nil { errMsg := "Invalid date format supplied in _since parameter. Date must be in FHIR Instant format." log.API.Warn(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, errMsg) return } else if sinceDate.After(time.Now()) { errMsg := "Invalid date format supplied in _since parameter. Date must be a date that has already passed" log.API.Warn(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, errMsg) return } rp.Since = sinceDate @@ -112,7 +112,7 @@ func ValidateRequestURL(next http.Handler) http.Handler { } else { errMsg := fmt.Sprintf("Repeated resource type %s", resource) log.API.Error(errMsg) - rw.Exception(w, http.StatusBadRequest, responseutils.RequestErr, errMsg) + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.RequestErr, errMsg) return } } @@ -141,21 +141,21 @@ func ValidateRequestHeaders(next http.Handler) http.Handler { if acceptHeader == "" { logger.Warn("Accept header is required") - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, "Accept header is required") + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, "Accept header is required") return } else if acceptHeader != "application/fhir+json" { logger.Warn("application/fhir+json is the only supported response format") - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, "application/fhir+json is the only supported response format") + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, "application/fhir+json is the only supported response format") return } if preferHeader == "" { logger.Warn("Prefer header is required") - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, "Prefer header is required") + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, "Prefer header is required") return } else if preferHeader != "respond-async" { logger.Warn("Only asynchronous responses are supported") - rw.Exception(w, http.StatusBadRequest, responseutils.FormatErr, "Only asynchronous responses are supported") + rw.Exception(r.Context(), w, http.StatusBadRequest, responseutils.FormatErr, "Only asynchronous responses are supported") return } @@ -182,8 +182,8 @@ func getVersion(path string) (string, error) { } type fhirResponseWriter interface { - Exception(http.ResponseWriter, int, string, string) - NotFound(http.ResponseWriter, int, string, string) + Exception(context.Context, http.ResponseWriter, int, string, string) + NotFound(context.Context, http.ResponseWriter, int, string, string) } func getRespWriter(version string) (fhirResponseWriter, error) { From ed2296046e2146edb102d46f44caf29dcc40b641 Mon Sep 17 00:00:00 2001 From: Lauren Krugen Date: Thu, 25 Jan 2024 11:21:30 -0700 Subject: [PATCH 2/4] missed file --- bcda/api/v1/api.go | 307 ++++++++++++++++++++++++--------------------- 1 file changed, 162 insertions(+), 145 deletions(-) diff --git a/bcda/api/v1/api.go b/bcda/api/v1/api.go index a78fa61e2..13add23c6 100644 --- a/bcda/api/v1/api.go +++ b/bcda/api/v1/api.go @@ -39,142 +39,150 @@ func init() { } /* - swagger:route GET /api/v1/alr/$export alrData alrRequest +swagger:route GET /api/v1/alr/$export alrData alrRequest - Start FHIR STU3 data export for all supported resource types +# Start FHIR STU3 data export for all supported resource types - Initiates a job to collect Assignment List Report data for your ACO. Supported resource types are Patient, Coverage, Group, Risk Assessment, Observation, and Covid Episode. +Initiates a job to collect Assignment List Report data for your ACO. Supported resource types are Patient, Coverage, Group, Risk Assessment, Observation, and Covid Episode. - Produces: - - application/fhir+json +Produces: +- application/fhir+json - Security: - bearer_token: +Security: - Responses: - 202: BulkRequestResponse - 400: badRequestResponse - 401: invalidCredentials - 429: tooManyRequestsResponse - 500: errorResponse + bearer_token: + +Responses: + + 202: BulkRequestResponse + 400: badRequestResponse + 401: invalidCredentials + 429: tooManyRequestsResponse + 500: errorResponse */ func ALRRequest(w http.ResponseWriter, r *http.Request) { h.ALRRequest(w, r) } /* - swagger:route GET /api/v1/Patient/$export bulkData bulkPatientRequest +swagger:route GET /api/v1/Patient/$export bulkData bulkPatientRequest + +# Start FHIR STU3 data export for all supported resource types - Start FHIR STU3 data export for all supported resource types +Initiates a job to collect data from the Blue Button API for your ACO. Supported resource types are Patient, Coverage, and ExplanationOfBenefit. - Initiates a job to collect data from the Blue Button API for your ACO. Supported resource types are Patient, Coverage, and ExplanationOfBenefit. +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Security: - Security: - bearer_token: + bearer_token: - Responses: - 202: BulkRequestResponse - 400: badRequestResponse - 401: invalidCredentials - 429: tooManyRequestsResponse - 500: errorResponse +Responses: + + 202: BulkRequestResponse + 400: badRequestResponse + 401: invalidCredentials + 429: tooManyRequestsResponse + 500: errorResponse */ func BulkPatientRequest(w http.ResponseWriter, r *http.Request) { h.BulkPatientRequest(w, r) } /* - swagger:route GET /api/v1/Group/{groupId}/$export bulkData bulkGroupRequest + swagger:route GET /api/v1/Group/{groupId}/$export bulkData bulkGroupRequest - Start FHIR STU3 data export (for the specified group identifier) for all supported resource types + Start FHIR STU3 data export (for the specified group identifier) for all supported resource types - Initiates a job to collect data from the Blue Button API for your ACO. The supported Group identifiers are `all` and `runout`. + Initiates a job to collect data from the Blue Button API for your ACO. The supported Group identifiers are `all` and `runout`. - The `all` identifier returns data for the group of all patients attributed to the requesting ACO. If used when specifying `_since`: all claims data which has been updated since the specified date will be returned for beneficiaries which have been attributed to the ACO since before the specified date; and all historical claims data will be returned for beneficiaries which have been newly attributed to the ACO since the specified date. + The `all` identifier returns data for the group of all patients attributed to the requesting ACO. If used when specifying `_since`: all claims data which has been updated since the specified date will be returned for beneficiaries which have been attributed to the ACO since before the specified date; and all historical claims data will be returned for beneficiaries which have been newly attributed to the ACO since the specified date. - The `runout` identifier returns claims runouts data. + The `runout` identifier returns claims runouts data. - Produces: - - application/fhir+json + Produces: + - application/fhir+json - Security: - bearer_token: + Security: + bearer_token: - Responses: - 202: BulkRequestResponse - 400: badRequestResponse - 401: invalidCredentials - 429: tooManyRequestsResponse - 500: errorResponse + Responses: + 202: BulkRequestResponse + 400: badRequestResponse + 401: invalidCredentials + 429: tooManyRequestsResponse + 500: errorResponse */ func BulkGroupRequest(w http.ResponseWriter, r *http.Request) { h.BulkGroupRequest(w, r) } /* - swagger:route GET /api/v1/jobs/{jobId} job jobStatus +swagger:route GET /api/v1/jobs/{jobId} job jobStatus + +# Get job status - Get job status +Returns the current status of an export job. - Returns the current status of an export job. +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Schemes: http, https - Schemes: http, https +Security: - Security: - bearer_token: + bearer_token: - Responses: - 202: jobStatusResponse - 200: completedJobResponse - 400: badRequestResponse - 401: invalidCredentials - 404: notFoundResponse - 410: goneResponse - 500: errorResponse +Responses: + + 202: jobStatusResponse + 200: completedJobResponse + 400: badRequestResponse + 401: invalidCredentials + 404: notFoundResponse + 410: goneResponse + 500: errorResponse */ func JobStatus(w http.ResponseWriter, r *http.Request) { h.JobStatus(w, r) } /* - swagger:route GET /api/v1/jobs job jobsStatus +swagger:route GET /api/v1/jobs job jobsStatus + +# Get jobs statuses - Get jobs statuses +Returns the current statuses of export jobs. Supported status types are Completed, Archived, Expired, Failed, FailedExpired, +Pending, In Progress, Cancelled, and CancelledExpired. If no status(s) is provided, all jobs will be returned. - Returns the current statuses of export jobs. Supported status types are Completed, Archived, Expired, Failed, FailedExpired, - Pending, In Progress, Cancelled, and CancelledExpired. If no status(s) is provided, all jobs will be returned. +Note on job status to fhir task resource status mapping: +Due to the fhir task status field having a smaller set of values, the following statuses will be set to different fhir values in the response - Note on job status to fhir task resource status mapping: - Due to the fhir task status field having a smaller set of values, the following statuses will be set to different fhir values in the response +Archived, Expired -> Completed +FailedExpired -> Failed +Pending -> In Progress +CancelledExpired -> Cancelled - Archived, Expired -> Completed - FailedExpired -> Failed - Pending -> In Progress - CancelledExpired -> Cancelled +Though the status name has been remapped the response will still only contain jobs pertaining to the provided job status in the request. - Though the status name has been remapped the response will still only contain jobs pertaining to the provided job status in the request. +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Schemes: http, https - Schemes: http, https +Security: - Security: - bearer_token: + bearer_token: - Responses: - 200: jobsStatusResponse - 400: badRequestResponse - 401: invalidCredentials - 404: notFoundResponse - 410: goneResponse - 500: errorResponse +Responses: + + 200: jobsStatusResponse + 400: badRequestResponse + 401: invalidCredentials + 404: notFoundResponse + 410: goneResponse + 500: errorResponse */ func JobsStatus(w http.ResponseWriter, r *http.Request) { h.JobsStatus(w, r) @@ -194,76 +202,82 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) { } /* - swagger:route DELETE /api/v1/jobs/{jobId} job deleteJob +swagger:route DELETE /api/v1/jobs/{jobId} job deleteJob + +# Cancel a job - Cancel a job +Cancels a currently running job. - Cancels a currently running job. +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Schemes: http, https - Schemes: http, https +Security: - Security: - bearer_token: + bearer_token: - Responses: - 202: deleteJobResponse - 400: badRequestResponse - 401: invalidCredentials - 404: notFoundResponse - 410: goneResponse - 500: errorResponse +Responses: + + 202: deleteJobResponse + 400: badRequestResponse + 401: invalidCredentials + 404: notFoundResponse + 410: goneResponse + 500: errorResponse */ func DeleteJob(w http.ResponseWriter, r *http.Request) { h.DeleteJob(w, r) } /* - swagger:route GET /api/v1/attribution_status attributionStatus attributionStatus +swagger:route GET /api/v1/attribution_status attributionStatus attributionStatus + +# Get attribution status - Get attribution status +Returns the status of the latest ingestion for attribution and claims runout files. The response will contain the Type to identify which ingestion and a Timestamp for the last time it was updated. - Returns the status of the latest ingestion for attribution and claims runout files. The response will contain the Type to identify which ingestion and a Timestamp for the last time it was updated. +Produces: +- application/json - Produces: - - application/json +Schemes: http, https - Schemes: http, https +Security: - Security: - bearer_token: + bearer_token: - Responses: - 200: AttributionFileStatusResponse - 404: notFoundResponse +Responses: + + 200: AttributionFileStatusResponse + 404: notFoundResponse */ func AttributionStatus(w http.ResponseWriter, r *http.Request) { h.AttributionStatus(w, r) } /* - swagger:route GET /data/{jobId}/{filename} job serveData +swagger:route GET /data/{jobId}/{filename} job serveData + +# Get data file - Get data file +Returns the NDJSON file of data generated by an export job. Will be in the format .ndjson. Get the full value from the job status response - Returns the NDJSON file of data generated by an export job. Will be in the format .ndjson. Get the full value from the job status response +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Schemes: http, https - Schemes: http, https +Security: - Security: - bearer_token: + bearer_token: - Responses: - 200: FileNDJSON - 400: badRequestResponse - 401: invalidCredentials - 404: notFoundResponse - 500: errorResponse +Responses: + + 200: FileNDJSON + 400: badRequestResponse + 401: invalidCredentials + 404: notFoundResponse + 500: errorResponse */ func ServeData(w http.ResponseWriter, r *http.Request) { dataDir := conf.GetEnv("FHIR_PAYLOAD_DIR") @@ -293,19 +307,20 @@ func ServeData(w http.ResponseWriter, r *http.Request) { } /* - swagger:route GET /api/v1/metadata metadata metadata +swagger:route GET /api/v1/metadata metadata metadata + +# Get metadata - Get metadata +Returns metadata about the API. - Returns metadata about the API. +Produces: +- application/fhir+json - Produces: - - application/fhir+json +Schemes: http, https - Schemes: http, https +Responses: - Responses: - 200: MetadataResponse + 200: MetadataResponse */ func Metadata(w http.ResponseWriter, r *http.Request) { dt := time.Now() @@ -316,23 +331,24 @@ func Metadata(w http.ResponseWriter, r *http.Request) { } host := fmt.Sprintf("%s://%s", scheme, r.Host) statement := responseutils.CreateCapabilityStatement(dt, constants.Version, host) - responseutils.WriteCapabilityStatement(statement, w) + responseutils.WriteCapabilityStatement(r.Context(), statement, w) } /* - swagger:route GET /_version metadata getVersion +swagger:route GET /_version metadata getVersion - Get API version +# Get API version - Returns the version of the API that is currently running. Note that this endpoint is **not** prefixed with the base path (e.g. /api/v1). +Returns the version of the API that is currently running. Note that this endpoint is **not** prefixed with the base path (e.g. /api/v1). - Produces: - - application/json +Produces: +- application/json - Schemes: http, https +Schemes: http, https - Responses: - 200: VersionResponse +Responses: + + 200: VersionResponse */ func GetVersion(w http.ResponseWriter, r *http.Request) { respMap := make(map[string]string) @@ -383,19 +399,20 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) { } /* - swagger:route GET /_auth metadata getAuthInfo +swagger:route GET /_auth metadata getAuthInfo + +# Get details about auth - Get details about auth +Returns the auth provider that is currently being used. Note that this endpoint is **not** prefixed with the base path (e.g. /api/v1). - Returns the auth provider that is currently being used. Note that this endpoint is **not** prefixed with the base path (e.g. /api/v1). +Produces: +- application/json - Produces: - - application/json +Schemes: http, https - Schemes: http, https +Responses: - Responses: - 200: AuthResponse + 200: AuthResponse */ func GetAuthInfo(w http.ResponseWriter, r *http.Request) { respMap := make(map[string]string) From ce81d1cb2b283dac47eee9359a2e05daf9f99a3f Mon Sep 17 00:00:00 2001 From: Lauren Krugen Date: Thu, 25 Jan 2024 12:19:27 -0700 Subject: [PATCH 3/4] adding deployment target to dockerfile for CI --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index e7d4d6b58..f719222ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,6 +80,7 @@ services: dockerfile: Dockerfiles/Dockerfile.ssas environment: - DATABASE_URL=postgresql://postgres:toor@db:5432/bcda?sslmode=disable + - DEPLOYMENT_TARGET=local - JWT_PUBLIC_KEY_FILE=/var/local/public.pem - JWT_PRIVATE_KEY_FILE=/var/local/private.pem - DEBUG=true From 0a3ed38d5cb615d6642749f02e02b67d8f7f51a3 Mon Sep 17 00:00:00 2001 From: Lauren Krugen Date: Thu, 25 Jan 2024 13:38:06 -0700 Subject: [PATCH 4/4] updating dockerfile.ssas for ci --- Dockerfiles/Dockerfile.ssas | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfiles/Dockerfile.ssas b/Dockerfiles/Dockerfile.ssas index 1ff2abd83..b575d7862 100644 --- a/Dockerfiles/Dockerfile.ssas +++ b/Dockerfiles/Dockerfile.ssas @@ -36,6 +36,7 @@ RUN openssl rsa -in /var/local/private.pem -outform PEM -pubout -out /var/local/ # Make sure we are in the directory to ensure the config files are resolved as expected COPY --from=base /go/bcda-ssas-app/ /go/bcda-ssas-app/ +COPY --from=base /go/bcda-ssas-app/ssas/cfg/configs /go/src/github.com/CMSgov/bcda-ssas-app/ssas/cfg/configs COPY --from=base /go/bin/ /go/bin/ WORKDIR /go/bcda-ssas-app/ssas/