Skip to content

Commit

Permalink
[#175014199] Health checks (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
balanza authored Oct 6, 2020
1 parent 3918cfd commit 57ed674
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 8 deletions.
30 changes: 30 additions & 0 deletions Info/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { taskEither, fromLeft } from "fp-ts/lib/TaskEither";
import { HealthCheck, HealthProblem } from "../../utils/healthcheck";
import { InfoHandler } from "../handler";

afterEach(() => {
jest.clearAllMocks();
});

describe("InfoHandler", () => {
it("should return an internal error if the application is not healthy", async () => {
const healthCheck: HealthCheck = fromLeft([
"failure 1" as HealthProblem<"Config">,
"failure 2" as HealthProblem<"Config">
]);
const handler = InfoHandler(healthCheck);

const response = await handler();

expect(response.kind).toBe("IResponseErrorInternal");
});

it("should return a success if the application is healthy", async () => {
const healthCheck: HealthCheck = taskEither.of(true);
const handler = InfoHandler(healthCheck);

const response = await handler();

expect(response.kind).toBe("IResponseSuccessJson");
});
});
28 changes: 20 additions & 8 deletions Info/handler.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import * as express from "express";
import { wrapRequestHandler } from "io-functions-commons/dist/src/utils/request_middleware";
import {
IResponseErrorInternal,
IResponseSuccessJson,
ResponseErrorInternal,
ResponseSuccessJson
} from "italia-ts-commons/lib/responses";
import * as packageJson from "../package.json";
import { checkApplicationHealth, HealthCheck } from "../utils/healthcheck";

interface IInfo {
name: string;
version: string;
}

type InfoHandler = () => Promise<IResponseSuccessJson<IInfo>>;
type InfoHandler = () => Promise<
IResponseSuccessJson<IInfo> | IResponseErrorInternal
>;

export function InfoHandler(): InfoHandler {
return async () => {
return ResponseSuccessJson({
version: packageJson.version
});
};
export function InfoHandler(healthCheck: HealthCheck): InfoHandler {
return () =>
healthCheck
.fold<IResponseSuccessJson<IInfo> | IResponseErrorInternal>(
problems => ResponseErrorInternal(problems.join("\n\n")),
_ =>
ResponseSuccessJson({
name: packageJson.name,
version: packageJson.version
})
)
.run();
}

export function Info(): express.RequestHandler {
const handler = InfoHandler();
const handler = InfoHandler(checkApplicationHealth());

return wrapRequestHandler(handler);
}
8 changes: 8 additions & 0 deletions openapi/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,14 @@ definitions:
type: string
version:
type: integer
ServerInfo:
type: object
title: Server information
properties:
version:
type: string
required:
- version
responses: {}
parameters:
SandboxFiscalCode:
Expand Down
150 changes: 150 additions & 0 deletions utils/healthcheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CosmosClient } from "@azure/cosmos";
import {
common as azurestorageCommon,
createBlobService,
createFileService,
createQueueService,
createTableService
} from "azure-storage";
import { sequenceT } from "fp-ts/lib/Apply";
import { array } from "fp-ts/lib/Array";
import { toError } from "fp-ts/lib/Either";
import {
fromEither,
taskEither,
TaskEither,
tryCatch
} from "fp-ts/lib/TaskEither";
import { readableReport } from "italia-ts-commons/lib/reporters";
import fetch from "node-fetch";
import { getConfig, IConfig } from "./config";

type ProblemSource = "AzureCosmosDB" | "AzureStorage" | "Config" | "Url";
export type HealthProblem<S extends ProblemSource> = string & { __source: S };
export type HealthCheck<
S extends ProblemSource = ProblemSource,
T = true
> = TaskEither<ReadonlyArray<HealthProblem<S>>, T>;

// format and cast a problem message with its source
const formatProblem = <S extends ProblemSource>(
source: S,
message: string
): HealthProblem<S> => `${source}|${message}` as HealthProblem<S>;

// utility to format an unknown error to an arry of HealthProblem
const toHealthProblems = <S extends ProblemSource>(source: S) => (
e: unknown
): ReadonlyArray<HealthProblem<S>> => [
formatProblem(source, toError(e).message)
];

/**
* Check application's configuration is correct
*
* @returns either true or an array of error messages
*/
export const checkConfigHealth = (): HealthCheck<"Config", IConfig> =>
fromEither(getConfig()).mapLeft(errors =>
errors.map(e =>
// give each problem its own line
formatProblem("Config", readableReport([e]))
)
);

/**
* Check the application can connect to an Azure CosmosDb instances
*
* @param dbUri uri of the database
* @param dbUri connection string for the storage
*
* @returns either true or an array of error messages
*/
export const checkAzureCosmosDbHealth = (
dbUri: string,
dbKey?: string
): HealthCheck<"AzureCosmosDB", true> =>
tryCatch(() => {
const client = new CosmosClient({
endpoint: dbUri,
key: dbKey
});
return client.getDatabaseAccount();
}, toHealthProblems("AzureCosmosDB")).map(_ => true);

/**
* Check the application can connect to an Azure Storage
*
* @param connStr connection string for the storage
*
* @returns either true or an array of error messages
*/
export const checkAzureStorageHealth = (
connStr: string
): HealthCheck<"AzureStorage"> =>
array
.sequence(taskEither)(
// try to instantiate a client for each product of azure storage
[
createBlobService,
createFileService,
createQueueService,
createTableService
]
// for each, create a task that wraps getServiceProperties
.map(createService =>
tryCatch(
() =>
new Promise<
azurestorageCommon.models.ServicePropertiesResult.ServiceProperties
>((resolve, reject) =>
createService(connStr).getServiceProperties((err, result) => {
err
? reject(err.message.replace(/\n/gim, " ")) // avoid newlines
: resolve(result);
})
),
toHealthProblems("AzureStorage")
)
)
)
.map(_ => true);

/**
* Check a url is reachable
*
* @param url url to connect with
*
* @returns either true or an array of error messages
*/
export const checkUrlHealth = (url: string): HealthCheck<"Url", true> =>
tryCatch(() => fetch(url, { method: "HEAD" }), toHealthProblems("Url")).map(
_ => true
);

/**
* Execute all the health checks for the application
*
* @returns either true or an array of error messages
*/
export const checkApplicationHealth = (): HealthCheck<ProblemSource, true> =>
taskEither
.of<ReadonlyArray<HealthProblem<ProblemSource>>, void>(void 0)
.chain(_ => checkConfigHealth())
.chain(config =>
// TODO: once we upgrade to fp-ts >= 1.19 we can use Validation to collect all errors, not just the first to happen
sequenceT(taskEither)<
ReadonlyArray<HealthProblem<ProblemSource>>,
// tslint:disable readonly-array beacuse the following is actually mutable
Array<TaskEither<ReadonlyArray<HealthProblem<ProblemSource>>, true>>
>(
checkAzureCosmosDbHealth(config.COSMOSDB_URI, config.COSMOSDB_KEY),
checkAzureStorageHealth(config.StorageConnection),
checkAzureStorageHealth(config.UserDataBackupStorageConnection),
checkAzureStorageHealth(config.UserDataArchiveStorageConnection),
checkUrlHealth(config.PUBLIC_API_URL),
checkUrlHealth(config.SESSION_API_URL),
checkUrlHealth(config.LOGOS_URL)
)
)
.map(_ => true);

0 comments on commit 57ed674

Please sign in to comment.