Skip to content

Commit

Permalink
[#IOPID-1876] Enable Application Insights (#52)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniele Manni <[email protected]>
  • Loading branch information
gquadrati and BurnedMarshal authored Jun 6, 2024
1 parent e2bf550 commit ef4f6e3
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-baboons-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pagopa/io-session-manager": minor
---

Enable AppInsights
9 changes: 9 additions & 0 deletions apps/session-manager/env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
################
# AppInsinghts #
################
APPINSIGHTS_CONNECTION_STRING=""
APPINSIGHTS_DISABLED=false
APPINSIGHTS_SAMPLING_PERCENTAGE=100
# cloud_RoleName value; if undefined, WEBSITE_SITE_NAME will be used
APPINSIGHTS_CLOUD_ROLE_NAME="session-manager-dev"

API_URL=""
API_KEY=""
API_BASE_PATH=""
Expand Down
6 changes: 3 additions & 3 deletions apps/session-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
"@azure/data-tables": "^13.2.2",
"@azure/storage-queue": "^12.16.0",
"@pagopa/io-functions-app-sdk": "x",
"@pagopa/io-functions-commons": "^29.0.4",
"@pagopa/io-functions-commons": "^29.1.0",
"@pagopa/io-spid-commons": "^13.5.0",
"@pagopa/ts-commons": "^13.1.0",
"applicationinsights": "^1.8.10",
"@pagopa/ts-commons": "^13.1.1",
"applicationinsights": "^2.9.5",
"body-parser": "^1.20.2",
"date-fns": "^1.30.1",
"express": "^4.19.2",
Expand Down
15 changes: 10 additions & 5 deletions apps/session-manager/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as appInsights from "applicationinsights";
import passport from "passport";
import express from "express";
import { Express } from "express";
Expand Down Expand Up @@ -77,17 +78,18 @@ import { localStrategy } from "./auth/local-strategy";
import { FF_LOLLIPOP_ENABLED } from "./config/lollipop";

export interface IAppFactoryParameters {
// TODO: Add the right AppInsigns type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly appInsightsClient?: any;
readonly appInsightsClient?: appInsights.TelemetryClient;
}

export const newApp: (
params: IAppFactoryParameters,
// eslint-disable-next-line max-lines-per-function
) => Promise<Express> = async ({ appInsightsClient }) => {
// Create the Session Storage service
const REDIS_CLIENT_SELECTOR = await RedisRepo.RedisClientSelector(!isDevEnv)(
const REDIS_CLIENT_SELECTOR = await RedisRepo.RedisClientSelector(
!isDevEnv,
appInsightsClient,
)(
getRequiredENVVar("REDIS_URL"),
process.env.REDIS_PASSWORD,
process.env.REDIS_PORT,
Expand Down Expand Up @@ -176,6 +178,7 @@ export const newApp: (
testLoginPassword,
FF_LOLLIPOP_ENABLED,
APIClients.fnLollipopAPIClient,
appInsightsClient,
),
);

Expand Down Expand Up @@ -359,7 +362,9 @@ export const newApp: (
...withSpidApp,
spidConfigTime: TIMER.getElapsedMilliseconds(),
})),
TE.chain(setupMetadataRefresherAndGS(REDIS_CLIENT_SELECTOR)),
TE.chain(
setupMetadataRefresherAndGS(REDIS_CLIENT_SELECTOR, appInsightsClient),
),
TE.chainFirst(checkIdpConfiguration),
TE.chainFirstTaskK(applyErrorMiddleware),
)();
Expand Down
9 changes: 8 additions & 1 deletion apps/session-manager/src/auth/local-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as appInsights from "applicationinsights";
import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import * as passport from "passport";
import { Strategy } from "passport-local";
Expand Down Expand Up @@ -29,6 +30,7 @@ export const localStrategy = (
validPassword: string,
isLollipopEnabled: boolean,
lollipopApiClient: LollipopApiClient,
appInsightsTelemetryClient?: appInsights.TelemetryClient,
): passport.Strategy =>
new Strategy({ passReqToCallback: true }, (req, username, password, done) => {
pipe(
Expand All @@ -45,7 +47,12 @@ export const localStrategy = (
),
TE.chain(() =>
TE.tryCatch(
() => lollipopLoginHandler(isLollipopEnabled, lollipopApiClient)(req),
() =>
lollipopLoginHandler(
isLollipopEnabled,
lollipopApiClient,
appInsightsTelemetryClient,
)(req),
E.toError,
),
),
Expand Down
24 changes: 24 additions & 0 deletions apps/session-manager/src/config/appinsights.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/lib/function";
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";

export const APPINSIGHTS_CONNECTION_STRING = O.fromNullable(
process.env.APPINSIGHTS_CONNECTION_STRING,
);

export const APPINSIGHTS_CLOUD_ROLE_NAME = pipe(
process.env.APPINSIGHTS_CLOUD_ROLE_NAME,
NonEmptyString.decode,
E.getOrElseW(() => undefined),
);

export const APPINSIGHTS_DISABLED = process.env.APPINSIGHTS_DISABLED === "true";

// Application insights sampling percentage
const DEFAULT_APPINSIGHTS_SAMPLING_PERCENTAGE = 5;
export const APPINSIGHTS_SAMPLING_PERCENTAGE = process.env
.APPINSIGHTS_SAMPLING_PERCENTAGE
? parseInt(process.env.APPINSIGHTS_SAMPLING_PERCENTAGE, 10)
: DEFAULT_APPINSIGHTS_SAMPLING_PERCENTAGE;
2 changes: 2 additions & 0 deletions apps/session-manager/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as SpidConfig from "./spid";
import * as SpidLogConfig from "./spid-logs";
import * as ZendeskConfig from "./zendesk";
import * as PagoPAConfig from "./pagopa";
import * as AppInsightsConfig from "./appinsights";

export const ENV = getNodeEnvironmentFromProcessEnv(process.env);

Expand Down Expand Up @@ -46,4 +47,5 @@ export {
SpidLogConfig,
ZendeskConfig,
PagoPAConfig,
AppInsightsConfig,
};
68 changes: 64 additions & 4 deletions apps/session-manager/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,76 @@
import { Server } from "http";
import * as appInsights from "applicationinsights";

import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/lib/function";

import { newApp } from "./app";
import { AppInsightsConfig } from "./config";
import {
StartupEventName,
initAppInsights,
trackStartupTime,
} from "./utils/appinsights";
import { log } from "./utils/logger";
import { getCurrentBackendVersion } from "./utils/package";
import { TimeTracer } from "./utils/timer";

const timer = TimeTracer();

// eslint-disable-next-line turbo/no-undeclared-env-vars
const port = process.env.WEBSITES_PORT ?? 3000;

newApp({})
const maybeAppInsightsClient = pipe(
AppInsightsConfig.APPINSIGHTS_CONNECTION_STRING,
O.map((key) =>
initAppInsights(key, {
cloudRole: AppInsightsConfig.APPINSIGHTS_CLOUD_ROLE_NAME,
applicationVersion: getCurrentBackendVersion(),
disableAppInsights: AppInsightsConfig.APPINSIGHTS_DISABLED,
samplingPercentage: AppInsightsConfig.APPINSIGHTS_SAMPLING_PERCENTAGE,
}),
),
O.toUndefined,
);

newApp({ appInsightsClient: maybeAppInsightsClient })
.then((app) => {
app.listen(port, () => {
log.info(`Example app listening on port ${port}`);
});
const server = app
.listen(port, () => {
const startupTimeMs = timer.getElapsedMilliseconds();

log.info("Listening on port %d", port);
log.info(`Startup time: %sms`, startupTimeMs.toString());
pipe(
maybeAppInsightsClient,
O.fromNullable,
O.map((_) =>
trackStartupTime(_, StartupEventName.SERVER, startupTimeMs),
),
);
})
.on("close", () => {
log.info("On close: emit 'server:stop' event");
const result = app.emit("server:stop");
log.info(
`On close: end emit 'server:stop' event. Listeners found: ${result}`,
);

maybeAppInsightsClient?.flush();
appInsights.dispose();
});

process.on("SIGTERM", shutDown(server, "SIGTERM"));
process.on("SIGINT", shutDown(server, "SIGINT"));
})
.catch((err) => {
log.error("Error loading app: %s", err);
process.exit(1);
});

const shutDown = (server: Server, signal: string) => () => {
log.info(`${signal} signal received: closing HTTP server`);
server.close(() => {
log.info("HTTP server closed");
});
};
75 changes: 75 additions & 0 deletions apps/session-manager/src/utils/appinsights.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,66 @@
import * as appInsights from "applicationinsights";
import {
ApplicationInsightsConfig,
initAppInsights as startAppInsights,
} from "@pagopa/ts-commons/lib/appinsights";
import { hashFiscalCode } from "@pagopa/ts-commons/lib/hash";
import { User } from "../types/user";

const SESSION_TRACKING_ID_KEY = "session_tracking_id";
const USER_TRACKING_ID_KEY = "user_tracking_id";

/**
* App Insights is initialized to collect the following informations:
* - Incoming API calls
* - Server performance information (CPU, RAM)
* - Unandled Runtime Exceptions
* - Outcoming API Calls (dependencies)
* - Realtime API metrics
*/
export function initAppInsights(
connectionString: string,
config: ApplicationInsightsConfig = {},
): ReturnType<typeof startAppInsights> {
const defaultClient = startAppInsights(connectionString, config);
defaultClient.addTelemetryProcessor(sessionIdPreprocessor);

return defaultClient;
}

export function sessionIdPreprocessor(
envelope: appInsights.Contracts.Envelope,
context?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly [name: string]: any;
},
): boolean {
if (context !== undefined) {
try {
const userTrackingId =
context.correlationContext.customProperties.getProperty(
USER_TRACKING_ID_KEY,
);
if (userTrackingId !== undefined) {
// eslint-disable-next-line functional/immutable-data
envelope.tags[appInsights.defaultClient.context.keys.userId] =
userTrackingId;
}
const sessionTrackingId =
context.correlationContext.customProperties.getProperty(
SESSION_TRACKING_ID_KEY,
);
if (sessionTrackingId !== undefined) {
// eslint-disable-next-line functional/immutable-data
envelope.tags[appInsights.defaultClient.context.keys.sessionId] =
sessionTrackingId;
}
} catch (e) {
// ignore errors caused by missing properties
}
}
return true;
}

/**
* Attach the userid (CF) hash to the correlation context.
* Also, if the user objects provides the session_tracking_id property, attach
Expand Down Expand Up @@ -40,3 +96,22 @@ export function attachTrackingData(user: User): void {
);
}
}

export enum StartupEventName {
SERVER = "api-backend.httpserver.startup",
SPID = "api-backend.spid.config",
}

export const trackStartupTime = (
telemetryClient: appInsights.TelemetryClient,
type: StartupEventName,
timeMs: bigint,
): void => {
telemetryClient.trackEvent({
name: type,
properties: {
time: timeMs.toString(),
},
tagOverrides: { samplingEnabled: "false" },
});
};
35 changes: 21 additions & 14 deletions apps/session-manager/src/utils/express.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as appInsights from "applicationinsights";
import {
IResponse,
ResponseErrorInternal,
Expand All @@ -13,6 +14,7 @@ import * as R from "fp-ts/Record";
import { getSpidStrategyOption } from "@pagopa/io-spid-commons/dist/utils/middleware";
import { RedisClientMode, RedisClientSelectorType } from "../types/redis";
import { IDP_METADATA_REFRESH_INTERVAL_SECONDS } from "../config/spid";
import { StartupEventName, trackStartupTime } from "./appinsights";
import { log } from "./logger";

export type ExpressMiddleware = (
Expand Down Expand Up @@ -125,13 +127,20 @@ export const checkIdpConfiguration: (
*/
export const setupMetadataRefresherAndGS: (
redisClientSelector: RedisClientSelectorType,
appInsightsClient?: appInsights.TelemetryClient,
) => (data: {
app: express.Express;
spidConfigTime: bigint;
idpMetadataRefresher: () => T.Task<void>;
}) => TE.TaskEither<Error, express.Express> =
(REDIS_CLIENT_SELECTOR) => (data) => {
// TODO: Add AppInsights startup track event
(REDIS_CLIENT_SELECTOR, appInsightsClient) => (data) => {
if (appInsightsClient) {
trackStartupTime(
appInsightsClient,
StartupEventName.SPID,
data.spidConfigTime,
);
}
log.info(`Spid init time: %sms`, data.spidConfigTime.toString());
// Schedule automatic idpMetadataRefresher
const startIdpMetadataRefreshTimer = setInterval(
Expand All @@ -147,19 +156,17 @@ export const setupMetadataRefresherAndGS: (
clearInterval(startIdpMetadataRefreshTimer);
// Graceful redis connection shutdown.
for (const client of REDIS_CLIENT_SELECTOR.select(RedisClientMode.ALL)) {
log.info(`Graceful closing redis connection`);
pipe(
O.fromNullable(client.quit),
O.map((redisQuitFn) =>
redisQuitFn().catch((err) =>
log.error(
`An Error occurred closing the redis connection: [${
E.toError(err).message
}]`,
),
log.info(`Gracefully closing redis connection`);

client
.quit()
.catch((err) =>
log.error(
`An Error occurred closing the redis connection: [${
E.toError(err).message
}]`,
),
),
);
);
}
});
return TE.of(data.app);
Expand Down
Loading

0 comments on commit ef4f6e3

Please sign in to comment.