UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

254 lines • 12 kB
import { SfError } from "@salesforce/core"; import c from "chalk"; import { NotifProviderRoot } from "./notifProviderRoot.js"; import { getCurrentGitBranch, getGitRepoName, uxLog } from "../utils/index.js"; import { UtilsNotifs } from "./index.js"; import { CONSTANTS, getEnvVar } from "../../config/index.js"; import { getSeverityIcon, removeMarkdown } from "../utils/notifUtils.js"; import { GitProvider } from "../gitProvider/index.js"; import axios from "axios"; const MAX_LOKI_LOG_LENGTH = Number(process.env.MAX_LOKI_LOG_LENGTH || 200000); const TRUNCATE_LOKI_ELEMENTS_LENGTH = Number(process.env.TRUNCATE_LOKI_ELEMENTS_LENGTH || 500); export class ApiProvider extends NotifProviderRoot { apiUrl; payload; payloadFormatted; metricsApiUrl; metricsPayload; getLabel() { return "sfdx-hardis Api connector"; } // Always send notifications to API endpoint // eslint-disable-next-line @typescript-eslint/no-unused-vars isApplicableForNotif(notifMessage) { return true; } isUserNotifProvider() { return false; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async postNotification(notifMessage) { const apiPromises = []; // Use Promises to optimize performances with api calls this.apiUrl = getEnvVar("NOTIF_API_URL"); if (this.apiUrl == null) { throw new SfError("[ApiProvider] You need to define a variable NOTIF_API_URL to use sfdx-hardis Api notifications"); } // Build initial payload data from notifMessage await this.buildPayload(notifMessage); // Format payload according to API endpoint: for example, Grafana loki await this.formatPayload(); // Send notif apiPromises.push(this.sendToApi()); // Handle Metrics API if provided this.metricsApiUrl = getEnvVar("NOTIF_API_METRICS_URL"); if (this.metricsApiUrl !== null) { this.buildMetricsPayload(); if (this.metricsPayload.length > 0) { apiPromises.push(this.sendToMetricsApi()); } } await Promise.allSettled(apiPromises); return; } // Build message async buildPayload(notifMessage) { const firstLineMarkdown = UtilsNotifs.prefixWithSeverityEmoji(UtilsNotifs.slackToTeamsMarkdown(notifMessage.text.split("\n")[0]), notifMessage.severity); const logTitle = removeMarkdown(firstLineMarkdown); let logBodyText = UtilsNotifs.prefixWithSeverityEmoji(UtilsNotifs.slackToTeamsMarkdown(notifMessage.text), notifMessage.severity); // Add text details if (notifMessage?.attachments?.length && notifMessage?.attachments?.length > 0) { let text = "\n\n"; for (const attachment of notifMessage.attachments) { if (attachment.text) { text += attachment.text + "\n\n"; } } if (text !== "") { logBodyText += UtilsNotifs.slackToTeamsMarkdown(text) + "\n\n"; } } // Add action blocks if (notifMessage.buttons?.length && notifMessage.buttons?.length > 0) { logBodyText += "Links:\n\n"; for (const button of notifMessage.buttons) { // Url button if (button.url) { logBodyText += " - " + button.text + ": " + button.url + "\n\n"; } } logBodyText += "\n\n"; } // Add sfdx-hardis ref logBodyText += `Powered by sfdx-hardis: ${CONSTANTS.DOC_URL_ROOT}`; logBodyText = removeMarkdown(logBodyText); // Build payload const repoName = (await getGitRepoName() || "").replace(".git", ""); const currentGitBranch = await getCurrentGitBranch(); const conn = globalThis.jsForceConn; const orgIdentifier = (conn.instanceUrl) ? conn.instanceUrl.replace("https://", "").replace(".my.salesforce.com", "").replace(/\./gm, "__") : currentGitBranch || "ERROR apiProvider"; const notifKey = orgIdentifier + "!!" + notifMessage.type; this.payload = { source: "sfdx-hardis", type: notifMessage.type, orgIdentifier: orgIdentifier, gitIdentifier: `${repoName}/${currentGitBranch}`, severity: notifMessage.severity, data: Object.assign(notifMessage.data, { _dateTime: new Date().toISOString(), _severityIcon: getSeverityIcon(notifMessage.severity), _title: logTitle, _logBodyText: logBodyText, _logElements: notifMessage.logElements, _metrics: notifMessage.metrics, _metricsKeys: Object.keys(notifMessage.metrics), _notifKey: notifKey, }), }; // Add job url if available const jobUrl = await GitProvider.getJobUrl(); if (jobUrl) { this.payload.data._jobUrl = jobUrl; } } async formatPayload() { if ((this.apiUrl || "").includes("loki/api/v1/push")) { await this.formatPayloadLoki(); return; } this.payloadFormatted = this.payload; } async formatPayloadLoki() { const currentTimeNanoseconds = Date.now() * 1000 * 1000; const payloadCopy = Object.assign({}, this.payload); delete payloadCopy.data; let payloadDataJson = JSON.stringify(this.payload.data); const bodyBytesLen = new TextEncoder().encode(payloadDataJson).length; // Truncate log elements if log entry is too big if (bodyBytesLen > MAX_LOKI_LOG_LENGTH) { const newPayloadData = Object.assign({}, this.payload.data); const logElements = newPayloadData._logElements; if (logElements.length > TRUNCATE_LOKI_ELEMENTS_LENGTH) { const truncatedLogElements = logElements.slice(0, TRUNCATE_LOKI_ELEMENTS_LENGTH); newPayloadData._logElements = truncatedLogElements; newPayloadData._logElementsTruncated = true; payloadDataJson = JSON.stringify(newPayloadData); uxLog(this, c.grey(`[ApiProvider] Truncated _logElements from ${logElements.length} to ${truncatedLogElements.length} to avoid Loki entry max size reached (initial size: ${bodyBytesLen} bytes)`)); } else { newPayloadData._logBodyText = (newPayloadData._logBodyText || "").slice(0, 100) + "\n ... (truncated)"; payloadDataJson = JSON.stringify(newPayloadData); uxLog(this, c.grey(`[ApiProvider] Truncated _logBodyText to 100 to avoid Loki entry max size reached (initial size: ${bodyBytesLen} bytes)`)); } } this.payloadFormatted = { streams: [ { stream: payloadCopy, values: [[`${currentTimeNanoseconds}`, payloadDataJson]], }, ], }; } // Call remote API async sendToApi() { const axiosConfig = { responseType: "json", }; // Basic Auth if (getEnvVar("NOTIF_API_BASIC_AUTH_USERNAME") != null) { axiosConfig.auth = { username: getEnvVar("NOTIF_API_BASIC_AUTH_USERNAME") || "", password: getEnvVar("NOTIF_API_BASIC_AUTH_PASSWORD") || "", }; } // Bearer token else if (getEnvVar("NOTIF_API_BEARER_TOKEN") != null) { axiosConfig.headers = { Authorization: `Bearer ${getEnvVar("NOTIF_API_BEARER_TOKEN")}` }; } // POST message try { const axiosResponse = await axios.post(this.apiUrl || "", this.payloadFormatted, axiosConfig); const httpStatus = axiosResponse.status; if (httpStatus > 200 && httpStatus < 300) { uxLog(this, c.cyan(`[ApiProvider] Posted message to API ${this.apiUrl} (${httpStatus})`)); if (getEnvVar("NOTIF_API_DEBUG") === "true") { uxLog(this, c.cyan(JSON.stringify(this.payloadFormatted, null, 2))); } } } catch (e) { uxLog(this, c.yellow(`[ApiProvider] Error while sending message to API ${this.apiUrl}: ${e.message}`)); uxLog(this, c.grey("Request body: \n" + JSON.stringify(this.payloadFormatted))); uxLog(this, c.grey("Response body: \n" + JSON.stringify(e?.response?.data || {}))); } } // Build something like MetricName,source=sfdx-hardis,orgIdentifier=hardis-group metric=12.7,min=0,max=70,percent=0.63 buildMetricsPayload() { // Build tag field const metricTags = `source=${this.payload.source},` + `type=${this.payload.type},` + `orgIdentifier=${this.payload.orgIdentifier},` + `gitIdentifier=${this.payload.gitIdentifier}`; // Add extra fields and value const metricsPayloadLines = []; for (const metricId of Object.keys(this.payload.data._metrics)) { const metricData = this.payload.data._metrics[metricId]; let metricPayloadLine = metricId + "," + metricTags + " "; if (typeof metricData === "number") { metricPayloadLine += "metric=" + metricData.toFixed(2); metricsPayloadLines.push(metricPayloadLine); } else if (typeof metricData === "object") { const metricFields = []; if (metricData.min) { metricFields.push("min=" + metricData.min.toFixed(2)); } if (metricData.max) { metricFields.push("max=" + metricData.max.toFixed(2)); } if (metricData.percent) { metricFields.push("percent=" + metricData.percent.toFixed(2)); } metricFields.push("metric=" + metricData.value.toFixed(2)); metricPayloadLine += metricFields.join(","); metricsPayloadLines.push(metricPayloadLine); } } // Result as single string with carriage returns this.metricsPayload = metricsPayloadLines.join("\n"); } // Call remote API async sendToMetricsApi() { const axiosConfig = { responseType: "json", }; // Basic Auth if (getEnvVar("NOTIF_API_METRICS_BASIC_AUTH_USERNAME") != null) { axiosConfig.auth = { username: getEnvVar("NOTIF_API_METRICS_BASIC_AUTH_USERNAME") || "", password: getEnvVar("NOTIF_API_METRICS_BASIC_AUTH_PASSWORD") || "", }; } // Bearer token else if (getEnvVar("NOTIF_API_METRICS_BEARER_TOKEN") != null) { axiosConfig.headers = { Authorization: `Bearer ${getEnvVar("NOTIF_API_METRICS_BEARER_TOKEN")}` }; } // POST message try { const axiosResponse = await axios.post(this.metricsApiUrl || "", this.metricsPayload, axiosConfig); const httpStatus = axiosResponse.status; if (httpStatus > 200 && httpStatus < 300) { uxLog(this, c.cyan(`[ApiMetricProvider] Posted message to API ${this.metricsApiUrl} (${httpStatus})`)); if (getEnvVar("NOTIF_API_DEBUG") === "true") { uxLog(this, c.cyan(JSON.stringify(this.metricsPayload, null, 2))); } } } catch (e) { uxLog(this, c.yellow(`[ApiMetricProvider] Error while sending message to API ${this.metricsApiUrl}: ${e.message}`)); uxLog(this, c.grey("Request body: \n" + JSON.stringify(this.metricsPayload))); uxLog(this, c.grey("Response body: \n" + JSON.stringify(e?.response?.data || {}))); } } } //# sourceMappingURL=apiProvider.js.map