All checks were successful
Build and Deploy QuickCart / build-and-deploy (push) Successful in 51s
110 lines
8.2 KiB
JSON
110 lines
8.2 KiB
JSON
{
|
|
"title": "[WORKSHOP] Predictive PVC USage",
|
|
"description": "",
|
|
"schemaVersion": 4,
|
|
"trigger": {},
|
|
"result": null,
|
|
"type": "STANDARD",
|
|
"input": {},
|
|
"hourlyExecutionLimit": 1000,
|
|
"guide": null,
|
|
"tasks": {
|
|
"send_email_1": {
|
|
"name": "send_email_1",
|
|
"input": {
|
|
"cc": [],
|
|
"to": [
|
|
"mark.bley@dynatrace.com"
|
|
],
|
|
"bcc": [],
|
|
"content": "PVC Is going to be full in the next hours !",
|
|
"subject": "PVC Is going to be full in the next hours !"
|
|
},
|
|
"action": "dynatrace.email:send-email",
|
|
"position": {
|
|
"x": 1,
|
|
"y": 3
|
|
},
|
|
"conditions": {
|
|
"custom": "{{result(\"simple_prediction\")}}",
|
|
"states": {
|
|
"simple_prediction": "SUCCESS"
|
|
}
|
|
},
|
|
"description": "Send email",
|
|
"predecessors": [
|
|
"simple_prediction"
|
|
]
|
|
},
|
|
"run_javascript_1": {
|
|
"name": "run_javascript_1",
|
|
"input": {
|
|
"script": "import { execution } from '@dynatrace-sdk/automation-utils';\n\nconst TASK_ID = 'predict_disk_capacity';\n\n// 2 GiB in bytes\nconst THRESHOLD_BYTES = 2n * 1024n * 1024n * 1024n; // 2147483648\n\n// Choose which forecast series to evaluate: \"upper\" (recommended), \"point\", or \"lower\"\nconst SERIES_TO_CHECK = 'upper';\n\nfunction toGiB(bytesNumber) {\n return bytesNumber / (1024 ** 3);\n}\n\nfunction parseIntervalNs(interval) {\n // interval comes as a string like \"180000000000\" (ns)\n try {\n return BigInt(interval);\n } catch {\n return null;\n }\n}\n\nfunction addMs(isoStart, ms) {\n return new Date(new Date(isoStart).getTime() + ms).toISOString();\n}\n\nfunction scanSeries(values, thresholdBigInt) {\n let max = -Infinity;\n let firstIdx = -1;\n let firstVal = null;\n\n for (let i = 0; i < (values?.length ?? 0); i++) {\n const v = values[i];\n if (typeof v === 'number' && Number.isFinite(v)) {\n if (v > max) max = v;\n if (firstIdx < 0 && BigInt(Math.trunc(v)) >= thresholdBigInt) {\n firstIdx = i;\n firstVal = v;\n }\n }\n }\n\n return { max, firstIdx, firstVal };\n}\n\nfunction pickForecastRecord(item) {\n // Your payload has: item.timeSeriesDataWithPredictions.records[0]\n // Use first record; adjust if you ever have multiple records per item.\n return item?.timeSeriesDataWithPredictions?.records?.[0] ?? null;\n}\n\nfunction extractIdentity(item, rec) {\n // Prefer the prediction record fields; fallback to analyzedTimeSeriesQuery fields.\n const analyzedRec = item?.analyzedTimeSeriesQuery?.expression?.records?.[0] ?? {};\n\n return {\n cluster:\n rec?.['k8s.cluster.name'] ??\n analyzedRec?.['k8s.cluster.name'] ??\n 'unknown-cluster',\n pvc:\n rec?.['k8s.persistent_volume_claim.name'] ??\n analyzedRec?.['k8s.persistent_volume_claim.name'] ??\n 'unknown-pvc',\n };\n}\n\nexport default async function ({ execution_id }) {\n const exe = await execution(execution_id);\n\n const predResult = await exe.result(TASK_ID);\n // In your snippet you did: const result = predResult['result'];\n // Here we handle both shapes gracefully:\n const payload = predResult?.result ?? predResult;\n const outputs = payload?.output ?? [];\n\n const breaches = [];\n\n for (const item of outputs) {\n const rec = pickForecastRecord(item);\n if (!rec) continue;\n\n const { cluster, pvc } = extractIdentity(item, rec);\n\n const series =\n (SERIES_TO_CHECK === 'upper' && rec?.['dt.davis.forecast:upper']) ||\n (SERIES_TO_CHECK === 'point' && rec?.['dt.davis.forecast:point']) ||\n (SERIES_TO_CHECK === 'lower' && rec?.['dt.davis.forecast:lower']) ||\n // fallback preference order\n rec?.['dt.davis.forecast:upper'] ||\n rec?.['dt.davis.forecast:point'] ||\n rec?.['dt.davis.forecast:lower'] ||\n [];\n\n const { max, firstIdx, firstVal } = scanSeries(series, THRESHOLD_BYTES);\n\n const start = rec?.timeframe?.start; // e.g. \"2026-03-04T00:33Z\"\n const intervalNs = parseIntervalNs(rec?.interval);\n const intervalMs = intervalNs ? Number(intervalNs / 1_000_000n) : null;\n\n const firstCrossingTime =\n firstIdx >= 0 && start && intervalMs != null\n ? addMs(start, firstIdx * intervalMs)\n : null;\n\n if (firstIdx >= 0) {\n breaches.push({\n cluster,\n pvc,\n analysisStatus: item?.analysisStatus,\n forecastQualityAssessment: item?.forecastQualityAssessment,\n checkedSeries:\n series === rec?.['dt.davis.forecast:upper']\n ? 'dt.davis.forecast:upper'\n : series === rec?.['dt.davis.forecast:point']\n ? 'dt.davis.forecast:point'\n : series === rec?.['dt.davis.forecast:lower']\n ? 'dt.davis.forecast:lower'\n : 'fallback',\n thresholdBytes: Number(THRESHOLD_BYTES),\n thresholdGiB: 2,\n firstCrossing: {\n index: firstIdx,\n time: firstCrossingTime,\n valueBytes: firstVal,\n valueGiB: firstVal != null ? toGiB(firstVal) : null,\n },\n maxForecast: {\n valueBytes: max,\n valueGiB: Number.isFinite(max) ? toGiB(max) : null,\n },\n });\n }\n }\n\n const summary = {\n thresholdBytes: Number(THRESHOLD_BYTES),\n thresholdGiB: 2,\n checkedSeriesPreference: SERIES_TO_CHECK,\n totalSeries: outputs.length,\n breachCount: breaches.length,\n okCount: outputs.length - breaches.length,\n breaches,\n };\n\n console.log(JSON.stringify(summary, null, 2));\n\n // OPTIONAL: Fail the workflow if any series breaches the threshold\n // if (breaches.length > 0) {\n // throw new Error(`Forecast exceeds 2GiB for ${breaches.length} series`);\n // }\n\n return summary;\n}"
|
|
},
|
|
"action": "dynatrace.automations:run-javascript",
|
|
"active": false,
|
|
"position": {
|
|
"x": -1,
|
|
"y": 2
|
|
},
|
|
"conditions": {
|
|
"states": {
|
|
"predict_disk_capacity": "OK"
|
|
}
|
|
},
|
|
"description": "Run custom JavaScript code.",
|
|
"predecessors": [
|
|
"predict_disk_capacity"
|
|
]
|
|
},
|
|
"simple_prediction": {
|
|
"name": "simple_prediction",
|
|
"input": {
|
|
"script": "import { execution } from '@dynatrace-sdk/automation-utils';\n\nconst TASK_ID = 'predict_disk_capacity';\nconst THRESHOLD = 2 * 1024 * 1024 * 1024; // 2GiB in bytes\n\nexport default async function ({ execution_id }) {\n const exe = await execution(execution_id);\n const predResult = await exe.result(TASK_ID);\n const result = predResult.result;\n\n for (const item of result.output) {\n const rec = item.timeSeriesDataWithPredictions.records[0];\n const series = rec['dt.davis.forecast:upper'] || rec['dt.davis.forecast:point'];\n\n for (const v of series) {\n if (v != null && v >= THRESHOLD) return true;\n }\n }\n\n return false;\n}"
|
|
},
|
|
"action": "dynatrace.automations:run-javascript",
|
|
"position": {
|
|
"x": 1,
|
|
"y": 2
|
|
},
|
|
"conditions": {
|
|
"states": {
|
|
"predict_disk_capacity": "OK"
|
|
}
|
|
},
|
|
"description": "Run custom JavaScript code.",
|
|
"predecessors": [
|
|
"predict_disk_capacity"
|
|
]
|
|
},
|
|
"predict_disk_capacity": {
|
|
"name": "predict_disk_capacity",
|
|
"input": {
|
|
"body": {
|
|
"nPaths": 200,
|
|
"forecastOffset": 0,
|
|
"timeSeriesData": "timeseries { avg(dt.kubernetes.persistentvolumeclaim.used), value.A = avg(dt.kubernetes.persistentvolumeclaim.used, scalar: true) }, by: { k8s.cluster.name, k8s.persistent_volume_claim.name }",
|
|
"forecastHorizon": 200,
|
|
"generalParameters": {
|
|
"timeframe": {
|
|
"endTime": "now",
|
|
"startTime": "now()-6h"
|
|
},
|
|
"logVerbosity": "WARNING",
|
|
"resolveDimensionalQueryData": false
|
|
},
|
|
"coverageProbability": 0.9,
|
|
"applyZeroLowerBoundHeuristic": true
|
|
},
|
|
"analyzerName": "dt.statistics.GenericForecastAnalyzer"
|
|
},
|
|
"action": "dynatrace.davis.workflow.actions:davis-analyze",
|
|
"position": {
|
|
"x": 0,
|
|
"y": 1
|
|
},
|
|
"description": "Execute a customizable AI/ML task using data analyzers",
|
|
"predecessors": []
|
|
}
|
|
}
|
|
} |