diff --git a/plugins/SampleData/v3/configValidation.json b/plugins/SampleData/v3/configValidation.json new file mode 100644 index 00000000..f3c224f0 --- /dev/null +++ b/plugins/SampleData/v3/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Load the sample company", + "dataStream": { "name": "objects" }, + "required": true, + "error": "Could not load the sample data model. Check SquaredUp can reach raw.githubusercontent.com.", + "success": "Sample data is ready." + } + ] +} diff --git a/plugins/SampleData/v3/correlationRules/relate-host-to-service.json b/plugins/SampleData/v3/correlationRules/relate-host-to-service.json new file mode 100644 index 00000000..59f73c6b --- /dev/null +++ b/plugins/SampleData/v3/correlationRules/relate-host-to-service.json @@ -0,0 +1,7 @@ +{ + "displayName": "Host runs Service", + "source": { "types": "Host" }, + "target": { "types": "Service" }, + "conditions": [{ "sourceProperty": "serviceName", "operator": "equals", "targetProperty": "name" }], + "labels": { "forward": "runs", "reverse": "runs on" } +} diff --git a/plugins/SampleData/v3/correlationRules/relate-pipeline-to-service.json b/plugins/SampleData/v3/correlationRules/relate-pipeline-to-service.json new file mode 100644 index 00000000..13964de3 --- /dev/null +++ b/plugins/SampleData/v3/correlationRules/relate-pipeline-to-service.json @@ -0,0 +1,7 @@ +{ + "displayName": "Pipeline deploys Service", + "source": { "types": "Pipeline" }, + "target": { "types": "Service" }, + "conditions": [{ "sourceProperty": "serviceName", "operator": "equals", "targetProperty": "name" }], + "labels": { "forward": "deploys", "reverse": "deployed by" } +} diff --git a/plugins/SampleData/v3/custom_types.json b/plugins/SampleData/v3/custom_types.json new file mode 100644 index 00000000..ab514bc3 --- /dev/null +++ b/plugins/SampleData/v3/custom_types.json @@ -0,0 +1,6 @@ +[ + { "name": "Service", "sourceType": "Service", "icon": "cubes", "singular": "Service", "plural": "Services" }, + { "name": "Host", "sourceType": "Host", "icon": "server", "singular": "Host", "plural": "Hosts" }, + { "name": "Pipeline", "sourceType": "Pipeline", "icon": "code-branch", "singular": "Pipeline", "plural": "Pipelines" }, + { "name": "Customer", "sourceType": "Customer", "icon": "building", "singular": "Customer", "plural": "Customers" } +] diff --git a/plugins/SampleData/v3/dataStreams/businessMetrics.json b/plugins/SampleData/v3/dataStreams/businessMetrics.json new file mode 100644 index 00000000..daa6ccd2 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/businessMetrics.json @@ -0,0 +1,88 @@ +{ + "name": "businessMetrics", + "displayName": "Business Metrics", + "description": "Visitors, signups, orders, order value and recurring revenue over time", + "tags": [ + "Business", + "Analytics" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "businessMetrics.js" + }, + "matches": "none", + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "visitors", + "displayName": "Visitors", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "signups", + "displayName": "Signups", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "orders", + "displayName": "Orders", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "orderValueUsd", + "displayName": "Order value", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "conversionRatePercent", + "displayName": "Conversion rate", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "mrrUsd", + "displayName": "MRR", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/dataStreams/cloudCost.json b/plugins/SampleData/v3/dataStreams/cloudCost.json new file mode 100644 index 00000000..c900d2a0 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/cloudCost.json @@ -0,0 +1,90 @@ +{ + "name": "cloudCost", + "displayName": "Cloud Cost", + "description": "Daily cloud spend and budget by service and cost category", + "tags": [ + "Cost", + "FinOps" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "cloudCost.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "metadata": [ + { + "name": "date", + "displayName": "Date", + "shape": [ + "date", + { + "format": "dd MMM yyyy" + } + ], + "role": "timestamp" + }, + { + "name": "serviceId", + "visible": false + }, + { + "name": "service", + "displayName": "Service", + "role": "label" + }, + { + "name": "team", + "displayName": "Team" + }, + { + "name": "provider", + "displayName": "Cloud provider" + }, + { + "name": "category", + "displayName": "Category" + }, + { + "name": "costUsd", + "displayName": "Cost", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "budgetUsd", + "displayName": "Budget", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "sourceId": "serviceId", + "sourceType": "Service", + "name": "service" + } + ], + "timeframes": [ + "last7days", + "last30days", + "thisMonth", + "lastMonth", + "thisQuarter", + "lastQuarter" + ] +} diff --git a/plugins/SampleData/v3/dataStreams/conversionFunnel.json b/plugins/SampleData/v3/dataStreams/conversionFunnel.json new file mode 100644 index 00000000..3975a852 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/conversionFunnel.json @@ -0,0 +1,57 @@ +{ + "name": "conversionFunnel", + "displayName": "Conversion Funnel", + "description": "Signup to paid-customer conversion stages for the selected timeframe", + "tags": [ + "Business", + "Analytics" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "conversionFunnel.js" + }, + "matches": "none", + "defaultShaping": { + "sort": { + "by": [ + [ + "order", + "asc" + ] + ] + } + }, + "metadata": [ + { + "name": "order", + "displayName": "Order", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "stage", + "displayName": "Stage", + "role": "label" + }, + { + "name": "count", + "displayName": "Count", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/dataStreams/customerSla.json b/plugins/SampleData/v3/dataStreams/customerSla.json new file mode 100644 index 00000000..96d15e8b --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/customerSla.json @@ -0,0 +1,164 @@ +{ + "name": "customerSla", + "displayName": "Customer SLAs", + "description": "Current SLA attainment, satisfaction and account details for each customer", + "tags": [ + "Customers", + "SLA" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "customerSla.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Customer" + ] + } + }, + "defaultShaping": { + "sort": { + "by": [ + [ + "customer", + "asc" + ] + ] + } + }, + "metadata": [ + { + "name": "customerId", + "visible": false + }, + { + "name": "customer", + "displayName": "Customer", + "role": "label" + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "plan", + "displayName": "Plan" + }, + { + "name": "city", + "displayName": "City" + }, + { + "name": "country", + "displayName": "Country" + }, + { + "name": "latitude", + "displayName": "Latitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "longitude", + "displayName": "Longitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "mrrUsd", + "displayName": "MRR", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "seats", + "displayName": "Seats", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "slaTargetPercent", + "displayName": "SLA target", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "slaActualPercent", + "displayName": "SLA (30 days)", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "openTickets", + "displayName": "Open tickets", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "csat", + "displayName": "CSAT", + "shape": [ + "number", + { + "decimalPlaces": 1, + "thousandsSeparator": true + } + ] + }, + { + "name": "renewalDate", + "displayName": "Renewal date", + "shape": [ + "date", + { + "format": "dd MMM yyyy" + } + ] + }, + { + "sourceId": "customerId", + "sourceType": "Customer", + "name": "customer" + } + ], + "timeframes": false +} diff --git a/plugins/SampleData/v3/dataStreams/deployments.json b/plugins/SampleData/v3/dataStreams/deployments.json new file mode 100644 index 00000000..fd940edb --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/deployments.json @@ -0,0 +1,113 @@ +{ + "name": "deployments", + "displayName": "Deployments", + "description": "Production deployments with outcome, duration and lead time (DORA metrics)", + "tags": [ + "DevOps", + "CI/CD" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "deployments.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Pipeline", + "Service" + ] + } + }, + "metadata": [ + { + "name": "timestamp", + "displayName": "Finished", + "shape": "date", + "role": "timestamp" + }, + { + "name": "deploymentId", + "displayName": "Deployment ID", + "visible": false + }, + { + "name": "pipelineId", + "visible": false + }, + { + "name": "pipeline", + "displayName": "Pipeline", + "role": "label" + }, + { + "name": "service", + "displayName": "Service" + }, + { + "name": "team", + "displayName": "Team" + }, + { + "name": "tool", + "displayName": "CI/CD tool" + }, + { + "name": "version", + "displayName": "Version" + }, + { + "name": "status", + "displayName": "Status" + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "changeFailure", + "displayName": "Caused a failure", + "shape": "boolean" + }, + { + "name": "durationMinutes", + "displayName": "Duration", + "shape": [ + "minutes", + { + "formatDuration": true + } + ] + }, + { + "name": "leadTimeHours", + "displayName": "Lead time (hours)", + "shape": [ + "number", + { + "decimalPlaces": 1, + "thousandsSeparator": true + } + ] + }, + { + "name": "timeToRestoreMinutes", + "displayName": "Time to restore", + "shape": [ + "minutes", + { + "formatDuration": true + } + ] + }, + { + "sourceId": "pipelineId", + "sourceType": "Pipeline", + "name": "pipeline" + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/dataStreams/hostHealth.json b/plugins/SampleData/v3/dataStreams/hostHealth.json new file mode 100644 index 00000000..4edf9b7d --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/hostHealth.json @@ -0,0 +1,141 @@ +{ + "name": "hostHealth", + "displayName": "Host Health", + "description": "Current resource usage and health for each host", + "tags": [ + "Infrastructure", + "Health" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "hostHealth.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Host", + "Service" + ] + } + }, + "defaultShaping": { + "sort": { + "by": [ + [ + "host", + "asc" + ] + ] + } + }, + "metadata": [ + { + "name": "hostId", + "visible": false + }, + { + "name": "host", + "displayName": "Host", + "role": "label" + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "service", + "displayName": "Service" + }, + { + "name": "region", + "displayName": "Region" + }, + { + "name": "provider", + "displayName": "Cloud provider" + }, + { + "name": "os", + "displayName": "Operating system" + }, + { + "name": "size", + "displayName": "Instance size" + }, + { + "name": "latitude", + "displayName": "Latitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "longitude", + "displayName": "Longitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "cpuPercent", + "displayName": "CPU", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "memoryPercent", + "displayName": "Memory", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "diskPercent", + "displayName": "Disk used", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "uptimeDays", + "displayName": "Uptime (days)", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "sourceId": "hostId", + "sourceType": "Host", + "name": "host" + } + ], + "timeframes": false +} diff --git a/plugins/SampleData/v3/dataStreams/hostPerformance.json b/plugins/SampleData/v3/dataStreams/hostPerformance.json new file mode 100644 index 00000000..0415d201 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/hostPerformance.json @@ -0,0 +1,86 @@ +{ + "name": "hostPerformance", + "displayName": "Host Performance", + "description": "CPU, memory, disk and network usage over time for each host", + "tags": [ + "Infrastructure", + "Performance" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "hostPerformance.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Host", + "Service" + ] + } + }, + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "hostId", + "visible": false + }, + { + "name": "host", + "displayName": "Host", + "role": "label" + }, + { + "name": "service", + "displayName": "Service" + }, + { + "name": "cpuPercent", + "displayName": "CPU", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "memoryPercent", + "displayName": "Memory", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "diskPercent", + "displayName": "Disk used", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "networkMbps", + "displayName": "Network", + "shape": "megabitspersecond" + }, + { + "sourceId": "hostId", + "sourceType": "Host", + "name": "host" + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/dataStreams/objects.json b/plugins/SampleData/v3/dataStreams/objects.json new file mode 100644 index 00000000..c9c7a5c4 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/objects.json @@ -0,0 +1,98 @@ +{ + "name": "objects", + "displayName": "Objects", + "description": "Every service, host, pipeline and customer in the sample company", + "tags": [ + "Inventory" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "objects.js" + }, + "matches": "none", + "visibility": { + "type": "hidden" + }, + "metadata": [ + { + "name": "id", + "displayName": "ID" + }, + { + "name": "name", + "displayName": "Name", + "role": "label" + }, + { + "name": "type", + "displayName": "Type" + }, + { + "name": "team", + "displayName": "Team" + }, + { + "name": "tier", + "displayName": "Tier" + }, + { + "name": "region", + "displayName": "Region" + }, + { + "name": "provider", + "displayName": "Cloud provider" + }, + { + "name": "sloTarget", + "displayName": "SLO target", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "serviceName", + "displayName": "Service" + }, + { + "name": "os", + "displayName": "Operating system" + }, + { + "name": "size", + "displayName": "Instance size" + }, + { + "name": "tool", + "displayName": "CI/CD tool" + }, + { + "name": "plan", + "displayName": "Plan" + }, + { + "name": "city", + "displayName": "City" + }, + { + "name": "country", + "displayName": "Country" + }, + { + "name": "slaTarget", + "displayName": "SLA target", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/SampleData/v3/dataStreams/scripts/businessMetrics.js b/plugins/SampleData/v3/dataStreams/scripts/businessMetrics.js new file mode 100644 index 00000000..3545496b --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/businessMetrics.js @@ -0,0 +1,158 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const checkout = services["checkout-api"]; +const payments = services[incident.service]; +const mrrNow = model.customers.reduce((sum, c) => sum + c.mrr, 0) * 2.4; +const rows = []; +for (const t of buckets()) { + const perMin = bucket / MIN; + const w = wave("business", t); + const visitors = 5200 * load(t, 16) * (1 + 0.12 * w) * perMin; + // Orders processed through the platform; failed payments turn into lost orders + const orders = (checkout.requestsPerMin / 35) * load(t, 16) * (1 + 0.1 * w) * (1 - 0.6 * stress(payments, t)) * perMin; + rows.push({ + timestamp: iso(t), + visitors: round(visitors, 0), + signups: round(visitors * 0.011 * (1 + 0.2 * wave("signups", t)), 0), + orders: round(orders, 0), + orderValueUsd: round(orders * 86 * (1 + 0.08 * wave("aov", t))), + conversionRatePercent: round((orders / Math.max(1, visitors)) * 100, 2), + // MRR grows about 3% a month + mrrUsd: round(mrrNow * Math.pow(1.03, (t - NOW) / (30 * DAY)), 0), + }); +} +result = rows; diff --git a/plugins/SampleData/v3/dataStreams/scripts/cloudCost.js b/plugins/SampleData/v3/dataStreams/scripts/cloudCost.js new file mode 100644 index 00000000..b81e0a23 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/cloudCost.js @@ -0,0 +1,168 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const categories = [ + { name: "Compute", share: 0.52 }, + { name: "Database", share: 0.22 }, + { name: "Storage", share: 0.14 }, + { name: "Network", share: 0.12 }, +]; +const rows = []; +const today = Math.floor(NOW / DAY); +for (const s of scoped(model.services)) { + const dailyBudget = (s.monthlyCost * 1.08) / 30; + for (let day = Math.floor(windowStart / DAY); day <= today; day++) { + // Search has a cost anomaly: an index rebuild left extra nodes running for the last 9 days + const anomaly = s.id === "search-service" && day > today - 9 ? 1.65 : 1; + // Today is a partial day + const partial = day === today ? (NOW - day * DAY) / DAY : 1; + for (const c of categories) { + const key = `${s.id}:${c.name}:${day}`; + const compute = c.name === "Compute" ? anomaly : 1; + rows.push({ + date: iso(day * DAY), + serviceId: s.id, + service: s.name, + team: teams[s.team].name, + provider: s.provider, + category: c.name, + costUsd: round(((s.monthlyCost / 30) * c.share * compute * (0.9 + 0.2 * rand(key))) * partial), + budgetUsd: round(dailyBudget * c.share * partial), + }); + } + } +} +result = rows; diff --git a/plugins/SampleData/v3/dataStreams/scripts/conversionFunnel.js b/plugins/SampleData/v3/dataStreams/scripts/conversionFunnel.js new file mode 100644 index 00000000..5bf1c96c --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/conversionFunnel.js @@ -0,0 +1,150 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const days = Math.max(1 / 24, span / DAY); +const visitors = 5200 * 0.78 * 60 * 24 * days; +// Visitors are left out: at ~100x the next stage they flatten the funnel +const stages = [ + ["Signups", 0.011], + ["Trials Started", 0.0072], + ["Activated", 0.0041], + ["Paid", 0.0017], +]; +result = stages.map(([stage, ratio], i) => ({ + order: i + 1, + stage, + count: Math.round(visitors * ratio * (1 + 0.05 * wave(`funnel:${stage}`, windowEnd))), +})); diff --git a/plugins/SampleData/v3/dataStreams/scripts/customerSla.js b/plugins/SampleData/v3/dataStreams/scripts/customerSla.js new file mode 100644 index 00000000..c4b8d598 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/customerSla.js @@ -0,0 +1,165 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +// Customers ride on the Tier 1 path, so their SLA follows checkout and payments +const path = ["api-gateway", "checkout-api", "payments-api"].map((id) => services[id]); +const baseAvailability = Math.min(...path.map(availability30d)); +const csatBase = { Enterprise: 4.5, Business: 4.4, Starter: 4.2 }; +result = scoped(model.customers).map((c) => { + const hit = incident.affectedCustomers.includes(c.id); + const r = rand(`${c.id}:sla`); + const slaActual = Math.min(99.999, baseAvailability + (r - 0.3) * 0.06 - (hit ? 0.07 : 0)); + const openTickets = Math.floor(rand(`${c.id}:open`) * (c.plan === "Enterprise" ? 6 : 3)) + (hit ? 1 : 0); + const csat = round(Math.min(5, csatBase[c.plan] + (r - 0.5) * 0.6 - (hit ? 0.5 : 0)), 1); + const state = slaActual < c.slaTarget ? "error" : slaActual < c.slaTarget + 0.02 || csat < 4 ? "warning" : "success"; + return { + customerId: c.id, + customer: c.name, + state, + plan: c.plan, + city: c.city, + country: c.country, + latitude: c.latitude, + longitude: c.longitude, + mrrUsd: c.mrr, + seats: c.seats, + slaTargetPercent: c.slaTarget, + slaActualPercent: round(slaActual, 3), + openTickets, + csat, + renewalDate: iso(Math.floor(NOW / DAY + 20 + rand(`${c.id}:renew`) * 330) * DAY).slice(0, 10), + }; +}); diff --git a/plugins/SampleData/v3/dataStreams/scripts/deployments.js b/plugins/SampleData/v3/dataStreams/scripts/deployments.js new file mode 100644 index 00000000..4dd0f755 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/deployments.js @@ -0,0 +1,191 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const rows = []; +const today = Math.floor(NOW / DAY); +for (const p of scoped(model.pipelines).filter((p) => forService(p.service))) { + const s = services[p.service]; + for (let day = Math.floor(windowStart / DAY); day <= today; day++) { + const weekday = new Date(day * DAY).getUTCDay(); + const rate = weekday === 0 || weekday === 6 ? p.deploysPerDay * 0.2 : p.deploysPerDay; + const count = Math.floor(rate + rand(`${p.id}:count:${day}`)); + for (let n = 0; n < count; n++) { + const key = `${p.id}:${day}:${n}`; + // Deploys land in working hours, 08:00-18:00 UTC + const at = day * DAY + (8 + rand(`${key}:at`) * 10) * HOUR; + if (at < windowStart || at > NOW - 30 * MIN) continue; + const failed = rand(`${key}:fail`) < p.failureRate; + const changeFailure = !failed && rand(`${key}:cf`) < p.failureRate * 0.6; + rows.push({ + timestamp: iso(at), + deploymentId: `${p.id}-${day}-${n}`, + pipeline: p.name, + pipelineId: p.id, + service: s.name, + team: teams[p.team].name, + tool: p.tool, + version: `${4 + Math.floor(day / 120) % 3}.${day % 40}.${n}`, + status: failed ? "Failed" : "Succeeded", + state: failed ? "error" : changeFailure ? "warning" : "success", + changeFailure, + durationMinutes: round(p.durationMin * (0.7 + 0.6 * rand(`${key}:dur`)), 1), + leadTimeHours: round(p.leadTimeHours * (0.4 + 1.2 * rand(`${key}:lead`)), 1), + timeToRestoreMinutes: changeFailure ? round(20 + rand(`${key}:mttr`) * 160, 0) : null, + }); + } + } + // The release that started the incident + const releaseAt = incidentStart - 6 * MIN; + if (p.id === incident.pipeline && releaseAt >= windowStart) { + rows.push({ + timestamp: iso(releaseAt), + deploymentId: `${p.id}-incident`, + pipeline: p.name, + pipelineId: p.id, + service: s.name, + team: teams[p.team].name, + tool: p.tool, + version: incident.release, + status: "Succeeded", + state: "warning", + changeFailure: true, + durationMinutes: round(p.durationMin * 1.1, 1), + leadTimeHours: round(p.leadTimeHours * 0.6, 1), + timeToRestoreMinutes: null, + }); + } +} +result = rows.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); diff --git a/plugins/SampleData/v3/dataStreams/scripts/hostHealth.js b/plugins/SampleData/v3/dataStreams/scripts/hostHealth.js new file mode 100644 index 00000000..daf2c417 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/hostHealth.js @@ -0,0 +1,171 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const metricsAt = (h, t) => { + const s = services[h.service]; + const k = stress(s, t); + const w = wave(h.id, t); + // Disk on the busiest analytics node fills steadily, a slow-burning problem + const growth = h.disk > 85 ? -6 * Math.max(0, (NOW - t) / (14 * DAY)) : 0; + return { + cpuPercent: round(Math.min(99, h.cpu * (0.8 + 0.4 * load(t)) * (1 + 0.25 * w) + 45 * k), 1), + memoryPercent: round(Math.min(99, h.memory * (1 + 0.06 * w) + 12 * k), 1), + diskPercent: round(Math.min(99, h.disk + growth + 1.5 * w), 1), + }; +}; +const hostState = (m) => + m.diskPercent >= 90 || m.cpuPercent >= 90 ? "error" + : m.diskPercent >= 80 || m.cpuPercent >= 75 || m.memoryPercent >= 85 ? "warning" + : "success"; +result = scoped(model.hosts).filter((h) => forService(h.service)).map((h, i) => { + const m = metricsAt(h, NOW); + const region = regions[h.region]; + return { + hostId: h.id, + host: h.id, + state: hostState(m), + service: services[h.service].name, + region: region.name, + provider: region.provider, + os: h.os, + size: h.size, + // Spread hosts around the region so map markers do not overlap + latitude: round(region.latitude + (rand(`${h.id}:lat`) - 0.5) * 1.6, 3), + longitude: round(region.longitude + (rand(`${h.id}:lon`) - 0.5) * 1.6, 3), + ...m, + uptimeDays: Math.floor(3 + rand(`${h.id}:uptime`) * 120), + }; +}); diff --git a/plugins/SampleData/v3/dataStreams/scripts/hostPerformance.js b/plugins/SampleData/v3/dataStreams/scripts/hostPerformance.js new file mode 100644 index 00000000..1d5fede6 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/hostPerformance.js @@ -0,0 +1,156 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const rows = []; +for (const h of scoped(model.hosts).filter((h) => forService(h.service))) { + const s = services[h.service]; + for (const t of buckets()) { + const k = stress(s, t); + const w = wave(h.id, t); + const growth = h.disk > 85 ? -6 * Math.max(0, (NOW - t) / (14 * DAY)) : 0; + rows.push({ + timestamp: iso(t), + hostId: h.id, + host: h.id, + service: s.name, + cpuPercent: round(Math.min(99, h.cpu * (0.8 + 0.4 * load(t)) * (1 + 0.25 * w) + 45 * k), 1), + memoryPercent: round(Math.min(99, h.memory * (1 + 0.06 * w) + 12 * k), 1), + diskPercent: round(Math.min(99, h.disk + growth + 1.5 * w), 1), + networkMbps: round((s.requestsPerMin / 40) * load(t) * (1 + 0.3 * w) * (1 + k), 1), + }); + } +} +result = rows; diff --git a/plugins/SampleData/v3/dataStreams/scripts/objects.js b/plugins/SampleData/v3/dataStreams/scripts/objects.js new file mode 100644 index 00000000..17d026f4 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/objects.js @@ -0,0 +1,175 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const services_ = model.services.map((s) => ({ + id: s.id, + name: s.name, + type: "Service", + team: teams[s.team].name, + tier: s.tier, + region: regions[s.region].name, + provider: s.provider, + sloTarget: s.sloTarget, +})); +const hosts = model.hosts.map((h) => ({ + id: h.id, + name: h.id, + type: "Host", + serviceName: services[h.service].name, + team: teams[services[h.service].team].name, + region: regions[h.region].name, + provider: regions[h.region].provider, + os: h.os, + size: h.size, +})); +const pipelines = model.pipelines.map((p) => ({ + id: p.id, + name: p.name, + type: "Pipeline", + serviceName: services[p.service].name, + team: teams[p.team].name, + tool: p.tool, +})); +const customers = model.customers.map((c) => ({ + id: c.id, + name: c.name, + type: "Customer", + plan: c.plan, + city: c.city, + country: c.country, + slaTarget: c.slaTarget, +})); +result = [...services_, ...hosts, ...pipelines, ...customers]; diff --git a/plugins/SampleData/v3/dataStreams/scripts/serviceDependencies.js b/plugins/SampleData/v3/dataStreams/scripts/serviceDependencies.js new file mode 100644 index 00000000..16349002 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/serviceDependencies.js @@ -0,0 +1,151 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const ids = new Set(scoped(model.services).map((s) => s.id)); +result = model.dependencies + .filter((d) => ids.has(d.from) || ids.has(d.to)) + .map((d) => { + const from = services[d.from]; + const to = services[d.to]; + const target = signals(to, NOW); + return { + source: from.name, + target: to.name, + requestsPerMin: round(signals(from, NOW).requestsPerMin * d.share, 0), + targetErrorRatePercent: target.errorRatePercent, + state: target.state, + }; + }); diff --git a/plugins/SampleData/v3/dataStreams/scripts/serviceHealth.js b/plugins/SampleData/v3/dataStreams/scripts/serviceHealth.js new file mode 100644 index 00000000..d3ba25ed --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/serviceHealth.js @@ -0,0 +1,163 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +result = scoped(model.services).map((s) => { + const now = signals(s, NOW); + const availability = availability30d(s); + const budget = 100 - s.sloTarget; + const budgetRemaining = ((budget - (100 - availability)) / budget) * 100; + const region = regions[s.region]; + // Burning the error budget fast is a warning even while the service is up + const state = now.state !== "success" ? now.state : budgetRemaining < 25 ? "warning" : "success"; + return { + serviceId: s.id, + service: s.name, + state, + team: teams[s.team].name, + tier: s.tier, + region: region.name, + provider: s.provider, + latitude: region.latitude, + longitude: region.longitude, + latencyP95Ms: now.latencyP95Ms, + requestsPerMin: now.requestsPerMin, + errorRatePercent: now.errorRatePercent, + availabilityPercent: round(availability, 3), + sloTargetPercent: s.sloTarget, + errorBudgetRemainingPercent: round(Math.max(-100, budgetRemaining), 1), + hosts: model.hosts.filter((h) => h.service === s.id).length, + }; +}); diff --git a/plugins/SampleData/v3/dataStreams/scripts/serviceMetrics.js b/plugins/SampleData/v3/dataStreams/scripts/serviceMetrics.js new file mode 100644 index 00000000..74e86164 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/serviceMetrics.js @@ -0,0 +1,143 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const rows = []; +for (const s of scoped(model.services)) { + for (const t of buckets()) { + rows.push({ timestamp: iso(t), serviceId: s.id, service: s.name, ...signals(s, t) }); + } +} +result = rows; diff --git a/plugins/SampleData/v3/dataStreams/scripts/tickets.js b/plugins/SampleData/v3/dataStreams/scripts/tickets.js new file mode 100644 index 00000000..259a763f --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/scripts/tickets.js @@ -0,0 +1,209 @@ +// --- Shared sample-data generator (identical in every script) --- +// Data stream scripts cannot import modules, so each script carries this block. +// Values are deterministic: noise is seeded by a key and an absolute time bucket, +// so history stays stable between refreshes, while the incident is anchored to +// "now" so it is always recent. +const model = data && data.services ? data : JSON.parse(response.body); +const NOW = Date.now(); +const MIN = 60000; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +// Uniform 0..1 from any string key +const rand = (key) => { + let h = 2166136261; + const s = String(key); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + h ^= h >>> 13; + h = Math.imul(h, 2246822507); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +}; + +// Smooth value noise in -0.5..0.5, slow drift plus faster wobble +const smooth = (key, t, step) => { + const i = Math.floor(t / step); + const f = t / step - i; + const a = rand(`${key}:${step}:${i}`); + const b = rand(`${key}:${step}:${i + 1}`); + const e = f * f * (3 - 2 * f); + return a + (b - a) * e; +}; +const wave = (key, t) => 0.6 * smooth(key, t, 3 * HOUR) + 0.4 * smooth(key, t, 15 * MIN) - 0.5; + +// Business-hours load curve, 0.55 overnight to 1.0 at peak (UTC) +const load = (t, peakHourUtc = 15) => { + const h = (t / HOUR) % 24; + return 0.775 + 0.225 * Math.cos((2 * Math.PI * (h - peakHourUtc)) / 24); +}; + +// The headline incident: 0 before it starts, 1 at peak, then a slow recovery +const incident = model.incident; +const incidentStart = NOW - incident.startedMinutesAgo * MIN; +const incidentPeakEnd = NOW - incident.peakEndsMinutesAgo * MIN; +const incidentLevel = (t) => { + if (t < incidentStart) return 0; + if (t < incidentStart + 10 * MIN) return (t - incidentStart) / (10 * MIN); + if (t <= incidentPeakEnd) return 1; + return Math.exp(-(t - incidentPeakEnd) / (45 * MIN)); +}; +const serviceImpact = (serviceId, t) => + serviceId === incident.service + ? incidentLevel(t) + : incident.affectedServices.includes(serviceId) + ? 0.35 * incidentLevel(t) + : 0; + +// Occasional short historical blips so long timeframes are not flat +const blip = (key, t) => { + const day = Math.floor(t / DAY); + if (day >= Math.floor(NOW / DAY) || rand(`${key}:blip:${day}`) > 0.1) return 0; + const at = day * DAY + rand(`${key}:blipAt:${day}`) * DAY; + const len = (20 + rand(`${key}:blipLen:${day}`) * 70) * MIN; + return t >= at && t <= at + len ? 0.3 + 0.4 * rand(`${key}:blipSev:${day}`) : 0; +}; + +const stateFor = (level) => (level >= 0.5 ? "error" : level >= 0.15 ? "warning" : "success"); +const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp; + +// Timeframe window and a bucket size that keeps row counts sensible +const tf = context.timeframe || {}; +const toMs = (v, unix) => (unix ? unix * 1000 : Date.parse(v)); +const windowEnd = Math.min(toMs(tf.end, tf.unixEnd) || NOW, NOW); +const windowStart = toMs(tf.start, tf.unixStart) || windowEnd - DAY; +const span = windowEnd - windowStart; +const bucket = + span <= 2 * HOUR ? MIN + : span <= 13 * HOUR ? 5 * MIN + : span <= 25 * HOUR ? 15 * MIN + : span <= 8 * DAY ? HOUR + : span <= 32 * DAY ? 4 * HOUR + : DAY; +const buckets = (step = bucket) => { + const out = []; + for (let t = Math.ceil(windowStart / step) * step; t <= windowEnd; t += step) out.push(t); + return out; +}; +const iso = (t) => new Date(t).toISOString(); + +// Objects the tile is scoped to; falls back to everything of that kind +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +// Ids of other kinds (e.g. a Service scoping a host stream) are ignored here +const scoped = (items) => { + const known = new Set(items.map((i) => i.id)); + const ids = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => known.has(id))); + return ids.size ? items.filter((i) => ids.has(i.id)) : items; +}; +const byId = (items) => Object.fromEntries(items.map((i) => [i.id, i])); +const services = byId(model.services); +const regions = byId(model.regions); +const teams = byId(model.teams); + +// Host, deployment and ticket streams can also be scoped to services (the Service +// perspective); rows are then narrowed to those services +const serviceFilter = new Set((context.objects || []).map((o) => unwrap(o.rawId)).filter((id) => services[id])); +const forService = (serviceId) => serviceFilter.size === 0 || serviceFilter.has(serviceId); + +// Golden signals for a service at time t +const stress = (s, t) => Math.min(1, serviceImpact(s.id, t) + blip(s.id, t)); +const signals = (s, t) => { + const k = stress(s, t); + const w = wave(s.id, t); + const errorRate = s.errorRate * (1 + 0.8 * w) + k * (s.id === incident.service ? 9 : 3.5); + return { + latencyP95Ms: round(s.latencyMs * (1 + 0.18 * w + 0.25 * (load(t) - 0.775)) * (1 + 2.8 * k), 0), + requestsPerMin: round(s.requestsPerMin * load(t) * (1 + 0.1 * w) * (1 - 0.3 * k), 0), + errorRatePercent: round(Math.max(0.01, errorRate), 3), + state: stateFor(k), + }; +}; + +// Availability over the last 30 days, sampled every 15 minutes. Only server-side +// failures (about a third of all errors) count against the SLO. +const availability30d = (s) => { + let total = 0; + let n = 0; + for (let t = NOW - 30 * DAY; t <= NOW; t += 15 * MIN) { + total += 100 - 0.3 * signals(s, t).errorRatePercent; + n++; + } + return total / n; +}; +// --- End shared block --- + +const priorities = [ + { name: "P1", weight: 0.03, slaMinutes: 240 }, + { name: "P2", weight: 0.12, slaMinutes: 480 }, + { name: "P3", weight: 0.45, slaMinutes: 1440 }, + { name: "P4", weight: 0.4, slaMinutes: 4320 }, +]; +const categoryList = ["Access & Login", "Billing", "Performance", "Bug", "How-to", "Integration"]; +const servicePool = model.services.filter((s) => s.tier !== "Tier 3"); +const ratePerDay = { Enterprise: 1.4, Business: 0.7, Starter: 0.25 }; +const pick = (r, list) => { + let acc = 0; + for (const item of list) { + acc += item.weight; + if (r < acc) return item; + } + return list[list.length - 1]; +}; +const ticket = (c, key, opened, priority, category, service, resolveFactor) => { + const resolution = priority.slaMinutes * resolveFactor; + const age = (NOW - opened) / MIN; + const resolved = age > resolution; + const elapsed = resolved ? resolution : age; + const breached = elapsed > priority.slaMinutes; + const status = resolved ? (age > resolution + 2 * DAY / MIN ? "Closed" : "Resolved") : age < 30 ? "New" : "In Progress"; + return { + ticketId: `LUM-${10000 + Math.floor(rand(key) * 89999)}`, + openedAt: iso(opened), + customerId: c.id, + customer: c.name, + plan: c.plan, + priority: priority.name, + category, + service: service.name, + team: teams[service.team].name, + status, + resolutionMinutes: resolved ? round(resolution, 0) : null, + slaTargetMinutes: priority.slaMinutes, + slaBreached: breached, + slaState: breached ? "error" : !resolved && elapsed > priority.slaMinutes * 0.75 ? "warning" : "success", + }; +}; +const rows = []; +const today = Math.floor(NOW / DAY); +for (const c of scoped(model.customers)) { + for (let day = Math.floor(windowStart / DAY); day <= today; day++) { + const weekday = new Date(day * DAY).getUTCDay(); + const rate = ratePerDay[c.plan] * (weekday === 0 || weekday === 6 ? 0.25 : 1); + const count = Math.floor(rate + rand(`${c.id}:tickets:${day}`)); + for (let n = 0; n < count; n++) { + const key = `${c.id}:${day}:${n}`; + // Business-hours bias for the day x hour heatmap + const hour = 7 + Math.floor(Math.pow(rand(`${key}:hour`), 0.9) * 12); + const opened = day * DAY + hour * HOUR + rand(`${key}:min`) * HOUR; + if (opened < windowStart || opened > NOW) continue; + const priority = pick(rand(`${key}:p`), priorities); + const category = categoryList[Math.floor(rand(`${key}:c`) * categoryList.length)]; + const service = servicePool[Math.floor(rand(`${key}:s`) * servicePool.length)]; + if (!forService(service.id)) continue; + rows.push(ticket(c, key, opened, priority, category, service, 0.1 + Math.pow(rand(`${key}:res`), 2))); + } + } + // Tickets raised by customers hit by the incident + if (incident.affectedCustomers.includes(c.id) && forService(incident.service)) { + const opened = incidentStart + (12 + rand(`${c.id}:inc`) * 25) * MIN; + if (opened >= windowStart) { + rows.push({ + ...ticket(c, `${c.id}:incident`, opened, priorities[0], "Performance", services[incident.service], 9), + category: "Payments Failing", + }); + } + } +} +result = rows.sort((a, b) => (a.openedAt < b.openedAt ? 1 : -1)); diff --git a/plugins/SampleData/v3/dataStreams/serviceDependencies.json b/plugins/SampleData/v3/dataStreams/serviceDependencies.json new file mode 100644 index 00000000..01f1be6d --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/serviceDependencies.json @@ -0,0 +1,59 @@ +{ + "name": "serviceDependencies", + "displayName": "Service Dependencies", + "description": "Request flow between services and the health of each downstream service", + "tags": [ + "Topology" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "serviceDependencies.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "metadata": [ + { + "name": "source", + "displayName": "From" + }, + { + "name": "target", + "displayName": "To" + }, + { + "name": "requestsPerMin", + "displayName": "Requests/min", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "targetErrorRatePercent", + "displayName": "Downstream error rate", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "state", + "displayName": "Downstream state", + "shape": "state" + } + ], + "timeframes": false +} diff --git a/plugins/SampleData/v3/dataStreams/serviceHealth.json b/plugins/SampleData/v3/dataStreams/serviceHealth.json new file mode 100644 index 00000000..40937b70 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/serviceHealth.json @@ -0,0 +1,162 @@ +{ + "name": "serviceHealth", + "displayName": "Service Health", + "description": "Current health, golden signals and SLO error budget for each service", + "tags": [ + "Health", + "SLO" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "serviceHealth.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "defaultShaping": { + "sort": { + "by": [ + [ + "service", + "asc" + ] + ] + } + }, + "metadata": [ + { + "name": "serviceId", + "visible": false + }, + { + "name": "service", + "displayName": "Service", + "role": "label" + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "team", + "displayName": "Team" + }, + { + "name": "tier", + "displayName": "Tier" + }, + { + "name": "region", + "displayName": "Region" + }, + { + "name": "provider", + "displayName": "Cloud provider" + }, + { + "name": "latitude", + "displayName": "Latitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "longitude", + "displayName": "Longitude", + "shape": [ + "number", + { + "decimalPlaces": 4, + "thousandsSeparator": true + } + ], + "visible": false + }, + { + "name": "latencyP95Ms", + "displayName": "Latency p95", + "shape": "milliseconds" + }, + { + "name": "requestsPerMin", + "displayName": "Requests/min", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "errorRatePercent", + "displayName": "Error rate", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "availabilityPercent", + "displayName": "Availability (30 days)", + "shape": [ + "percent", + { + "decimalPlaces": 3 + } + ] + }, + { + "name": "sloTargetPercent", + "displayName": "SLO target", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "errorBudgetRemainingPercent", + "displayName": "Error budget remaining", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "hosts", + "displayName": "Hosts", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "sourceId": "serviceId", + "sourceType": "Service", + "name": "service" + } + ], + "timeframes": false +} diff --git a/plugins/SampleData/v3/dataStreams/serviceMetrics.json b/plugins/SampleData/v3/dataStreams/serviceMetrics.json new file mode 100644 index 00000000..587a9736 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/serviceMetrics.json @@ -0,0 +1,77 @@ +{ + "name": "serviceMetrics", + "displayName": "Service Metrics", + "description": "Latency, throughput, error rate and health over time for each service", + "tags": [ + "Performance", + "Health" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "serviceMetrics.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "serviceId", + "visible": false + }, + { + "name": "service", + "displayName": "Service", + "role": "label" + }, + { + "name": "latencyP95Ms", + "displayName": "Latency p95", + "shape": "milliseconds" + }, + { + "name": "requestsPerMin", + "displayName": "Requests/min", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "name": "errorRatePercent", + "displayName": "Error rate", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "sourceId": "serviceId", + "sourceType": "Service", + "name": "service" + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/dataStreams/tickets.json b/plugins/SampleData/v3/dataStreams/tickets.json new file mode 100644 index 00000000..84dba063 --- /dev/null +++ b/plugins/SampleData/v3/dataStreams/tickets.json @@ -0,0 +1,105 @@ +{ + "name": "tickets", + "displayName": "Tickets", + "description": "Service desk tickets with priority, status and SLA outcome", + "tags": [ + "Service Management", + "Tickets" + ], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "model.json", + "postRequestScript": "tickets.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Customer", + "Service" + ] + } + }, + "metadata": [ + { + "name": "ticketId", + "displayName": "Ticket" + }, + { + "name": "openedAt", + "displayName": "Opened", + "shape": "date", + "role": "timestamp" + }, + { + "name": "customerId", + "visible": false + }, + { + "name": "customer", + "displayName": "Customer", + "role": "label" + }, + { + "name": "plan", + "displayName": "Plan" + }, + { + "name": "priority", + "displayName": "Priority" + }, + { + "name": "category", + "displayName": "Category" + }, + { + "name": "service", + "displayName": "Service" + }, + { + "name": "team", + "displayName": "Assigned team" + }, + { + "name": "status", + "displayName": "Status" + }, + { + "name": "resolutionMinutes", + "displayName": "Resolution time", + "shape": [ + "minutes", + { + "formatDuration": true + } + ] + }, + { + "name": "slaTargetMinutes", + "displayName": "SLA target", + "shape": [ + "minutes", + { + "formatDuration": true + } + ] + }, + { + "name": "slaBreached", + "displayName": "SLA breached", + "shape": "boolean" + }, + { + "name": "slaState", + "displayName": "SLA", + "shape": "state" + }, + { + "sourceId": "customerId", + "sourceType": "Customer", + "name": "customer" + } + ], + "timeframes": true +} diff --git a/plugins/SampleData/v3/defaultContent/Perspectives/customer.dash.json b/plugins/SampleData/v3/defaultContent/Perspectives/customer.dash.json new file mode 100644 index 00000000..9f5f69b9 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/Perspectives/customer.dash.json @@ -0,0 +1,702 @@ +{ + "name": "Customer", + "schemaVersion": "1.5", + "timeframe": "last30days", + "variables": [ + "{{variables.[Customer]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "52ebfd03-5312-4970-9c4f-105ce5839931", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "plan", + "city", + "country", + "slaTarget" + ], + "hiddenColumns": [ + "id", + "sourceId", + "sourceType", + "sourceName", + "team", + "tier", + "region", + "provider", + "sloTarget", + "serviceName", + "os", + "size", + "tool", + "rawId", + "link" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "plan", + "title": "Plan", + "renderer": { + "type": "text" + } + }, + { + "field": "city", + "title": "City", + "renderer": { + "type": "text" + } + }, + { + "field": "country", + "title": "Country", + "renderer": { + "type": "text" + } + }, + { + "field": "slaTarget", + "title": "SLA target", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true + } + } + } + } + }, + { + "i": "e4ad9189-ccce-4efd-aeba-84a7e6e0a244", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "SLA Attainment", + "description": "30-day SLA; the marker is the contracted target", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "arr", + "columns": [ + "slaActualPercent" + ] + }, + "minimum": 99, + "maximum": 100 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "slaActualPercent", + "type": "quantitative", + "title": "SLA (30 days)" + }, + "target": { + "field": "slaTargetPercent", + "type": "quantitative", + "title": "SLA target" + } + }, + "style": { + "gaugeMin": 99, + "gaugeMax": 100, + "gaugeTargetColor": "#d63939" + } + } + } + } + }, + { + "i": "59d5ff78-1909-465d-979b-fd39bc0be3ef", + "x": 2, + "y": 0, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Account", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "mrrUsd", + "comparisonColumn": "none", + "label": "MRR" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "mrrUsd", + "type": "quantitative", + "title": "MRR" + }, + { + "field": "seats", + "type": "quantitative", + "title": "Seats" + }, + { + "field": "csat", + "type": "quantitative", + "title": "CSAT (out of 5)" + }, + { + "field": "openTickets", + "type": "quantitative", + "title": "Open tickets" + } + ] + }, + "style": { + "scalarColumns": 4 + } + } + } + } + }, + { + "i": "4267ba68-9638-4358-8c28-98bec92e5b2e", + "x": 1, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets by Priority", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "priority", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "priority_uniqueValues", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "priority_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "priority_uniqueValues", + "type": "nominal", + "title": "Priority" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Tickets" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "mappedColors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + } + } + } + }, + { + "i": "f3dc7165-1631-4a0a-b10a-798675596644", + "x": 2, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets per Day", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "openedAt", + "byDay" + ], + [ + "priority", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "openedAt_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "openedAt_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "priority_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "stacked", + "showTotals": false, + "showValue": false, + "grouping": true, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "openedAt_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Tickets" + }, + "series": { + "field": "priority_uniqueValues", + "type": "nominal", + "title": "Priority" + } + }, + "style": { + "cartesianStack": "on", + "mappedColors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + } + } + } + }, + { + "i": "b658fba4-80b9-4740-b8ad-d5f39fe7536b", + "x": 0, + "y": 4, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "openedAt", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "ticketId", + "openedAt", + "priority", + "category", + "service", + "status", + "resolutionMinutes", + "slaState" + ], + "hiddenColumns": [ + "customerId", + "customer", + "plan", + "team", + "slaTargetMinutes", + "slaBreached" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "ticketId", + "title": "Ticket", + "renderer": { + "type": "text" + } + }, + { + "field": "openedAt", + "title": "Opened", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "priority", + "title": "Priority", + "renderer": { + "type": "badge", + "colors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + }, + { + "field": "category", + "title": "Category", + "renderer": { + "type": "text" + } + }, + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "New": "#206bc4", + "In Progress": "#f59f00" + }, + "defaultColor": "#9ba3af" + } + }, + { + "field": "resolutionMinutes", + "title": "Resolution time", + "align": "right", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " min" + } + }, + { + "field": "slaState", + "title": "SLA", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "slaState", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "openedAt", + "direction": "desc" + } + } + } + } + } + }, + { + "i": "7521ac27-e02a-491e-89e6-946c52e03248", + "x": 0, + "y": 9, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ticket SLA History", + "description": "Worst SLA outcome of the tickets opened each day", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Customer]}}" + }, + "variables": [ + "{{variables.[Customer]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "healthHistory", + "encoding": { + "time": { + "field": "openedAt", + "type": "temporal", + "title": "Opened" + }, + "status": { + "field": "slaState", + "type": "nominal", + "title": "SLA" + } + }, + "style": { + "historySlots": 30, + "historyBarHeight": 36, + "historyUptimeLabel": "within SLA" + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/Perspectives/host.dash.json b/plugins/SampleData/v3/defaultContent/Perspectives/host.dash.json new file mode 100644 index 00000000..12fac1e8 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/Perspectives/host.dash.json @@ -0,0 +1,777 @@ +{ + "name": "Host", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Host]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "87418068-6eb1-4729-9edb-2bfbd02d19cc", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Host Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serviceName", + "team", + "region", + "provider", + "os", + "size" + ], + "hiddenColumns": [ + "id", + "sourceId", + "sourceType", + "sourceName", + "tier", + "sloTarget", + "tool", + "plan", + "city", + "country", + "slaTarget", + "rawId", + "link" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "serviceName", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "region", + "title": "Region", + "renderer": { + "type": "text" + } + }, + { + "field": "provider", + "title": "Cloud provider", + "renderer": { + "type": "text" + } + }, + { + "field": "os", + "title": "Operating system", + "renderer": { + "type": "text" + } + }, + { + "field": "size", + "title": "Instance size", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true + } + } + } + } + }, + { + "i": "0cae4756-4010-4bac-b938-ffbb33ea7e8c", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CPU", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "arr", + "columns": [ + "cpuPercent" + ] + }, + "minimum": 0, + "maximum": 100 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "cpuPercent", + "type": "quantitative", + "title": "CPU" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 100, + "gaugeBands": [ + { + "to": 0.75, + "color": "#2fb344" + }, + { + "to": 0.9, + "color": "#f59f00" + }, + { + "to": 1, + "color": "#d63939" + } + ] + } + } + } + } + }, + { + "i": "f2e4a310-9ba5-492a-a6b2-bdd11a6fd4f2", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Memory", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "arr", + "columns": [ + "memoryPercent" + ] + }, + "minimum": 0, + "maximum": 100 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "memoryPercent", + "type": "quantitative", + "title": "Memory" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 100, + "gaugeBands": [ + { + "to": 0.75, + "color": "#2fb344" + }, + { + "to": 0.9, + "color": "#f59f00" + }, + { + "to": 1, + "color": "#d63939" + } + ] + } + } + } + } + }, + { + "i": "748f9c62-a262-4e33-a2c6-146a2a7db505", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Disk Used", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-gauge", + "config": { + "data-stream-gauge": { + "value": { + "type": "arr", + "columns": [ + "diskPercent" + ] + }, + "minimum": 0, + "maximum": 100 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "diskPercent", + "type": "quantitative", + "title": "Disk Used" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 100, + "gaugeBands": [ + { + "to": 0.75, + "color": "#2fb344" + }, + { + "to": 0.9, + "color": "#f59f00" + }, + { + "to": 1, + "color": "#d63939" + } + ] + } + } + } + } + }, + { + "i": "f98819ce-0f55-45cf-84d0-96dd88526480", + "x": 1, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Health", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "host", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "host", + "type": "nominal", + "title": "Host" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + } + }, + "style": { + "statusGridColumnsPerRow": 1 + } + } + } + } + }, + { + "i": "d5951717-a7fc-451e-98d2-fafed30c39e7", + "x": 2, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Network Throughput", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[hostPerformance]}}", + "name": "hostPerformance", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "networkMbps" + ] + }, + { + "type": "max", + "names": [ + "networkMbps" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "networkMbps_mean", + "comparisonColumn": "none", + "label": "Average network" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "networkMbps_mean", + "type": "quantitative", + "title": "Average network" + }, + { + "field": "networkMbps_max", + "type": "quantitative", + "title": "Peak network" + } + ] + }, + "style": { + "scalarColumns": 2 + } + } + } + } + }, + { + "i": "6b4503d6-9a34-4f1e-a6a5-fed79fd2e862", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CPU and Memory", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[hostPerformance]}}", + "name": "hostPerformance", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"timestamp\", \"cpuPercent\", \"memoryPercent\" FROM \"dataset\" ORDER BY \"timestamp\"", + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "cpuPercent", + "displayName": "CPU", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "memoryPercent", + "displayName": "Memory", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "cpuPercent", + "memoryPercent" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + } + }, + "style": { + "cartesianSymbol": "none", + "legendPosition": "bottom", + "cartesianYAxisMax": 100, + "cartesianYAxisMin": 0 + }, + "markLines": [ + { + "value": 80, + "label": "80%", + "color": "#f59f00" + } + ] + } + } + } + }, + { + "i": "d4f69015-780f-424a-ae93-506a37284f41", + "x": 0, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Disk Usage Trend", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[hostPerformance]}}", + "name": "hostPerformance", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "diskPercent" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "diskPercent", + "type": "quantitative", + "title": "Disk used" + } + }, + "style": { + "cartesianSymbol": "none", + "cartesianAreaGradient": true, + "cartesianYAxisMin": 0, + "cartesianYAxisMax": 100, + "colors": [ + "#206bc4" + ] + }, + "markAreas": [ + { + "from": 90, + "to": 100, + "label": "Critical", + "color": "rgba(214,57,57,0.1)" + } + ] + } + } + } + }, + { + "i": "cc96b645-145d-4dc9-84f8-825bec020ce8", + "x": 2, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Network", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[hostPerformance]}}", + "name": "hostPerformance", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Host]}}" + }, + "variables": [ + "{{variables.[Host]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "networkMbps" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "networkMbps", + "type": "quantitative", + "title": "Network" + } + }, + "style": { + "cartesianSymbol": "none", + "cartesianAreaGradient": true, + "colors": [ + "#7048e8" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/Perspectives/manifest.json b/plugins/SampleData/v3/defaultContent/Perspectives/manifest.json new file mode 100644 index 00000000..643fcb94 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/Perspectives/manifest.json @@ -0,0 +1,20 @@ +{ + "items": [ + { + "name": "service", + "type": "dashboard" + }, + { + "name": "host", + "type": "dashboard" + }, + { + "name": "pipeline", + "type": "dashboard" + }, + { + "name": "customer", + "type": "dashboard" + } + ] +} diff --git a/plugins/SampleData/v3/defaultContent/Perspectives/pipeline.dash.json b/plugins/SampleData/v3/defaultContent/Perspectives/pipeline.dash.json new file mode 100644 index 00000000..c12b11f1 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/Perspectives/pipeline.dash.json @@ -0,0 +1,718 @@ +{ + "name": "Pipeline", + "schemaVersion": "1.5", + "timeframe": "last30days", + "variables": [ + "{{variables.[Pipeline]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "8d440dc0-116c-49fa-bfaf-5e1354bad544", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pipeline Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serviceName", + "team", + "tool" + ], + "hiddenColumns": [ + "id", + "sourceId", + "sourceType", + "sourceName", + "tier", + "region", + "provider", + "sloTarget", + "os", + "size", + "plan", + "city", + "country", + "slaTarget", + "rawId", + "link" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "serviceName", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "tool", + "title": "CI/CD tool", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true + } + } + } + } + }, + { + "i": "ca37a547-ea7b-4116-a3e8-3cdbe25034a5", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployments", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Deployments" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Deployments" + } + ] + } + } + } + } + }, + { + "i": "7ffe4a1d-3acb-4f05-a8d7-0fc13640bedc", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Change Failure Rate", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(100.0 * SUM(CASE WHEN \"changeFailure\" THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 1) AS \"changeFailureRate\" FROM \"dataset\"", + "metadata": [ + { + "name": "changeFailureRate", + "displayName": "Change failure rate", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "changeFailureRate", + "comparisonColumn": "none", + "label": "Deployments that caused a failure" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "changeFailureRate", + "type": "quantitative", + "title": "Deployments that caused a failure" + } + ] + } + } + } + } + }, + { + "i": "333e16e0-fbb7-4009-bf72-fe097797e31f", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Lead Time for Changes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "leadTimeHours" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "leadTimeHours_mean", + "comparisonColumn": "none", + "label": "Average hours from commit to production" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "leadTimeHours_mean", + "type": "quantitative", + "title": "Average hours from commit to production" + } + ] + } + } + } + } + }, + { + "i": "8fbbb740-67ec-43d5-8792-8d13b46e00c8", + "x": 1, + "y": 2, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment History", + "description": "Worst deployment outcome per day; amber means a deployment caused a failure", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "healthHistory", + "encoding": { + "time": { + "field": "timestamp", + "type": "temporal", + "title": "Finished" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + } + }, + "style": { + "historySlots": 30, + "historyBarHeight": 36 + } + } + } + } + }, + { + "i": "39d50945-f932-4dcf-9a7b-8b34da8fa82d", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployments per Day", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "timestamp", + "byDay" + ], + [ + "status", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "timestamp_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "timestamp_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "status_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "stacked", + "showTotals": false, + "showValue": false, + "grouping": true, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "timestamp_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Deployments" + }, + "series": { + "field": "status_uniqueValues", + "type": "nominal", + "title": "Status" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom", + "mappedColors": { + "Succeeded": "#2fb344", + "Failed": "#d63939" + } + } + } + } + } + }, + { + "i": "567a135f-6be2-49b5-9356-52351233b6e4", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment Duration", + "description": "How long deployments take", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "histogram", + "encoding": { + "x": { + "field": "durationMinutes", + "type": "quantitative", + "title": "Duration (minutes)" + } + }, + "style": { + "histogramBins": 10, + "colors": [ + "#206bc4" + ] + } + } + } + } + }, + { + "i": "8ab3e6f0-5acc-4c77-a146-9633cb4adbf9", + "x": 0, + "y": 7, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment Log", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "timestamp", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pipeline]}}" + }, + "variables": [ + "{{variables.[Pipeline]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "timestamp", + "version", + "status", + "changeFailure", + "state", + "durationMinutes", + "leadTimeHours", + "timeToRestoreMinutes" + ], + "hiddenColumns": [ + "deploymentId", + "pipelineId", + "pipeline", + "service", + "team", + "tool" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Finished", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "version", + "title": "Version", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "Succeeded": "#2fb344", + "Failed": "#d63939" + } + } + }, + { + "field": "changeFailure", + "title": "Caused a failure", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No", + "trueColor": "#d63939", + "falseColor": "#9ba3af" + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + }, + { + "field": "durationMinutes", + "title": "Duration", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " min" + } + }, + { + "field": "leadTimeHours", + "title": "Lead time", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " h" + } + }, + { + "field": "timeToRestoreMinutes", + "title": "Time to restore", + "align": "right", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " min" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "timestamp", + "direction": "desc" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/Perspectives/service.dash.json b/plugins/SampleData/v3/defaultContent/Perspectives/service.dash.json new file mode 100644 index 00000000..6d49d833 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/Perspectives/service.dash.json @@ -0,0 +1,1171 @@ +{ + "name": "Service", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Service]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "7c351175-10d6-4ea1-8878-7a6a5efa68a5", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "team", + "tier", + "region", + "provider", + "sloTarget" + ], + "hiddenColumns": [ + "id", + "sourceId", + "sourceType", + "sourceName", + "serviceName", + "os", + "size", + "tool", + "plan", + "city", + "country", + "slaTarget", + "rawId", + "link" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "name", + "title": "Name", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "tier", + "title": "Tier", + "renderer": { + "type": "text" + } + }, + { + "field": "region", + "title": "Region", + "renderer": { + "type": "text" + } + }, + { + "field": "provider", + "title": "Cloud provider", + "renderer": { + "type": "text" + } + }, + { + "field": "sloTarget", + "title": "SLO target", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableTranspose": true + } + } + } + } + }, + { + "i": "788a3035-9426-4481-8632-5f6763316d24", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Health", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "service", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "service", + "type": "nominal", + "title": "Service" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + } + }, + "style": { + "statusGridColumnsPerRow": 1 + } + } + } + } + }, + { + "i": "aec69c4d-b32f-4553-9126-ed99eb112ea8", + "x": 2, + "y": 0, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Golden Signals", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "latencyP95Ms", + "comparisonColumn": "none", + "label": "Latency p95" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "latencyP95Ms", + "type": "quantitative", + "title": "Latency p95" + }, + { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + }, + { + "field": "errorRatePercent", + "type": "quantitative", + "title": "Error rate" + }, + { + "field": "errorBudgetRemainingPercent", + "type": "quantitative", + "title": "Error budget left" + } + ] + }, + "style": { + "scalarColumns": 4 + } + } + } + } + }, + { + "i": "9a150b9e-e231-4e86-abe1-b3ce46b15b7f", + "x": 1, + "y": 2, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Health History", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "healthHistory", + "encoding": { + "time": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + } + }, + "style": { + "historySlots": 96, + "historyBarHeight": 36, + "historyBarGap": 1 + } + } + } + } + }, + { + "i": "4bf6673a-cb51-40c3-b369-803c3e7723df", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Latency", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "latencyP95Ms" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "latencyP95Ms", + "type": "quantitative", + "title": "Latency p95" + } + }, + "style": { + "cartesianSymbol": "none", + "colors": [ + "#206bc4" + ] + }, + "markLines": [ + { + "type": "average", + "label": "Average" + } + ] + } + } + } + }, + { + "i": "cd857f5c-0062-45c4-8c9a-a630919360f8", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Error Rate", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "errorRatePercent" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "errorRatePercent", + "type": "quantitative", + "title": "Error rate" + } + }, + "style": { + "cartesianSymbol": "none", + "cartesianAreaGradient": true, + "colors": [ + "#d63939" + ] + }, + "markLines": [ + { + "value": 1, + "label": "1% alert threshold", + "color": "#d63939" + } + ] + } + } + } + }, + { + "i": "60fce2f8-6d83-480c-bac2-f5f65e87769e", + "x": 0, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Requests per Minute", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "requestsPerMin" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + } + }, + "style": { + "cartesianSymbol": "none", + "cartesianAreaGradient": true, + "colors": [ + "#2fb344" + ] + } + } + } + } + }, + { + "i": "70fde704-f922-477d-a85b-7d4f6f568e9d", + "x": 2, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Dependencies", + "description": "Requests per minute to and from this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceDependencies]}}", + "name": "serviceDependencies", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "sankey", + "encoding": { + "source": { + "field": "source", + "type": "nominal", + "title": "From" + }, + "target": { + "field": "target", + "type": "nominal", + "title": "To" + }, + "value": { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + } + } + } + } + } + }, + { + "i": "c8c4beac-d4e9-40be-be32-1ee63101ed17", + "x": 0, + "y": 10, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Hosts", + "description": "Hosts running this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "host", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "host", + "type": "nominal", + "title": "Host" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "group": { + "field": "region", + "type": "nominal", + "title": "Region" + }, + "value": { + "field": "diskPercent", + "type": "quantitative", + "title": "Disk used" + } + }, + "style": { + "statusGridColumnsPerRow": 8, + "statusGridMaxBlockHeight": 80 + } + } + }, + "variables": [ + "{{variables.[Service]}}" + ], + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + } + } + }, + { + "i": "7029b88c-e50f-4f50-af82-c3d06495f2f9", + "x": 0, + "y": 12, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Deployments", + "description": "Production deployments of this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "timestamp", + "desc" + ] + ], + "top": 50 + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "timestamp", + "pipeline", + "service", + "team", + "version", + "status", + "changeFailure", + "state", + "durationMinutes", + "leadTimeHours" + ], + "hiddenColumns": [ + "deploymentId", + "pipelineId", + "tool", + "timeToRestoreMinutes" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Finished", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "pipeline", + "title": "Pipeline", + "renderer": { + "type": "text" + } + }, + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "version", + "title": "Version", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "Succeeded": "#2fb344", + "Failed": "#d63939" + } + } + }, + { + "field": "changeFailure", + "title": "Caused a failure", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No", + "trueColor": "#d63939", + "falseColor": "#9ba3af" + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + }, + { + "field": "durationMinutes", + "title": "Duration", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " min" + } + }, + { + "field": "leadTimeHours", + "title": "Lead time", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " h" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "timestamp", + "direction": "desc" + } + } + } + }, + "timeframe": "last30days", + "variables": [ + "{{variables.[Service]}}" + ], + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + } + } + }, + { + "i": "e41c16ee-13fa-47be-9082-2c5958520c66", + "x": 2, + "y": 12, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open and Breached Tickets", + "description": "Customer tickets raised against this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "New" + }, + { + "column": "status", + "operation": "equals", + "value": "In Progress" + }, + { + "column": "slaState", + "operation": "equals", + "value": "error" + } + ] + }, + "sort": { + "by": [ + [ + "openedAt", + "desc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "ticketId", + "openedAt", + "customer", + "priority", + "category", + "service", + "team", + "status", + "slaState" + ], + "hiddenColumns": [ + "customerId", + "plan", + "resolutionMinutes", + "slaTargetMinutes", + "slaBreached" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "ticketId", + "title": "Ticket", + "renderer": { + "type": "text" + } + }, + { + "field": "openedAt", + "title": "Opened", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "customer", + "title": "Customer", + "renderer": { + "type": "text" + } + }, + { + "field": "priority", + "title": "Priority", + "renderer": { + "type": "badge", + "colors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + }, + { + "field": "category", + "title": "Category", + "renderer": { + "type": "text" + } + }, + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Assigned team", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "New": "#206bc4", + "In Progress": "#f59f00" + }, + "defaultColor": "#9ba3af" + } + }, + { + "field": "slaState", + "title": "SLA", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "slaState", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "openedAt", + "direction": "desc" + } + } + } + }, + "timeframe": "last30days", + "variables": [ + "{{variables.[Service]}}" + ], + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + } + } + }, + { + "i": "1579fef9-8e13-4ed2-b63e-6f260b57f9d5", + "x": 0, + "y": 16, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Cost by Category", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "date", + "byDay" + ], + [ + "category", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + }, + "sort": { + "by": [ + [ + "date_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "variables": [ + "{{variables.[Service]}}" + ], + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date_byDay", + "yAxisData": [ + "costUsd_sum" + ], + "xAxisGroup": "category_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "stacked", + "showTotals": false, + "showValue": false, + "grouping": true, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "date_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Cost" + }, + "series": { + "field": "category_uniqueValues", + "type": "nominal", + "title": "Category" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom" + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/business.dash.json b/plugins/SampleData/v3/defaultContent/business.dash.json new file mode 100644 index 00000000..f42ea378 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/business.dash.json @@ -0,0 +1,609 @@ +{ + "name": "Business Metrics", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "1601c8ab-369b-4f91-96d4-cf29da44cdb0", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Orders", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "orders" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "orders_sum", + "comparisonColumn": "none", + "label": "Orders" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "orders_sum", + "type": "quantitative", + "title": "Orders" + } + ] + } + } + } + } + }, + { + "i": "02a4afd5-2753-4c3b-9acd-b8c438d54eb9", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Order Value", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "orderValueUsd" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "orderValueUsd_sum", + "comparisonColumn": "none", + "label": "Total order value" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "orderValueUsd_sum", + "type": "quantitative", + "title": "Total order value" + } + ] + } + } + } + } + }, + { + "i": "1411237f-6b74-4f9a-a8a3-273cb20e637e", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Conversion Rate", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(100.0 * SUM(\"orders\") / NULLIF(SUM(\"visitors\"), 0), 2) AS \"conversionRatePercent\" FROM \"dataset\"", + "metadata": [ + { + "name": "conversionRatePercent", + "displayName": "Conversion rate", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "conversionRatePercent", + "comparisonColumn": "none", + "label": "Visitors who placed an order" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "conversionRatePercent", + "type": "quantitative", + "title": "Visitors who placed an order" + } + ] + } + } + } + } + }, + { + "i": "afdd5561-d3b9-4210-ac4c-b7c6daf68383", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Signups", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "signups" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "signups_sum", + "comparisonColumn": "none", + "label": "New signups" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "signups_sum", + "type": "quantitative", + "title": "New signups" + } + ] + } + } + } + } + }, + { + "i": "29ebed04-6f36-4859-a65b-e8a971fb2823", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Conversion Funnel", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[conversionFunnel]}}", + "name": "conversionFunnel", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "order", + "asc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "funnel", + "encoding": { + "label": { + "field": "stage", + "type": "nominal", + "title": "Stage" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Count" + } + }, + "style": { + "funnelSort": "none", + "funnelLabelPosition": "inside" + } + } + } + } + }, + { + "i": "8ccad5bd-6c8b-42ae-a973-67df2a635718", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Orders and Visitors", + "description": "Orders (left axis) and visitors (right axis)", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "orders" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "orders", + "type": "quantitative", + "title": "Orders" + } + }, + "style": { + "cartesianY2AxisField": "visitors", + "cartesianY2AxisTitle": "Visitors", + "cartesianSymbol": "none", + "legendPosition": "bottom" + } + } + } + } + }, + { + "i": "2dba2faf-ec73-4320-a21b-aa359755c454", + "x": 0, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Revenue per Day", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "timestamp", + "byDay" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "orderValueUsd" + ] + } + ] + }, + "sort": { + "by": [ + [ + "timestamp_byDay", + "asc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "timestamp_byDay", + "yAxisData": [ + "orderValueUsd_sum" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "timestamp_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "orderValueUsd_sum", + "type": "quantitative", + "title": "Order value" + } + }, + "style": { + "cartesianBarBorderRadius": 4, + "colors": [ + "#206bc4" + ] + } + } + } + } + }, + { + "i": "3d87d3be-a412-4ea7-926d-4e0fa7848399", + "x": 2, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "MRR Trend", + "description": "Monthly recurring revenue over the last 30 days", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "mrrUsd" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "mrrUsd", + "type": "quantitative", + "title": "MRR" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "cartesianYAxisRangeMode": "fit", + "colors": [ + "#7048e8" + ] + } + } + } + } + }, + { + "i": "a63c5bf7-9836-4232-bcce-e0722a5e5338", + "x": 0, + "y": 9, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Orders in the Last 24 Hours", + "description": "Orders per 15 minutes; failed payments turn into lost orders", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last24hours", + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "orders" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "orders", + "type": "quantitative", + "title": "Orders" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "colors": [ + "#2fb344" + ] + }, + "markLines": [ + { + "type": "average", + "label": "Average" + } + ] + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/cloudCost.dash.json b/plugins/SampleData/v3/defaultContent/cloudCost.dash.json new file mode 100644 index 00000000..b5ba8b11 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/cloudCost.dash.json @@ -0,0 +1,862 @@ +{ + "name": "Cloud Cost", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "61521a75-d92f-4245-9e54-009bbcfbbdb8", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Spend This Month", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "thisMonth", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "costUsd_sum", + "comparisonColumn": "none", + "label": "Month-to-date cloud spend" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Month-to-date cloud spend" + } + ] + } + } + } + } + }, + { + "i": "5ccaff0b-5298-4a94-a49a-961bc3d52ffe", + "x": 1, + "y": 0, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Spend vs Budget This Month", + "description": "Month-to-date spend against the month-to-date budget", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "thisMonth", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + }, + { + "type": "sum", + "names": [ + "budgetUsd" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bullet", + "encoding": { + "value": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Spend" + }, + "target": { + "field": "budgetUsd_sum", + "type": "quantitative", + "title": "Budget" + } + }, + "style": { + "bulletFillColor": "#206bc4", + "gaugeTargetColor": "#d63939" + } + } + } + } + }, + { + "i": "80ddef55-3509-4c48-8892-c8f83f01b0d4", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Services Over Budget", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "thisMonth", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "sql": "SELECT COUNT(*) AS \"overBudget\" FROM (SELECT \"service\", SUM(\"costUsd\") AS \"cost\", SUM(\"budgetUsd\") AS \"budget\" FROM \"dataset\" GROUP BY \"service\") WHERE \"cost\" > \"budget\"", + "metadata": [ + { + "name": "overBudget", + "displayName": "Services over budget", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "overBudget", + "comparisonColumn": "none", + "label": "Services over budget this month" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "overBudget", + "type": "quantitative", + "title": "Services over budget this month" + } + ] + } + } + } + } + }, + { + "i": "1cff0bf2-188d-44f7-a892-54c09f370e16", + "x": 0, + "y": 2, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Spend vs Budget", + "description": "Total daily cloud spend against the daily budget", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "date", + "byDay" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + }, + { + "type": "sum", + "names": [ + "budgetUsd" + ] + } + ] + }, + "sort": { + "by": [ + [ + "date_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date_byDay", + "yAxisColumn": [ + "costUsd_sum", + "budgetUsd_sum" + ], + "seriesColumn": "none", + "showLegend": false, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "date_byDay", + "type": "temporal", + "title": "Day" + } + }, + "style": { + "cartesianAreaGradient": true, + "legendPosition": "bottom", + "cartesianSymbol": "none", + "colors": [ + "#206bc4", + "#d63939" + ] + } + } + } + } + }, + { + "i": "529e20f6-bc02-4ea5-8a23-f2819eefe233", + "x": 3, + "y": 2, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cost by Category", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "category", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "costUsd_sum", + "labelColumn": "category_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "category_uniqueValues", + "type": "nominal", + "title": "Category" + }, + "value": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Cost" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "pieLegendStyle": "table" + } + } + } + } + }, + { + "i": "2c6cbf94-0193-4270-b014-ef2f5f4bca35", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cost by Service", + "description": "Cloud spend per service, grouped by owning team", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "team", + "uniqueValues" + ], + [ + "service", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "treemap", + "encoding": { + "label": { + "field": "service_uniqueValues", + "type": "nominal", + "title": "Service" + }, + "value": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Cost" + }, + "parent": { + "field": "team_uniqueValues", + "type": "nominal", + "title": "Team" + } + }, + "style": { + "treemapGap": 2 + } + } + } + } + }, + { + "i": "daf09d7e-466d-4f50-8258-93dea38795a5", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cost by Team", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "team", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + }, + "sort": { + "by": [ + [ + "costUsd_sum", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "team_uniqueValues", + "yAxisData": [ + "costUsd_sum" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "team_uniqueValues", + "type": "nominal", + "title": "Team" + }, + "y": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Cost" + } + }, + "style": { + "cartesianHorizontal": true, + "cartesianBarBorderRadius": 4, + "colors": [ + "#206bc4" + ] + } + } + } + } + }, + { + "i": "7ed3c86b-ee88-4bd0-b921-e7398fe6311b", + "x": 0, + "y": 10, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Compute Cost by Service", + "description": "Compute spend per service per day; look for services that step up", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "category", + "operation": "equals", + "value": "Compute" + } + ] + }, + "group": { + "by": [ + [ + "date", + "byDay" + ], + [ + "service", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + } + ] + }, + "sort": { + "by": [ + [ + "date_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date_byDay", + "yAxisColumn": [ + "costUsd_sum" + ], + "seriesColumn": "service_uniqueValues", + "showLegend": true, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "date_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Compute cost" + }, + "series": { + "field": "service_uniqueValues", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "legendPosition": "right", + "cartesianSymbol": "none" + } + } + } + } + }, + { + "i": "afd5ea56-402a-41ea-8a03-83dd80c569e8", + "x": 0, + "y": 13, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Budget Status by Service", + "description": "Month-to-date spend against budget for each service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "thisMonth", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"service\", \"team\", SUM(\"costUsd\") AS \"costUsd\", SUM(\"budgetUsd\") AS \"budgetUsd\", ROUND(100.0 * SUM(\"costUsd\") / NULLIF(SUM(\"budgetUsd\"), 0), 1) AS \"budgetUsedPercent\", CASE WHEN SUM(\"costUsd\") > SUM(\"budgetUsd\") THEN 'error' WHEN SUM(\"costUsd\") > 0.95 * SUM(\"budgetUsd\") THEN 'warning' ELSE 'success' END AS \"state\" FROM \"dataset\" GROUP BY \"service\", \"team\" ORDER BY \"budgetUsedPercent\" DESC", + "metadata": [ + { + "name": "service", + "displayName": "Service" + }, + { + "name": "team", + "displayName": "Team" + }, + { + "name": "costUsd", + "displayName": "Spend", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "budgetUsd", + "displayName": "Budget", + "shape": [ + "usd", + { + "decimalPlaces": 0 + } + ] + }, + { + "name": "budgetUsedPercent", + "displayName": "Budget used", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "service", + "team", + "costUsd", + "budgetUsd", + "budgetUsedPercent", + "state" + ], + "hiddenColumns": [] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "costUsd", + "title": "Spend", + "align": "right", + "renderer": { + "type": "number", + "decimals": 0, + "prefix": "$" + } + }, + { + "field": "budgetUsd", + "title": "Budget", + "align": "right", + "renderer": { + "type": "number", + "decimals": 0, + "prefix": "$" + } + }, + { + "field": "budgetUsedPercent", + "title": "Budget used", + "renderer": { + "type": "progress", + "max": 100 + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + } + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "state", + "column": "state", + "aggregation": "top", + "condition": { + "columns": [ + "state" + ] + }, + "frequency": 180 + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/customers.dash.json b/plugins/SampleData/v3/defaultContent/customers.dash.json new file mode 100644 index 00000000..32be5abf --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/customers.dash.json @@ -0,0 +1,624 @@ +{ + "name": "Customers & SLAs", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "ce6f6b96-8982-4ba1-8a4f-f1ae63ca8979", + "x": 0, + "y": 0, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Summary", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + }, + { + "type": "sum", + "names": [ + "mrrUsd" + ] + }, + { + "type": "mean", + "names": [ + "csat" + ] + }, + { + "type": "sum", + "names": [ + "openTickets" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Customers" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Customers" + }, + { + "field": "mrrUsd_sum", + "type": "quantitative", + "title": "MRR from these customers" + }, + { + "field": "csat_mean", + "type": "quantitative", + "title": "Average CSAT (out of 5)" + }, + { + "field": "openTickets_sum", + "type": "quantitative", + "title": "Open tickets" + } + ] + }, + "style": { + "scalarColumns": 4 + } + } + } + } + }, + { + "i": "5df4d82b-ed2e-4d50-8435-335be8fafe35", + "x": 0, + "y": 2, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Locations", + "description": "Customer locations, coloured by SLA state", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "geoMap", + "encoding": { + "latitude": { + "field": "latitude", + "type": "quantitative" + }, + "longitude": { + "field": "longitude", + "type": "quantitative" + }, + "label": { + "field": "customer", + "type": "nominal", + "title": "Customer" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "SLA state" + }, + "details": [ + { + "field": "plan", + "type": "nominal", + "title": "Plan" + }, + { + "field": "city", + "type": "nominal", + "title": "City" + }, + { + "field": "mrrUsd", + "type": "quantitative", + "title": "MRR" + }, + { + "field": "slaActualPercent", + "type": "quantitative", + "title": "SLA (30 days)" + }, + { + "field": "openTickets", + "type": "quantitative", + "title": "Open tickets" + } + ] + }, + "style": { + "geoMapBase": "world", + "geoMapSymbol": "pin", + "geoMapPointSize": 16, + "geoMapZoomControls": true, + "geoMapWheelZoom": false, + "showLabels": false + } + } + } + } + }, + { + "i": "86c59788-a06f-4376-836f-6a3283c24524", + "x": 2, + "y": 2, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer SLA Health", + "description": "SLA state of each customer: red is below target, amber is close to target or low CSAT", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "customer", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "customer", + "type": "nominal", + "title": "Customer" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "SLA state" + }, + "group": { + "field": "plan", + "type": "nominal", + "title": "Plan" + }, + "value": { + "field": "slaActualPercent", + "type": "quantitative", + "title": "SLA (30 days)" + } + }, + "style": { + "statusGridColumnsPerRow": 4 + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "state", + "column": "state", + "aggregation": "top", + "condition": { + "columns": [ + "state" + ] + }, + "frequency": 15 + } + } + }, + { + "i": "258a2040-ce31-422d-bf63-7ecfe626c6b6", + "x": 0, + "y": 7, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Satisfaction", + "description": "CSAT score out of 5 for each customer", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "csat", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "csat", + "type": "quantitative", + "title": "CSAT" + }, + "label": { + "field": "customer", + "type": "nominal", + "title": "Customer" + } + }, + "style": { + "ringGridMax": 5, + "ringGridColumnsPerRow": 5, + "ringGridThresholds": [ + { + "to": 0.8, + "color": "#d63939" + }, + { + "to": 0.88, + "color": "#f59f00" + }, + { + "to": 1, + "color": "#2fb344" + } + ] + } + } + } + } + }, + { + "i": "7481f385-a1e5-4ca7-9486-7b2a03af134f", + "x": 2, + "y": 7, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "MRR vs Satisfaction", + "description": "Each bubble is a customer, sized by seats", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scatter", + "encoding": { + "x": { + "field": "csat", + "type": "quantitative", + "title": "CSAT" + }, + "y": { + "field": "mrrUsd", + "type": "quantitative", + "title": "MRR" + }, + "size": { + "field": "seats", + "type": "quantitative", + "title": "Seats" + }, + "series": { + "field": "plan", + "type": "nominal", + "title": "Plan" + } + }, + "style": { + "legendPosition": "bottom", + "cartesianShowXAxisTitle": true, + "cartesianShowYAxisTitle": true, + "cartesianTooltipTemplate": "{{customer}}: CSAT {{csat}}, MRR {{mrrUsd}}" + } + } + } + } + }, + { + "i": "ef8aabfd-b943-4903-9807-bc35d750a65c", + "x": 0, + "y": 11, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Accounts", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "customer", + "plan", + "city", + "mrrUsd", + "seats", + "slaTargetPercent", + "slaActualPercent", + "openTickets", + "csat", + "renewalDate", + "state" + ], + "hiddenColumns": [ + "customerId", + "country", + "latitude", + "longitude" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "customer", + "title": "Customer", + "renderer": { + "type": "text" + } + }, + { + "field": "plan", + "title": "Plan", + "renderer": { + "type": "badge", + "colors": { + "Enterprise": "#7048e8", + "Business": "#206bc4", + "Starter": "#9ba3af" + } + } + }, + { + "field": "city", + "title": "City", + "renderer": { + "type": "text" + } + }, + { + "field": "mrrUsd", + "title": "MRR", + "renderer": { + "type": "bar", + "color": "#206bc4" + } + }, + { + "field": "seats", + "title": "Seats", + "align": "right", + "renderer": { + "type": "number" + } + }, + { + "field": "slaTargetPercent", + "title": "SLA target", + "align": "right", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": "%" + } + }, + { + "field": "slaActualPercent", + "title": "SLA (30 days)", + "align": "right", + "renderer": { + "type": "number", + "decimals": 3, + "suffix": "%" + } + }, + { + "field": "openTickets", + "title": "Open tickets", + "align": "right", + "renderer": { + "type": "number" + } + }, + { + "field": "csat", + "title": "CSAT", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "thresholds": [ + { + "to": 4.0, + "color": "#d63939" + }, + { + "to": 4.4, + "color": "#f59f00" + }, + { + "to": 5, + "color": "#2fb344" + } + ] + } + }, + { + "field": "renewalDate", + "title": "Renewal", + "renderer": { + "type": "date" + } + }, + { + "field": "state", + "title": "SLA state", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "mrrUsd", + "direction": "desc" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/devops.dash.json b/plugins/SampleData/v3/defaultContent/devops.dash.json new file mode 100644 index 00000000..ec1c6cb0 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/devops.dash.json @@ -0,0 +1,750 @@ +{ + "name": "DevOps & DORA", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "6a756969-72d5-4eec-884f-f67e23a17fd7", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment Frequency", + "description": "Average production deployments per day", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(COUNT(*) / (DATE_DIFF('day', MIN(\"timestamp\"), MAX(\"timestamp\")) + 1), 1) AS \"deploysPerDay\" FROM \"dataset\"", + "metadata": [ + { + "name": "deploysPerDay", + "displayName": "Deployments per day", + "shape": [ + "number", + { + "decimalPlaces": 1, + "thousandsSeparator": true + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "deploysPerDay", + "comparisonColumn": "none", + "label": "Deployments per day" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "deploysPerDay", + "type": "quantitative", + "title": "Deployments per day" + } + ] + } + } + } + } + }, + { + "i": "f17d4c8f-ca4f-492a-8708-e8d1ba558d1f", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Lead Time for Changes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "leadTimeHours" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "leadTimeHours_mean", + "comparisonColumn": "none", + "label": "Average hours from commit to production" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "leadTimeHours_mean", + "type": "quantitative", + "title": "Average hours from commit to production" + } + ] + } + } + } + } + }, + { + "i": "731950d2-27a1-4e9b-9991-7b75ed716bf8", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Change Failure Rate", + "description": "Share of deployments that caused a failure in production", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(100.0 * SUM(CASE WHEN \"changeFailure\" THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 1) AS \"changeFailureRate\" FROM \"dataset\"", + "metadata": [ + { + "name": "changeFailureRate", + "displayName": "Change failure rate", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "changeFailureRate", + "comparisonColumn": "none", + "label": "Deployments that caused a failure" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "changeFailureRate", + "type": "quantitative", + "title": "Deployments that caused a failure" + } + ] + } + } + } + } + }, + { + "i": "2e36172e-98b9-46e1-8b6f-eeb6a5c2889d", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Time to Restore", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "timeToRestoreMinutes" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "timeToRestoreMinutes_mean", + "comparisonColumn": "none", + "label": "Average time to restore after a failed change" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "timeToRestoreMinutes_mean", + "type": "quantitative", + "title": "Average time to restore after a failed change" + } + ] + } + } + } + } + }, + { + "i": "76f341fb-58f3-4a60-a3b2-b6fd6aa4d306", + "x": 0, + "y": 2, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployments per Day by Team", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "timestamp", + "byDay" + ], + [ + "team", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "timestamp_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "timestamp_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "team_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "stacked", + "showTotals": false, + "showValue": false, + "grouping": true, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "timestamp_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Deployments" + }, + "series": { + "field": "team_uniqueValues", + "type": "nominal", + "title": "Team" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom", + "cartesianBarBorderRadius": 2 + } + } + } + } + }, + { + "i": "a70d5885-9321-49ea-826b-26c5538df350", + "x": 3, + "y": 2, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment Outcomes", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "status", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "status_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "status_uniqueValues", + "type": "nominal", + "title": "Status" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Deployments" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "pieLegendStyle": "table", + "mappedColors": { + "Succeeded": "#2fb344", + "Failed": "#d63939" + } + } + } + } + } + }, + { + "i": "2ad9307e-6ff4-4725-8d77-1918b13f5106", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Lead Time Distribution", + "description": "How many deployments took each length of time from commit to production", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "histogram", + "encoding": { + "x": { + "field": "leadTimeHours", + "type": "quantitative", + "title": "Lead time (hours)" + } + }, + "style": { + "histogramBins": 12, + "colors": [ + "#206bc4" + ] + } + } + } + } + }, + { + "i": "3b9df9e5-74a3-4f73-905e-500f0079bc26", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pipeline Health", + "description": "Worst deployment outcome per pipeline per day; amber means a deployment caused a failure", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "healthHistory", + "encoding": { + "time": { + "field": "timestamp", + "type": "temporal", + "title": "Finished" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "label": { + "field": "pipeline", + "type": "nominal", + "title": "Pipeline" + } + }, + "style": { + "historySlots": 30, + "statusGridLabelWidth": 140, + "historyBarHeight": 18 + } + } + } + } + }, + { + "i": "855ed3a9-3af9-4d9d-b2bf-2a3b55bf5672", + "x": 0, + "y": 8, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Deployments", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "timestamp", + "desc" + ] + ], + "top": 50 + } + }, + "scope": { + "scope": "{{scopes.[Pipelines]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "timestamp", + "pipeline", + "service", + "team", + "version", + "status", + "changeFailure", + "state", + "durationMinutes", + "leadTimeHours" + ], + "hiddenColumns": [ + "deploymentId", + "pipelineId", + "tool", + "timeToRestoreMinutes" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "timestamp", + "title": "Finished", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "pipeline", + "title": "Pipeline", + "renderer": { + "type": "text" + } + }, + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "version", + "title": "Version", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "Succeeded": "#2fb344", + "Failed": "#d63939" + } + } + }, + { + "field": "changeFailure", + "title": "Caused a failure", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No", + "trueColor": "#d63939", + "falseColor": "#9ba3af" + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + }, + { + "field": "durationMinutes", + "title": "Duration", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " min" + } + }, + { + "field": "leadTimeHours", + "title": "Lead time", + "align": "right", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " h" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "timestamp", + "direction": "desc" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/executiveOverview.dash.json b/plugins/SampleData/v3/defaultContent/executiveOverview.dash.json new file mode 100644 index 00000000..f3287819 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/executiveOverview.dash.json @@ -0,0 +1,656 @@ +{ + "name": "Executive Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "221cdefc-e365-473d-b278-ce30d7179ad6", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Availability (30 Days)", + "description": "Average 30-day availability across all services", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "availabilityPercent" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "availabilityPercent_mean", + "comparisonColumn": "none", + "label": "Average availability across all services" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "availabilityPercent_mean", + "type": "quantitative", + "title": "Average availability across all services" + } + ] + } + } + }, + "kpi": { + "name": "Availability (30 Days)" + } + } + }, + { + "i": "ca0c0f0a-eab6-40aa-864d-3479cbc130a4", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open P1/P2 Tickets", + "description": "P1 and P2 tickets that are new or in progress", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last7days", + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "sql": "SELECT COUNT(*) AS \"openHighPriority\" FROM \"dataset\" WHERE \"status\" IN ('New', 'In Progress') AND \"priority\" IN ('P1', 'P2')", + "metadata": [ + { + "name": "openHighPriority", + "displayName": "Open P1/P2 tickets", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "openHighPriority", + "comparisonColumn": "none", + "label": "Open P1 and P2 tickets" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "openHighPriority", + "type": "quantitative", + "title": "Open P1 and P2 tickets" + } + ] + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "threshold", + "aggregation": "max", + "groupBy": "__group_by_none__", + "frequency": 60, + "condition": { + "columns": [ + "openHighPriority" + ], + "logic": { + "if": [ + { + ">": [ + { + "var": "max" + }, + 0 + ] + }, + "warning", + "success" + ] + } + } + }, + "kpi": { + "name": "Open P1/P2 Tickets" + } + } + }, + { + "i": "1917c6df-5a2c-48e6-a3ef-295a9921deaa", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Monthly Recurring Revenue", + "description": "Current monthly recurring revenue", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[businessMetrics]}}", + "name": "businessMetrics", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "max", + "names": [ + "mrrUsd" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "mrrUsd_max", + "comparisonColumn": "none", + "label": "MRR" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "mrrUsd_max", + "type": "quantitative", + "title": "MRR" + } + ] + } + } + }, + "kpi": { + "name": "Monthly Recurring Revenue" + } + } + }, + { + "i": "ad0c5979-c127-481d-8fa8-d1fea56e9223", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cloud Spend vs Budget", + "description": "Cloud spend so far this month against the month-to-date budget", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "thisMonth", + "dataStream": { + "id": "{{dataStreams.[cloudCost]}}", + "name": "cloudCost", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": [ + "costUsd" + ] + }, + { + "type": "sum", + "names": [ + "budgetUsd" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bullet", + "encoding": { + "value": { + "field": "costUsd_sum", + "type": "quantitative", + "title": "Spend" + }, + "target": { + "field": "budgetUsd_sum", + "type": "quantitative", + "title": "Budget" + } + }, + "style": { + "bulletFillColor": "#206bc4", + "gaugeTargetColor": "#d63939" + } + } + } + } + }, + { + "i": "d4659b98-8360-4b9f-9087-1d469b95b15a", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Health", + "description": "Current health of every service, grouped by owning team", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "service", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "service", + "type": "nominal", + "title": "Service" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "group": { + "field": "team", + "type": "nominal", + "title": "Team" + }, + "value": { + "field": "latencyP95Ms", + "type": "quantitative", + "title": "Latency p95" + } + }, + "style": { + "statusGridColumnsPerRow": 4 + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "state", + "column": "state", + "aggregation": "top", + "condition": { + "columns": [ + "state" + ] + }, + "frequency": 15 + } + } + }, + { + "i": "601721d8-6915-4627-807e-ae668ba2cec5", + "x": 2, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer SLAs", + "description": "Customer locations, coloured by SLA state", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[customerSla]}}", + "name": "customerSla", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "geoMap", + "encoding": { + "latitude": { + "field": "latitude", + "type": "quantitative" + }, + "longitude": { + "field": "longitude", + "type": "quantitative" + }, + "label": { + "field": "customer", + "type": "nominal", + "title": "Customer" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "SLA state" + }, + "details": [ + { + "field": "plan", + "type": "nominal", + "title": "Plan" + }, + { + "field": "city", + "type": "nominal", + "title": "City" + }, + { + "field": "slaActualPercent", + "type": "quantitative", + "title": "SLA (30 days)" + }, + { + "field": "openTickets", + "type": "quantitative", + "title": "Open tickets" + } + ] + }, + "style": { + "geoMapBase": "world", + "geoMapPointSize": 12, + "geoMapZoomControls": true, + "geoMapWheelZoom": false, + "showLabels": false + } + } + } + } + }, + { + "i": "2adc884f-f9b8-48f8-a0bc-743a99cb507a", + "x": 0, + "y": 6, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checkout and Payments Latency", + "description": "p95 latency of Checkout API and Payments API; above 500 ms is degraded", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "service", + "operation": "equals", + "value": "Checkout API" + }, + { + "column": "service", + "operation": "equals", + "value": "Payments API" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "latencyP95Ms" + ], + "seriesColumn": "service", + "showLegend": true, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "latencyP95Ms", + "type": "quantitative", + "title": "Latency p95" + }, + "series": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "legendPosition": "bottom", + "cartesianSymbol": "none" + }, + "markLines": [ + { + "value": 500, + "label": "500 ms", + "color": "#d63939" + } + ], + "markAreas": [ + { + "from": 500, + "to": "max", + "label": "Degraded", + "color": "rgba(214,57,57,0.08)" + } + ] + } + } + } + }, + { + "i": "bcba9c20-3600-4bd0-b354-bca15d8dbb9e", + "x": 3, + "y": 6, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Payments API Health", + "description": "Health of Payments API over the dashboard timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "service", + "operation": "equals", + "value": "Payments API" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "healthHistory", + "encoding": { + "time": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + } + }, + "style": { + "historySlots": 48, + "historyBarHeight": 40 + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/manifest.json b/plugins/SampleData/v3/defaultContent/manifest.json new file mode 100644 index 00000000..1ad1fc87 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/manifest.json @@ -0,0 +1,40 @@ +{ + "items": [ + { + "name": "executiveOverview", + "type": "dashboard" + }, + { + "name": "serviceHealth", + "type": "dashboard" + }, + { + "name": "slos", + "type": "dashboard" + }, + { + "name": "devops", + "type": "dashboard" + }, + { + "name": "cloudCost", + "type": "dashboard" + }, + { + "name": "serviceDesk", + "type": "dashboard" + }, + { + "name": "customers", + "type": "dashboard" + }, + { + "name": "business", + "type": "dashboard" + }, + { + "name": "Perspectives", + "type": "folder" + } + ] +} diff --git a/plugins/SampleData/v3/defaultContent/scopes.json b/plugins/SampleData/v3/defaultContent/scopes.json new file mode 100644 index 00000000..07723012 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/scopes.json @@ -0,0 +1,70 @@ +[ + { + "name": "Services", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "variable": { + "name": "Service", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Hosts", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Host" + ] + } + }, + "variable": { + "name": "Host", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Pipelines", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Pipeline" + ] + } + }, + "variable": { + "name": "Pipeline", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Customers", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Customer" + ] + } + }, + "variable": { + "name": "Customer", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/SampleData/v3/defaultContent/serviceDesk.dash.json b/plugins/SampleData/v3/defaultContent/serviceDesk.dash.json new file mode 100644 index 00000000..14b22d95 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/serviceDesk.dash.json @@ -0,0 +1,964 @@ +{ + "name": "Service Desk", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "83fa1d60-1f3e-4200-9f5e-c6e96486b721", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open Tickets", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "notequals", + "value": "Resolved" + }, + { + "column": "status", + "operation": "notequals", + "value": "Closed" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "New or in progress" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "New or in progress" + } + ] + } + } + } + } + }, + { + "i": "21c583ba-18f4-49cb-a28f-6579137058e7", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open P1 Tickets", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "notequals", + "value": "Resolved" + }, + { + "column": "status", + "operation": "notequals", + "value": "Closed" + }, + { + "column": "priority", + "operation": "equals", + "value": "P1" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Open priority 1 tickets" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Open priority 1 tickets" + } + ] + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "threshold", + "aggregation": "count", + "groupBy": "__group_by_none__", + "frequency": 60, + "condition": { + "columns": [], + "logic": { + "if": [ + { + ">": [ + { + "var": "count" + }, + 0 + ] + }, + "error", + "success" + ] + } + } + } + } + }, + { + "i": "d23fa31c-2ee6-40ce-b1d3-1320c50019a3", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "SLA Breaches", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "slaState", + "operation": "equals", + "value": "error" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Tickets that breached their SLA" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Tickets that breached their SLA" + } + ] + } + } + } + } + }, + { + "i": "37643bf5-f4f0-4b43-91eb-8b5c9d59b209", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Average Resolution Time", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "resolutionMinutes" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "resolutionMinutes_mean", + "comparisonColumn": "none", + "label": "Resolved tickets" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "resolutionMinutes_mean", + "type": "quantitative", + "title": "Resolved tickets" + } + ] + } + } + } + } + }, + { + "i": "35d30824-de6a-4ff6-bb05-aa3fc0f6c436", + "x": 0, + "y": 2, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets by Day and Hour", + "description": "When tickets are opened, by day and hour (UTC)", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "sql": "SELECT STRFTIME(\"d\", '%a %d %b') AS \"day\", \"h\" AS \"hour\", COUNT(*) AS \"tickets\" FROM (SELECT DATE_TRUNC('day', \"openedAt\") AS \"d\", STRFTIME(\"openedAt\", '%H:00') AS \"h\" FROM \"dataset\") GROUP BY \"d\", \"h\" ORDER BY \"d\", \"h\"", + "metadata": [ + { + "name": "day", + "displayName": "Day" + }, + { + "name": "hour", + "displayName": "Hour (UTC)" + }, + { + "name": "tickets", + "displayName": "Tickets", + "shape": [ + "number", + { + "decimalPlaces": 0, + "thousandsSeparator": true + } + ] + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "heatmap", + "encoding": { + "x": { + "field": "day", + "type": "nominal", + "title": "Day" + }, + "y": { + "field": "hour", + "type": "nominal", + "title": "Hour (UTC)" + }, + "color": { + "field": "tickets", + "type": "quantitative", + "title": "Tickets" + } + }, + "style": { + "colorScheme": "squaredUpBlue", + "heatmapCellBorderWidth": 1 + } + } + } + } + }, + { + "i": "b5c03a0e-4311-4800-bf11-7648c767457a", + "x": 2, + "y": 2, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets by Priority", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "priority", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "priority_uniqueValues", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "priority_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "priority_uniqueValues", + "type": "nominal", + "title": "Priority" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Tickets" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "pieLegendStyle": "table", + "mappedColors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + } + } + } + }, + { + "i": "86a7e5ca-12a4-44ff-9569-a1de0a007d6e", + "x": 3, + "y": 2, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets by Category", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "category", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "category_uniqueValues", + "yAxisData": [ + "count" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "category_uniqueValues", + "type": "nominal", + "title": "Category" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Tickets" + } + }, + "style": { + "cartesianHorizontal": true, + "cartesianBarBorderRadius": 4, + "colors": [ + "#206bc4" + ], + "cartesianColorRules": [ + { + "label": "Payments Failing", + "color": "#d63939" + } + ] + } + } + } + } + }, + { + "i": "fe6a8e68-e3d4-447c-a320-7b128413ceb5", + "x": 0, + "y": 6, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open and Breached Tickets", + "description": "Tickets that are still open or have breached their SLA", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "New" + }, + { + "column": "status", + "operation": "equals", + "value": "In Progress" + }, + { + "column": "slaState", + "operation": "equals", + "value": "error" + } + ] + }, + "sort": { + "by": [ + [ + "openedAt", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "ticketId", + "openedAt", + "customer", + "priority", + "category", + "service", + "team", + "status", + "slaState" + ], + "hiddenColumns": [ + "customerId", + "plan", + "resolutionMinutes", + "slaTargetMinutes", + "slaBreached" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "ticketId", + "title": "Ticket", + "renderer": { + "type": "text" + } + }, + { + "field": "openedAt", + "title": "Opened", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "customer", + "title": "Customer", + "renderer": { + "type": "text" + } + }, + { + "field": "priority", + "title": "Priority", + "renderer": { + "type": "badge", + "colors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + }, + { + "field": "category", + "title": "Category", + "renderer": { + "type": "text" + } + }, + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "team", + "title": "Assigned team", + "renderer": { + "type": "text" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "badge", + "colors": { + "New": "#206bc4", + "In Progress": "#f59f00" + }, + "defaultColor": "#9ba3af" + } + }, + { + "field": "slaState", + "title": "SLA", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "slaState", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "openedAt", + "direction": "desc" + } + } + } + } + } + }, + { + "i": "a3988f31-ab2a-472f-8878-5575456b5ee3", + "x": 0, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Resolution Time Distribution", + "description": "How long resolved tickets took to resolve", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "resolutionMinutes", + "operation": "notempty", + "value": "" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "histogram", + "encoding": { + "x": { + "field": "resolutionMinutes", + "type": "quantitative", + "title": "Resolution time (minutes)" + } + }, + "style": { + "histogramBins": 15, + "colors": [ + "#206bc4" + ] + } + } + } + } + }, + { + "i": "8b5817a9-773f-483f-99d1-1d3afc125228", + "x": 2, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tickets Opened per Day", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[tickets]}}", + "name": "tickets", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "openedAt", + "byDay" + ], + [ + "priority", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "openedAt_byDay", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Customers]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "openedAt_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "priority_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "stacked", + "showTotals": false, + "showValue": false, + "grouping": true, + "range": { + "type": "auto" + } + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "openedAt_byDay", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Tickets" + }, + "series": { + "field": "priority_uniqueValues", + "type": "nominal", + "title": "Priority" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom", + "mappedColors": { + "P1": "#d63939", + "P2": "#f59f00", + "P3": "#206bc4", + "P4": "#9ba3af" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/serviceHealth.dash.json b/plugins/SampleData/v3/defaultContent/serviceHealth.dash.json new file mode 100644 index 00000000..2876a001 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/serviceHealth.dash.json @@ -0,0 +1,666 @@ +{ + "name": "Service Health", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "f292fa91-b626-453a-acce-7b3427adbade", + "x": 0, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "All Services", + "description": "Current health of every service, grouped by tier", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "service", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "service", + "type": "nominal", + "title": "Service" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "group": { + "field": "tier", + "type": "nominal", + "title": "Tier" + }, + "value": { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + } + }, + "style": { + "statusGridColumnsPerRow": 6 + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "state", + "column": "state", + "aggregation": "top", + "condition": { + "columns": [ + "state" + ] + }, + "frequency": 15 + } + } + }, + { + "i": "e0eaaad6-4ad5-4c9c-8904-c4ca5bcd0982", + "x": 3, + "y": 0, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Services by State", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "state", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "state_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "state_uniqueValues", + "type": "nominal", + "title": "State" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Services" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "pieLegendStyle": "table", + "mappedColors": { + "success": "#2fb344", + "warning": "#f59f00", + "error": "#d63939" + } + } + } + } + } + }, + { + "i": "3295eaac-74eb-4bca-ab45-92d2d12476d1", + "x": 0, + "y": 3, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Dependencies", + "description": "Requests per minute flowing between services", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceDependencies]}}", + "name": "serviceDependencies", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "sankey", + "encoding": { + "source": { + "field": "source", + "type": "nominal", + "title": "From" + }, + "target": { + "field": "target", + "type": "nominal", + "title": "To" + }, + "value": { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + } + } + } + } + } + }, + { + "i": "781333fb-d365-4d6f-bfb4-9e557b9c42a2", + "x": 2, + "y": 3, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "p95 Latency by Hour", + "description": "Average p95 latency per service per hour (UTC)", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"service\", STRFTIME(\"h\", '%a %H:00') AS \"hour\", ROUND(AVG(\"latencyP95Ms\"), 0) AS \"latencyP95Ms\" FROM (SELECT \"service\", \"latencyP95Ms\", DATE_TRUNC('hour', \"timestamp\") AS \"h\" FROM \"dataset\") GROUP BY \"service\", \"h\" ORDER BY \"h\", \"service\"", + "metadata": [ + { + "name": "service", + "displayName": "Service" + }, + { + "name": "hour", + "displayName": "Hour (UTC)" + }, + { + "name": "latencyP95Ms", + "displayName": "Latency p95", + "shape": "milliseconds" + }, + { + "pattern": ".*" + } + ] + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "heatmap", + "encoding": { + "x": { + "field": "hour", + "type": "nominal", + "title": "Hour (UTC)" + }, + "y": { + "field": "service", + "type": "nominal", + "title": "Service" + }, + "color": { + "field": "latencyP95Ms", + "type": "quantitative", + "title": "Latency p95" + } + }, + "style": { + "colorScheme": "health", + "heatmapCellBorderWidth": 1, + "cartesianXAxisLabelRotation": -45 + } + } + } + } + }, + { + "i": "f2e1b025-09fc-4c33-9180-5d0d0387337a", + "x": 0, + "y": 7, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Requests per Minute", + "description": "Throughput of every service, stacked", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "requestsPerMin" + ], + "seriesColumn": "service", + "showLegend": true, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "requestsPerMin", + "type": "quantitative", + "title": "Requests/min" + }, + "series": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "cartesianStack": "on", + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "legendPosition": "right" + } + } + } + } + }, + { + "i": "b2335742-1817-42a9-a238-bd939cb44404", + "x": 0, + "y": 10, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Details", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "service", + "state", + "team", + "tier", + "region", + "latencyP95Ms", + "requestsPerMin", + "errorRatePercent", + "availabilityPercent", + "hosts" + ], + "hiddenColumns": [ + "serviceId", + "provider", + "latitude", + "longitude", + "sloTargetPercent", + "errorBudgetRemainingPercent" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "tier", + "title": "Tier", + "renderer": { + "type": "badge", + "colors": { + "Tier 1": "#206bc4", + "Tier 2": "#4299e1", + "Tier 3": "#9ba3af" + } + } + }, + { + "field": "region", + "title": "Region", + "renderer": { + "type": "text" + } + }, + { + "field": "latencyP95Ms", + "title": "Latency p95", + "align": "right", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " ms", + "thresholds": [ + { + "to": 300, + "color": "#2fb344" + }, + { + "to": 600, + "color": "#f59f00" + }, + { + "to": 100000, + "color": "#d63939" + } + ] + } + }, + { + "field": "requestsPerMin", + "title": "Requests/min", + "renderer": { + "type": "bar", + "color": "#206bc4" + } + }, + { + "field": "errorRatePercent", + "title": "Error rate", + "align": "right", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": "%", + "thresholds": [ + { + "to": 0.5, + "color": "#2fb344" + }, + { + "to": 2, + "color": "#f59f00" + }, + { + "to": 100, + "color": "#d63939" + } + ] + } + }, + { + "field": "availabilityPercent", + "title": "Availability (30 days)", + "align": "right", + "renderer": { + "type": "number", + "decimals": 3, + "suffix": "%" + } + }, + { + "field": "hosts", + "title": "Hosts", + "align": "right", + "renderer": { + "type": "number" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "state", + "colors": { + "error": "rgba(214,57,57,0.15)", + "warning": "rgba(245,159,0,0.15)" + } + }, + "richTableSort": { + "field": "errorRatePercent", + "direction": "desc" + } + } + } + } + } + }, + { + "i": "6803de3e-c3e2-4a01-8edd-53c37ff0bfec", + "x": 0, + "y": 15, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Host Health", + "description": "Current health of every host, grouped by region", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[hostHealth]}}", + "name": "hostHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Hosts]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "host", + "stateColumn": "state", + "linkColumn": "none", + "columns": 4 + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "host", + "type": "nominal", + "title": "Host" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "group": { + "field": "region", + "type": "nominal", + "title": "Region" + }, + "value": { + "field": "diskPercent", + "type": "quantitative", + "title": "Disk used" + } + }, + "style": { + "statusGridColumnsPerRow": 8 + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "state", + "column": "state", + "aggregation": "top", + "condition": { + "columns": [ + "state" + ] + }, + "frequency": 15 + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/defaultContent/slos.dash.json b/plugins/SampleData/v3/defaultContent/slos.dash.json new file mode 100644 index 00000000..c918b2e4 --- /dev/null +++ b/plugins/SampleData/v3/defaultContent/slos.dash.json @@ -0,0 +1,519 @@ +{ + "name": "SLOs & Error Budgets", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "5eb82187-65f1-4e0b-bdb3-f8571e8fa0af", + "x": 0, + "y": 0, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "SLO Summary", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "availabilityPercent" + ] + }, + { + "type": "mean", + "names": [ + "errorBudgetRemainingPercent" + ] + }, + { + "type": "min", + "names": [ + "errorBudgetRemainingPercent" + ] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "availabilityPercent_mean", + "comparisonColumn": "none", + "label": "Average availability (30 days)" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "availabilityPercent_mean", + "type": "quantitative", + "title": "Average availability (30 days)" + }, + { + "field": "errorBudgetRemainingPercent_mean", + "type": "quantitative", + "title": "Average error budget remaining" + }, + { + "field": "errorBudgetRemainingPercent_min", + "type": "quantitative", + "title": "Lowest error budget remaining" + } + ] + }, + "style": { + "scalarColumns": 3 + } + } + }, + "monitor": { + "_type": "simple", + "tileRollsUp": true, + "monitorType": "threshold", + "aggregation": "min", + "groupBy": "__group_by_none__", + "frequency": 15, + "condition": { + "columns": [ + "errorBudgetRemainingPercent_min" + ], + "logic": { + "if": [ + { + "<": [ + { + "var": "min" + }, + 10 + ] + }, + "error", + { + "<": [ + { + "var": "min" + }, + 25 + ] + }, + "warning", + "success" + ] + } + } + } + } + }, + { + "i": "8dfd2b81-50a1-4140-bd57-9c2deef366aa", + "x": 0, + "y": 2, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Availability vs SLO Target", + "description": "30-day availability of each service; the red marker is its SLO target", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bullet", + "encoding": { + "value": { + "field": "availabilityPercent", + "type": "quantitative", + "title": "Availability (30 days)" + }, + "target": { + "field": "sloTargetPercent", + "type": "quantitative", + "title": "SLO target" + }, + "label": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "gaugeMin": 98.9, + "gaugeMax": 100, + "bulletFillColor": "#206bc4", + "gaugeTargetColor": "#d63939", + "bulletLabelWidth": 140, + "bulletTrackHeight": 14 + } + } + } + } + }, + { + "i": "a9c2db59-1454-406d-b6fd-6f13546bb963", + "x": 2, + "y": 2, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Error Budget Remaining", + "description": "Share of each service's 30-day error budget still unspent", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "errorBudgetRemainingPercent", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "errorBudgetRemainingPercent", + "type": "quantitative", + "title": "Error budget remaining" + }, + "label": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "ringGridColumnsPerRow": 4, + "ringGridThresholds": [ + { + "to": 0.25, + "color": "#d63939" + }, + { + "to": 0.5, + "color": "#f59f00" + }, + { + "to": 1, + "color": "#2fb344" + } + ] + } + } + } + } + }, + { + "i": "665de496-5ba6-4818-b07f-56149f53c5ee", + "x": 0, + "y": 7, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Error Rate by Service", + "description": "Error rate of every service; 1% is the alerting threshold", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceMetrics]}}", + "name": "serviceMetrics", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "errorRatePercent" + ], + "seriesColumn": "service", + "showLegend": true, + "legendPosition": "bottom", + "showTrendLine": false + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal", + "title": "Time" + }, + "y": { + "field": "errorRatePercent", + "type": "quantitative", + "title": "Error rate" + }, + "series": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "legendPosition": "right", + "cartesianSymbol": "none" + }, + "markLines": [ + { + "value": 1, + "label": "1% alert threshold", + "color": "#d63939" + } + ] + } + } + } + }, + { + "i": "959de73f-6c2b-4b6c-a044-4cb109b81139", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "SLO Status", + "description": "Rows under 50% error budget are amber, under 25% red", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[serviceHealth]}}", + "name": "serviceHealth", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "service", + "tier", + "team", + "sloTargetPercent", + "availabilityPercent", + "errorBudgetRemainingPercent", + "errorRatePercent", + "state" + ], + "hiddenColumns": [ + "serviceId", + "region", + "provider", + "latitude", + "longitude", + "latencyP95Ms", + "requestsPerMin", + "hosts" + ] + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "service", + "title": "Service", + "renderer": { + "type": "text" + } + }, + { + "field": "tier", + "title": "Tier", + "renderer": { + "type": "badge", + "colors": { + "Tier 1": "#206bc4", + "Tier 2": "#4299e1", + "Tier 3": "#9ba3af" + } + } + }, + { + "field": "team", + "title": "Team", + "renderer": { + "type": "text" + } + }, + { + "field": "sloTargetPercent", + "title": "SLO target", + "align": "right", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": "%" + } + }, + { + "field": "availabilityPercent", + "title": "Availability (30 days)", + "align": "right", + "renderer": { + "type": "number", + "decimals": 3, + "suffix": "%" + } + }, + { + "field": "errorBudgetRemainingPercent", + "title": "Error budget remaining", + "renderer": { + "type": "progress", + "max": 100 + } + }, + { + "field": "errorRatePercent", + "title": "Error rate now", + "align": "right", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": "%" + } + }, + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + } + } + ] + }, + "style": { + "richTableRowHighlight": { + "field": "errorBudgetRemainingPercent", + "thresholds": [ + { + "to": 25, + "color": "rgba(214,57,57,0.15)" + }, + { + "to": 50, + "color": "rgba(245,159,0,0.15)" + }, + { + "to": 100, + "color": "rgba(0,0,0,0)" + } + ] + }, + "richTableSort": { + "field": "errorBudgetRemainingPercent", + "direction": "asc" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/SampleData/v3/docs/README.md b/plugins/SampleData/v3/docs/README.md new file mode 100644 index 00000000..4318a54b --- /dev/null +++ b/plugins/SampleData/v3/docs/README.md @@ -0,0 +1,61 @@ +Explore SquaredUp before connecting your own tools. This plugin creates a fictional software company with services, hosts, release pipelines and customers, and fills a set of ready-made dashboards with realistic data. + +> ⚠️ All data is generated. Nothing here reflects a real system, and the plugin never connects to your environment. + +## Setup + +There is nothing to configure. + +1. Add the **Sample Data** data source. +2. Leave the dashboards option selected so the out-of-the-box dashboards are installed. +3. Open the **Executive Overview** dashboard to start. + +## Configuration fields + +This plugin has no configuration fields. + +## What this plugin monitors + +The data tells one story. A release to **Payments API** went out about two and a half hours ago and caused an incident that is still recovering. You can follow it across every dashboard: + +- **Service health and SLOs** — Payments API is in error, and **Checkout API** and **Orders Service** are degraded. Latency, error rates and error budgets show the impact. +- **DevOps** — the release that caused the incident appears as a change failure next to the DORA metrics. +- **Infrastructure** — host CPU rises during the incident. Separately, a disk on an analytics host is slowly filling up. +- **Service desk and customers** — three customers raised P1 tickets and are now breaching their SLAs. +- **Business metrics** — orders dip while payments fail. +- **Cloud cost** — **Search Service** compute spend is running over budget after an index rebuild. + +The incident moves with the clock, so it is always recent. Historical data stays the same between refreshes. + +The out-of-the-box dashboards are **Executive Overview**, **Service Health (NOC)**, **SLOs & Error Budgets**, **DevOps · DORA**, **Cloud Cost · FinOps**, **Service Desk**, **Customers & SLAs** and **Business Metrics**. There is also a perspective for each **Service**, **Host**, **Pipeline** and **Customer**. + +## Data streams + +- **Service Health** — current state, golden signals, 30-day availability and error budget for each service. +- **Service Metrics** — latency, requests per minute, error rate and state over time for each service. +- **Service Dependencies** — request flow between services and the health of each downstream service. +- **Host Health** — current CPU, memory and disk usage and state for each host. +- **Host Performance** — CPU, memory, disk and network usage over time for each host. +- **Deployments** — production deployments for each pipeline, with status, duration, lead time and change failures. +- **Cloud Cost** — daily spend and budget by service and cost category. +- **Tickets** — service desk tickets for each customer, with priority, status and SLA outcome. +- **Customer SLAs** — SLA attainment, satisfaction, revenue and renewal date for each customer. +- **Business Metrics** — visitors, signups, orders, order value, conversion rate and MRR over time. Not tied to any object. +- **Conversion Funnel** — signup-to-paid conversion stages for the selected timeframe. Not tied to any object. + +## What gets indexed + +| Object type | Source | Represents | +| ------------ | ---------------------- | -------------------------------------------- | +| **Service** | The sample model (12) | A microservice owned by an engineering team. | +| **Host** | The sample model (19) | A cloud virtual machine running a service. | +| **Pipeline** | The sample model (6) | A release pipeline that deploys a service. | +| **Customer** | The sample model (20) | A customer account with an SLA. | + +**Relationships:** each Host runs a Service, and each Pipeline deploys a Service. + +## Known limitations + +- **Generated data** — values come from a fixed model, not from real systems. +- **Cloud cost is daily** — its timeframes start at the last 7 days and go up to a quarter. +- **Read-only** — the plugin never writes anywhere. diff --git a/plugins/SampleData/v3/icon.svg b/plugins/SampleData/v3/icon.svg new file mode 100644 index 00000000..f7b6ec31 --- /dev/null +++ b/plugins/SampleData/v3/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/SampleData/v3/indexDefinitions/default.json b/plugins/SampleData/v3/indexDefinitions/default.json new file mode 100644 index 00000000..263680c8 --- /dev/null +++ b/plugins/SampleData/v3/indexDefinitions/default.json @@ -0,0 +1,29 @@ +{ + "steps": [ + { + "name": "objects", + "dataStream": { "name": "objects" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": "type", + "properties": [ + "team", + "tier", + "region", + "provider", + "sloTarget", + "serviceName", + "os", + "size", + "tool", + "plan", + "city", + "country", + "slaTarget" + ] + } + } + ] +} diff --git a/plugins/SampleData/v3/metadata.json b/plugins/SampleData/v3/metadata.json new file mode 100644 index 00000000..6a72def4 --- /dev/null +++ b/plugins/SampleData/v3/metadata.json @@ -0,0 +1,64 @@ +{ + "name": "sample-data", + "displayName": "Sample Data", + "version": "3.0.0", + "author": { + "name": "SquaredUp", + "type": "firstParty" + }, + "description": "Explore SquaredUp with a realistic, ready-made company: service health, SLOs, DevOps, cloud cost, service desk and customer dashboards.", + "category": "Utility", + "type": "cloud", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "demo", + "sample", + "example", + "tutorial", + "getting started" + ], + "objectTypes": [ + "Service", + "Host", + "Pipeline", + "Customer" + ], + "screenshots": [ + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/executive-overview.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/service-health.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/slos-and-error-budgets.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/devops-and-dora.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/cloud-cost.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/service-desk.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/customers-and-slas.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/business-metrics.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/perspective-service-payments-api.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/perspective-host-pay-use-01.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/perspective-pipeline-payments-release.png", + "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/screenshots/perspective-customer-harbor-financial.png" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/SampleData/v3/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/SampleData/v3", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://raw.githubusercontent.com/squaredup/plugins/main/plugins/SampleData/v3/model", + "authMode": "none", + "headers": [], + "queryArgs": [] + } + } +} diff --git a/plugins/SampleData/v3/model/model.json b/plugins/SampleData/v3/model/model.json new file mode 100644 index 00000000..5102d4a4 --- /dev/null +++ b/plugins/SampleData/v3/model/model.json @@ -0,0 +1,103 @@ +{ + "company": "Contoso", + "incident": { + "service": "payments-api", + "title": "Payments API elevated error rate after release 4.12.0", + "startedMinutesAgo": 150, + "peakEndsMinutesAgo": 20, + "affectedServices": ["checkout-api", "orders-service"], + "affectedCustomers": ["northpeak-retail", "harbor-financial", "veltra-health"], + "pipeline": "payments-release", + "release": "4.12.0" + }, + "regions": [ + { "id": "us-east", "name": "US East (Virginia)", "provider": "AWS", "latitude": 38.95, "longitude": -77.45 }, + { "id": "eu-west", "name": "EU West (Dublin)", "provider": "AWS", "latitude": 53.35, "longitude": -6.26 }, + { "id": "ap-southeast", "name": "AP Southeast (Sydney)", "provider": "Azure", "latitude": -33.87, "longitude": 151.21 } + ], + "teams": [ + { "id": "platform", "name": "Platform" }, + { "id": "payments", "name": "Payments" }, + { "id": "commerce", "name": "Commerce" }, + { "id": "growth", "name": "Growth" }, + { "id": "data", "name": "Data" } + ], + "services": [ + { "id": "web-frontend", "name": "Web Frontend", "team": "growth", "tier": "Tier 1", "region": "us-east", "provider": "AWS", "latencyMs": 180, "requestsPerMin": 9200, "errorRate": 0.12, "sloTarget": 99.9, "monthlyCost": 6400 }, + { "id": "api-gateway", "name": "API Gateway", "team": "platform", "tier": "Tier 1", "region": "us-east", "provider": "AWS", "latencyMs": 35, "requestsPerMin": 15800, "errorRate": 0.05, "sloTarget": 99.95, "monthlyCost": 8900 }, + { "id": "auth-service", "name": "Auth Service", "team": "platform", "tier": "Tier 1", "region": "us-east", "provider": "AWS", "latencyMs": 60, "requestsPerMin": 7300, "errorRate": 0.04, "sloTarget": 99.95, "monthlyCost": 3100 }, + { "id": "checkout-api", "name": "Checkout API", "team": "commerce", "tier": "Tier 1", "region": "us-east", "provider": "AWS", "latencyMs": 240, "requestsPerMin": 1850, "errorRate": 0.15, "sloTarget": 99.9, "monthlyCost": 5200 }, + { "id": "payments-api", "name": "Payments API", "team": "payments", "tier": "Tier 1", "region": "us-east", "provider": "AWS", "latencyMs": 310, "requestsPerMin": 1420, "errorRate": 0.08, "sloTarget": 99.95, "monthlyCost": 7600 }, + { "id": "orders-service", "name": "Orders Service", "team": "commerce", "tier": "Tier 1", "region": "eu-west", "provider": "AWS", "latencyMs": 120, "requestsPerMin": 2600, "errorRate": 0.1, "sloTarget": 99.9, "monthlyCost": 4300 }, + { "id": "catalog-service", "name": "Catalog Service", "team": "commerce", "tier": "Tier 2", "region": "eu-west", "provider": "AWS", "latencyMs": 85, "requestsPerMin": 6100, "errorRate": 0.07, "sloTarget": 99.5, "monthlyCost": 3900 }, + { "id": "search-service", "name": "Search Service", "team": "growth", "tier": "Tier 2", "region": "eu-west", "provider": "AWS", "latencyMs": 140, "requestsPerMin": 4800, "errorRate": 0.2, "sloTarget": 99.5, "monthlyCost": 9800 }, + { "id": "notifications-service", "name": "Notifications Service", "team": "growth", "tier": "Tier 3", "region": "eu-west", "provider": "AWS", "latencyMs": 95, "requestsPerMin": 1300, "errorRate": 0.3, "sloTarget": 99.0, "monthlyCost": 1700 }, + { "id": "billing-service", "name": "Billing Service", "team": "payments", "tier": "Tier 2", "region": "us-east", "provider": "AWS", "latencyMs": 210, "requestsPerMin": 380, "errorRate": 0.09, "sloTarget": 99.9, "monthlyCost": 2600 }, + { "id": "customer-portal", "name": "Customer Portal", "team": "growth", "tier": "Tier 2", "region": "ap-southeast", "provider": "Azure", "latencyMs": 260, "requestsPerMin": 950, "errorRate": 0.18, "sloTarget": 99.5, "monthlyCost": 2900 }, + { "id": "analytics-pipeline", "name": "Analytics Pipeline", "team": "data", "tier": "Tier 3", "region": "ap-southeast", "provider": "Azure", "latencyMs": 900, "requestsPerMin": 220, "errorRate": 0.4, "sloTarget": 99.0, "monthlyCost": 12400 } + ], + "dependencies": [ + { "from": "web-frontend", "to": "api-gateway", "share": 1.0 }, + { "from": "customer-portal", "to": "api-gateway", "share": 1.0 }, + { "from": "api-gateway", "to": "auth-service", "share": 0.45 }, + { "from": "api-gateway", "to": "catalog-service", "share": 0.3 }, + { "from": "api-gateway", "to": "search-service", "share": 0.22 }, + { "from": "api-gateway", "to": "checkout-api", "share": 0.12 }, + { "from": "checkout-api", "to": "payments-api", "share": 0.75 }, + { "from": "checkout-api", "to": "orders-service", "share": 0.9 }, + { "from": "orders-service", "to": "notifications-service", "share": 0.5 }, + { "from": "payments-api", "to": "billing-service", "share": 0.25 }, + { "from": "orders-service", "to": "analytics-pipeline", "share": 0.08 } + ], + "hosts": [ + { "id": "web-use-01", "service": "web-frontend", "region": "us-east", "os": "Ubuntu 24.04", "size": "c6i.xlarge", "cpu": 38, "memory": 52, "disk": 41 }, + { "id": "web-use-02", "service": "web-frontend", "region": "us-east", "os": "Ubuntu 24.04", "size": "c6i.xlarge", "cpu": 35, "memory": 49, "disk": 39 }, + { "id": "gw-use-01", "service": "api-gateway", "region": "us-east", "os": "Amazon Linux 2023", "size": "c6i.2xlarge", "cpu": 44, "memory": 47, "disk": 28 }, + { "id": "gw-use-02", "service": "api-gateway", "region": "us-east", "os": "Amazon Linux 2023", "size": "c6i.2xlarge", "cpu": 42, "memory": 45, "disk": 30 }, + { "id": "auth-use-01", "service": "auth-service", "region": "us-east", "os": "Amazon Linux 2023", "size": "m6i.large", "cpu": 27, "memory": 58, "disk": 33 }, + { "id": "chk-use-01", "service": "checkout-api", "region": "us-east", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 41, "memory": 61, "disk": 46 }, + { "id": "chk-use-02", "service": "checkout-api", "region": "us-east", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 39, "memory": 59, "disk": 44 }, + { "id": "pay-use-01", "service": "payments-api", "region": "us-east", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 36, "memory": 55, "disk": 52 }, + { "id": "pay-use-02", "service": "payments-api", "region": "us-east", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 34, "memory": 54, "disk": 50 }, + { "id": "bill-use-01", "service": "billing-service", "region": "us-east", "os": "Windows Server 2022", "size": "m6i.large", "cpu": 22, "memory": 63, "disk": 71 }, + { "id": "ord-euw-01", "service": "orders-service", "region": "eu-west", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 33, "memory": 57, "disk": 48 }, + { "id": "ord-euw-02", "service": "orders-service", "region": "eu-west", "os": "Ubuntu 24.04", "size": "m6i.xlarge", "cpu": 31, "memory": 56, "disk": 47 }, + { "id": "cat-euw-01", "service": "catalog-service", "region": "eu-west", "os": "Amazon Linux 2023", "size": "r6i.large", "cpu": 29, "memory": 72, "disk": 38 }, + { "id": "srch-euw-01", "service": "search-service", "region": "eu-west", "os": "Amazon Linux 2023", "size": "r6i.2xlarge", "cpu": 57, "memory": 78, "disk": 64 }, + { "id": "srch-euw-02", "service": "search-service", "region": "eu-west", "os": "Amazon Linux 2023", "size": "r6i.2xlarge", "cpu": 55, "memory": 76, "disk": 83 }, + { "id": "ntf-euw-01", "service": "notifications-service", "region": "eu-west", "os": "Ubuntu 24.04", "size": "t3.large", "cpu": 18, "memory": 44, "disk": 36 }, + { "id": "portal-ause-01", "service": "customer-portal", "region": "ap-southeast", "os": "Windows Server 2022", "size": "D4s v5", "cpu": 31, "memory": 66, "disk": 55 }, + { "id": "etl-ause-01", "service": "analytics-pipeline", "region": "ap-southeast", "os": "Ubuntu 24.04", "size": "E8s v5", "cpu": 64, "memory": 81, "disk": 69 }, + { "id": "etl-ause-02", "service": "analytics-pipeline", "region": "ap-southeast", "os": "Ubuntu 24.04", "size": "E8s v5", "cpu": 61, "memory": 79, "disk": 92 } + ], + "pipelines": [ + { "id": "web-release", "name": "Web Release", "service": "web-frontend", "team": "growth", "tool": "GitHub Actions", "deploysPerDay": 6, "leadTimeHours": 5, "failureRate": 0.06, "durationMin": 11 }, + { "id": "platform-release", "name": "Platform Release", "service": "api-gateway", "team": "platform", "tool": "GitHub Actions", "deploysPerDay": 2, "leadTimeHours": 18, "failureRate": 0.04, "durationMin": 22 }, + { "id": "commerce-release", "name": "Commerce Release", "service": "checkout-api", "team": "commerce", "tool": "Azure DevOps", "deploysPerDay": 4, "leadTimeHours": 9, "failureRate": 0.08, "durationMin": 17 }, + { "id": "payments-release", "name": "Payments Release", "service": "payments-api", "team": "payments", "tool": "Azure DevOps", "deploysPerDay": 1.5, "leadTimeHours": 30, "failureRate": 0.1, "durationMin": 26 }, + { "id": "search-release", "name": "Search Release", "service": "search-service", "team": "growth", "tool": "GitHub Actions", "deploysPerDay": 3, "leadTimeHours": 7, "failureRate": 0.05, "durationMin": 14 }, + { "id": "data-release", "name": "Data Release", "service": "analytics-pipeline", "team": "data", "tool": "Jenkins", "deploysPerDay": 0.7, "leadTimeHours": 52, "failureRate": 0.14, "durationMin": 41 } + ], + "customers": [ + { "id": "northpeak-retail", "name": "Northpeak Retail", "city": "Chicago", "country": "United States", "latitude": 41.88, "longitude": -87.63, "plan": "Enterprise", "mrr": 48000, "slaTarget": 99.9, "seats": 1200 }, + { "id": "harbor-financial", "name": "Harbor Financial", "city": "New York", "country": "United States", "latitude": 40.71, "longitude": -74.01, "plan": "Enterprise", "mrr": 62000, "slaTarget": 99.95, "seats": 2100 }, + { "id": "veltra-health", "name": "Veltra Health", "city": "Boston", "country": "United States", "latitude": 42.36, "longitude": -71.06, "plan": "Enterprise", "mrr": 39000, "slaTarget": 99.9, "seats": 950 }, + { "id": "brightline-logistics", "name": "Brightline Logistics", "city": "Dallas", "country": "United States", "latitude": 32.78, "longitude": -96.8, "plan": "Business", "mrr": 18500, "slaTarget": 99.5, "seats": 420 }, + { "id": "cobalt-games", "name": "Cobalt Games", "city": "Seattle", "country": "United States", "latitude": 47.61, "longitude": -122.33, "plan": "Business", "mrr": 14200, "slaTarget": 99.5, "seats": 310 }, + { "id": "summit-education", "name": "Summit Education", "city": "Denver", "country": "United States", "latitude": 39.74, "longitude": -104.99, "plan": "Business", "mrr": 9800, "slaTarget": 99.5, "seats": 600 }, + { "id": "maple-civic", "name": "Maple Civic Services", "city": "Toronto", "country": "Canada", "latitude": 43.65, "longitude": -79.38, "plan": "Enterprise", "mrr": 27000, "slaTarget": 99.9, "seats": 800 }, + { "id": "tidewater-energy", "name": "Tidewater Energy", "city": "Houston", "country": "United States", "latitude": 29.76, "longitude": -95.37, "plan": "Enterprise", "mrr": 33500, "slaTarget": 99.9, "seats": 700 }, + { "id": "orbit-media", "name": "Orbit Media Group", "city": "London", "country": "United Kingdom", "latitude": 51.51, "longitude": -0.13, "plan": "Enterprise", "mrr": 41000, "slaTarget": 99.9, "seats": 1100 }, + { "id": "fjord-mobility", "name": "Fjord Mobility", "city": "Oslo", "country": "Norway", "latitude": 59.91, "longitude": 10.75, "plan": "Business", "mrr": 12600, "slaTarget": 99.5, "seats": 280 }, + { "id": "kestrel-insurance", "name": "Kestrel Insurance", "city": "Edinburgh", "country": "United Kingdom", "latitude": 55.95, "longitude": -3.19, "plan": "Enterprise", "mrr": 36800, "slaTarget": 99.95, "seats": 900 }, + { "id": "lumen-pharma", "name": "Lumen Pharma", "city": "Basel", "country": "Switzerland", "latitude": 47.56, "longitude": 7.59, "plan": "Enterprise", "mrr": 52000, "slaTarget": 99.95, "seats": 1500 }, + { "id": "atelier-fashion", "name": "Atelier Fashion House", "city": "Paris", "country": "France", "latitude": 48.86, "longitude": 2.35, "plan": "Business", "mrr": 15800, "slaTarget": 99.5, "seats": 350 }, + { "id": "rhine-automotive", "name": "Rhine Automotive", "city": "Stuttgart", "country": "Germany", "latitude": 48.78, "longitude": 9.18, "plan": "Enterprise", "mrr": 44500, "slaTarget": 99.9, "seats": 1300 }, + { "id": "duna-telecom", "name": "Duna Telecom", "city": "Budapest", "country": "Hungary", "latitude": 47.5, "longitude": 19.04, "plan": "Starter", "mrr": 4200, "slaTarget": 99.0, "seats": 90 }, + { "id": "sakura-robotics", "name": "Sakura Robotics", "city": "Osaka", "country": "Japan", "latitude": 34.69, "longitude": 135.5, "plan": "Business", "mrr": 16900, "slaTarget": 99.5, "seats": 400 }, + { "id": "coral-travel", "name": "Coral Travel Group", "city": "Singapore", "country": "Singapore", "latitude": 1.35, "longitude": 103.82, "plan": "Business", "mrr": 13300, "slaTarget": 99.5, "seats": 330 }, + { "id": "southern-cross-bank", "name": "Southern Cross Bank", "city": "Sydney", "country": "Australia", "latitude": -33.87, "longitude": 151.21, "plan": "Enterprise", "mrr": 58000, "slaTarget": 99.95, "seats": 1700 }, + { "id": "kauri-agritech", "name": "Kauri Agritech", "city": "Auckland", "country": "New Zealand", "latitude": -36.85, "longitude": 174.76, "plan": "Starter", "mrr": 3600, "slaTarget": 99.0, "seats": 60 }, + { "id": "ipanema-retail", "name": "Ipanema Retail", "city": "São Paulo", "country": "Brazil", "latitude": -23.55, "longitude": -46.63, "plan": "Business", "mrr": 11200, "slaTarget": 99.5, "seats": 260 } + ] +} diff --git a/plugins/SampleData/v3/screenshots/business-metrics.png b/plugins/SampleData/v3/screenshots/business-metrics.png new file mode 100644 index 00000000..1d3c12fa Binary files /dev/null and b/plugins/SampleData/v3/screenshots/business-metrics.png differ diff --git a/plugins/SampleData/v3/screenshots/cloud-cost.png b/plugins/SampleData/v3/screenshots/cloud-cost.png new file mode 100644 index 00000000..6ff462d1 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/cloud-cost.png differ diff --git a/plugins/SampleData/v3/screenshots/customers-and-slas.png b/plugins/SampleData/v3/screenshots/customers-and-slas.png new file mode 100644 index 00000000..03b90e9e Binary files /dev/null and b/plugins/SampleData/v3/screenshots/customers-and-slas.png differ diff --git a/plugins/SampleData/v3/screenshots/devops-and-dora.png b/plugins/SampleData/v3/screenshots/devops-and-dora.png new file mode 100644 index 00000000..9c32d278 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/devops-and-dora.png differ diff --git a/plugins/SampleData/v3/screenshots/executive-overview.png b/plugins/SampleData/v3/screenshots/executive-overview.png new file mode 100644 index 00000000..6b86fc5b Binary files /dev/null and b/plugins/SampleData/v3/screenshots/executive-overview.png differ diff --git a/plugins/SampleData/v3/screenshots/perspective-customer-harbor-financial.png b/plugins/SampleData/v3/screenshots/perspective-customer-harbor-financial.png new file mode 100644 index 00000000..51762ed4 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/perspective-customer-harbor-financial.png differ diff --git a/plugins/SampleData/v3/screenshots/perspective-host-pay-use-01.png b/plugins/SampleData/v3/screenshots/perspective-host-pay-use-01.png new file mode 100644 index 00000000..9c6e5ccd Binary files /dev/null and b/plugins/SampleData/v3/screenshots/perspective-host-pay-use-01.png differ diff --git a/plugins/SampleData/v3/screenshots/perspective-pipeline-payments-release.png b/plugins/SampleData/v3/screenshots/perspective-pipeline-payments-release.png new file mode 100644 index 00000000..32e4f8c1 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/perspective-pipeline-payments-release.png differ diff --git a/plugins/SampleData/v3/screenshots/perspective-service-payments-api.png b/plugins/SampleData/v3/screenshots/perspective-service-payments-api.png new file mode 100644 index 00000000..ec799aeb Binary files /dev/null and b/plugins/SampleData/v3/screenshots/perspective-service-payments-api.png differ diff --git a/plugins/SampleData/v3/screenshots/service-desk.png b/plugins/SampleData/v3/screenshots/service-desk.png new file mode 100644 index 00000000..72f98f9e Binary files /dev/null and b/plugins/SampleData/v3/screenshots/service-desk.png differ diff --git a/plugins/SampleData/v3/screenshots/service-health.png b/plugins/SampleData/v3/screenshots/service-health.png new file mode 100644 index 00000000..966ed599 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/service-health.png differ diff --git a/plugins/SampleData/v3/screenshots/slos-and-error-budgets.png b/plugins/SampleData/v3/screenshots/slos-and-error-budgets.png new file mode 100644 index 00000000..7b84eee4 Binary files /dev/null and b/plugins/SampleData/v3/screenshots/slos-and-error-budgets.png differ diff --git a/plugins/SampleData/v3/ui.json b/plugins/SampleData/v3/ui.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/plugins/SampleData/v3/ui.json @@ -0,0 +1 @@ +[]