diff --git a/backend/src/lib/telemetry/metrics.ts b/backend/src/lib/telemetry/metrics.ts index df6aacaec..5f650ffc6 100644 --- a/backend/src/lib/telemetry/metrics.ts +++ b/backend/src/lib/telemetry/metrics.ts @@ -1,5 +1,8 @@ +import { requestContext } from "@fastify/request-context"; import opentelemetry from "@opentelemetry/api"; +import { getConfig } from "../config/env"; + const infisicalMeter = opentelemetry.metrics.getMeter("Infisical"); export enum AuthAttemptAuthMethod { @@ -32,3 +35,66 @@ export const authAttemptCounter = infisicalMeter.createCounter("infisical.auth.a description: "Authentication attempts (both successful and failed)", unit: "{attempt}" }); + +export const secretReadCounter = infisicalMeter.createCounter("infisical.secret.read.count", { + description: "Number of secret read operations", + unit: "{operation}" +}); + +export const recordSecretReadMetric = (params: { environment: string; secretPath: string; name?: string }) => { + const appCfg = getConfig(); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + const attributes: Record = { + "infisical.environment": params.environment, + "infisical.secret.path": params.secretPath, + ...(params.name ? { "infisical.secret.name": params.name } : {}) + }; + + const orgId = requestContext.get("orgId"); + if (orgId) { + attributes["infisical.organization.id"] = orgId; + } + + const orgName = requestContext.get("orgName"); + if (orgName) { + attributes["infisical.organization.name"] = orgName; + } + + const projectDetails = requestContext.get("projectDetails"); + if (projectDetails?.id) { + attributes["infisical.project.id"] = projectDetails.id; + } + if (projectDetails?.name) { + attributes["infisical.project.name"] = projectDetails.name; + } + + const userAuthInfo = requestContext.get("userAuthInfo"); + if (userAuthInfo?.userId) { + attributes["infisical.user.id"] = userAuthInfo.userId; + } + if (userAuthInfo?.email) { + attributes["infisical.user.email"] = userAuthInfo.email; + } + + const identityAuthInfo = requestContext.get("identityAuthInfo"); + if (identityAuthInfo?.identityId) { + attributes["infisical.identity.id"] = identityAuthInfo.identityId; + } + if (identityAuthInfo?.identityName) { + attributes["infisical.identity.name"] = identityAuthInfo.identityName; + } + + const userAgent = requestContext.get("userAgent"); + if (userAgent) { + attributes["user_agent.original"] = userAgent; + } + + const ip = requestContext.get("ip"); + if (ip) { + attributes["client.address"] = ip; + } + + secretReadCounter.add(1, attributes); + } +}; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 465babebe..559c86843 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -34,6 +34,7 @@ import { diff, groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { recordSecretReadMetric } from "@app/lib/telemetry/metrics"; import { ActorType } from "../auth/auth-type"; import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; @@ -1052,6 +1053,11 @@ export const secretV2BridgeServiceFactory = ({ }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); + recordSecretReadMetric({ + environment, + secretPath: path + }); + const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); const secretDalVersion = Number(cachedSecretDalVersion || 0); const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { @@ -1482,6 +1488,12 @@ export const secretV2BridgeServiceFactory = ({ secretTags: (secret?.tags || []).map((el) => el.slug) }); + recordSecretReadMetric({ + environment, + secretPath: path, + name: secretName + }); + // this will throw if the user doesn't have read value permission no matter what // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({