diff --git a/dashboards/bitmovin/dashboard.json b/dashboards/bitmovin/dashboard.json new file mode 100644 index 0000000000..f16e5d14ee --- /dev/null +++ b/dashboards/bitmovin/dashboard.json @@ -0,0 +1,179 @@ +{ + "name": "Bitmovin Analytics", + "description": null, + "pages": [ + { + "name": "Bitmovin", + "description": null, + "widgets": [ + { + "title": "", + "layout": { + "column": 1, + "row": 1, + "width": 12, + "height": 1 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.markdown" + }, + "rawConfiguration": { + "text": "# To query more metrics, or add dimensions, modify your integration configuration following [these steps](https://github.com/newrelic/newrelic-bitmovin-analytics/tree/feat/refactor?tab=readme-ov-file#query-configuration)\n" + } + }, + { + "title": "All Bitmovin Metrics", + "layout": { + "column": 1, + "row": 2, + "width": 4, + "height": 4 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT uniques(metricName) WHERE metricName like 'bitmovin%'" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Concurrent Viewers", + "layout": { + "column": 5, + "row": 2, + "width": 8, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.stacked-bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT max(bitmovin.max_concurrent_viewers) as 'Concurrent Viewrs' TIMESERIES 1 minute" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Play Attemps", + "layout": { + "column": 5, + "row": 5, + "width": 8, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.line" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT max(bitmovin.cnt_play_attempts) as 'Play Attempts' TIMESERIES 1 minute since 15 minutes ago" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": { + "isLabelVisible": true + }, + "yAxisLeft": { + "zero": true + }, + "yAxisRight": { + "zero": true + } + } + }, + { + "title": "All Bitmovin Dimensions", + "layout": { + "column": 1, + "row": 6, + "width": 4, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.json" + }, + "rawConfiguration": { + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT keyset() WHERE metricName like 'bitmovin%'" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Video Startup Time (Histogram)", + "layout": { + "column": 5, + "row": 8, + "width": 8, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.histogram" + }, + "rawConfiguration": { + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT histogram(bitmovin.cnt_video_startuptime/1000,width: 1, buckets: 100) as 'Video Startup Time (s)'" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "yAxisLeft": { + "zero": true + } + } + } + ] + } + ], + "variables": [] +} \ No newline at end of file diff --git a/dashboards/bitmovin/dashboard.png b/dashboards/bitmovin/dashboard.png new file mode 100644 index 0000000000..8fa56cf17f Binary files /dev/null and b/dashboards/bitmovin/dashboard.png differ diff --git a/dashboards/slow-nrql-queries/slow-nrql-queries.json b/dashboards/slow-nrql-queries/slow-nrql-queries.json new file mode 100644 index 0000000000..6251c8646e --- /dev/null +++ b/dashboards/slow-nrql-queries/slow-nrql-queries.json @@ -0,0 +1,793 @@ +{ + "name": "Slow NRQL Queries and Timeouts", + "description": null, + "pages": [ + { + "name": "Overview", + "description": null, + "widgets": [ + { + "title": "Timeouts by Event Type", + "layout": { + "column": 1, + "row": 1, + "width": 5, + "height": 4 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT count(*) FROM NrdbQuery WHERE status = 'timedout' FACET query.eventType limit 100" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Long Running (> 30 secs) By Event Type", + "layout": { + "column": 6, + "row": 1, + "width": 7, + "height": 4 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "dataFormatters": [ + { + "name": "Avg Duration in Secs", + "precision": 2, + "type": "decimal" + } + ], + "facet": { + "showOtherSeries": false + }, + "initialSorting": { + "direction": "desc", + "name": "Avg Duration in Secs" + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT average(durationMs) / 1000 as 'Avg Duration in Secs' where durationMs > 30000 and status != 'timedout' FACET query.eventType LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": [ + { + "columnName": "Avg Duration in Secs", + "from": 30, + "severity": "warning", + "to": 39.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 40, + "severity": "severe", + "to": 49.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 50, + "severity": "critical", + "to": 80 + } + ] + } + }, + { + "title": "Long Running (> 30 secs) By Event Type & Time Window", + "layout": { + "column": 1, + "row": 5, + "width": 12, + "height": 4 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "dataFormatters": [ + { + "name": "Avg Duration in Secs", + "precision": 2, + "type": "decimal" + } + ], + "facet": { + "showOtherSeries": false + }, + "initialSorting": { + "direction": "desc", + "name": "Avg Duration in Secs" + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery \n WITH timeWindowMinutes/86400 AS timeWindowDays\nSELECT average(durationMs) / 1000 as 'Avg Duration in Secs' where durationMs > 30000 WHERE status != 'timedout' FACET query.eventType, timeWindowMinutes, timeWindowDays LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": [ + { + "columnName": "Avg Duration in Secs", + "from": 30, + "severity": "warning", + "to": 39.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 40, + "severity": "severe", + "to": 49.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 50, + "severity": "critical", + "to": 80 + } + ] + } + }, + { + "title": "Long Running (> 30 secs) Queries Stats", + "layout": { + "column": 1, + "row": 9, + "width": 12, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "dataFormatters": [ + { + "name": "Avg Duration in Secs", + "precision": 2, + "type": "decimal" + } + ], + "facet": { + "showOtherSeries": false + }, + "initialSorting": { + "direction": "desc", + "name": "Avg Duration in Secs" + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT average(durationMs) / 1000 as 'Avg Duration in Secs' where durationMs > 30000 and status != 'timedout' FACET query LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": [ + { + "columnName": "Avg Duration in Secs", + "from": 30, + "severity": "warning", + "to": 39.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 40, + "severity": "severe", + "to": 49.999 + }, + { + "columnName": "Avg Duration in Secs", + "from": 50, + "severity": "critical", + "to": 80 + } + ] + } + }, + { + "title": "", + "layout": { + "column": 1, + "row": 14, + "width": 2, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.markdown" + }, + "rawConfiguration": { + "text": "# Reference\n## Source Types:\n- apm: Application performance monitoring\n- dashboard: Dashboards displayed on the New Relic platform\n- Distributed tracing: Distributed tracing\n- nerdgraph: NerdGraph, our GraphQL API\n- New Relic user interface: A New Relic curated UI\n- query-api: Insights query API\n- query-builder: Query builder\n [more](https://docs.newrelic.com/attribute-dictionary/?event=NrdbQuery&attribute=source.name)\n---\n### Data Types —[data types](https://docs.newrelic.com/attribute-dictionary/).\nlet me know if you have question: srohatgi@newrelic.com" + } + }, + { + "title": "Queries That Timed Out Stats", + "layout": { + "column": 3, + "row": 14, + "width": 10, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT count(*) where status = 'timedout' FACET nr.source, query LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Successful Execuations", + "layout": { + "column": 1, + "row": 19, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.line" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT ((count(query) - filter(count(query), WHERE status !='successful' AND status !='error')) / count(query)) * 100 AS '% Successful' FACET cases(WHERE query LIKE '%bytecountestimate()%' AS 'bytecountestimate()', WHERE query NOT LIKE '%bytecountestimate()%' AS 'Normal') TIMESERIES 30 minutes EXTRAPOLATE " + } + ], + "nullValues": { + "nullValue": "remove" + }, + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": { + "isLabelVisible": true + }, + "yAxisLeft": { + "zero": false + }, + "yAxisRight": { + "zero": true + } + } + }, + { + "title": "Timedout", + "layout": { + "column": 7, + "row": 19, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.line" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT filter(count(query), WHERE query LIKE '%bytecountestimate()%') AS bytecountestimate, filter(count(query), WHERE query NOT LIKE '%bytecountestimate()%' ) as 'Normal' FROM NrdbQuery WHERE status = 'timedout' TIMESERIES 30 minutes FACET status " + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": { + "isLabelVisible": true + }, + "yAxisLeft": { + "zero": true + }, + "yAxisRight": { + "zero": true + } + } + }, + { + "title": "Errors", + "layout": { + "column": 1, + "row": 22, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.stacked-bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT filter(count(query), WHERE query LIKE '%bytecountestimate()%') AS bytecountestimate, filter(count(query), WHERE query NOT LIKE '%bytecountestimate()%' ) as 'Normal' FROM NrdbQuery WHERE status = 'error' TIMESERIES 30 minutes FACET status " + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "NRDB query API requests per minute", + "layout": { + "column": 7, + "row": 22, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.line" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM Metric SELECT sum(newrelic.resourceConsumption.currentValue) AS 'Usage' WHERE limitName = 'NRDB query API requests per minute' TIMESERIES AUTO " + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": { + "isLabelVisible": true + }, + "yAxisLeft": { + "zero": true + }, + "yAxisRight": { + "zero": true + } + } + } + ] + }, + { + "name": "Detail Stats", + "description": null, + "widgets": [ + { + "title": "Log Query Performance MoM (Time Window >= 1 day)", + "layout": { + "column": 1, + "row": 1, + "width": 6, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.billboard" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT FILTER(COUNT(*), WHERE timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS queries, FILTER(COUNT(*), WHERE status = 'timedout' AND timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS query_timedout, FILTER(COUNT(*), WHERE status = 'error' AND timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS query_error, FILTER(MEDIAN(durationMs) / 1000, WHERE timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS median_duration_sec, FILTER(MEDIAN(timeWindowMinutes) / 60, WHERE timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS median_window_hours, FILTER(MEDIAN(inspectedCount), WHERE timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS inspectedCounts, FILTER(MEDIAN((inspectedCount / 1000000) / (durationMs / 1000)), WHERE timeWindowMinutes >= 1440 AND query.eventType LIKE '%') AS meps FROM NrdbQuery LIMIT MAX since 1 month ago compare with 1 month ago" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Log Query Performance MoM (Time Window < 1 day)", + "layout": { + "column": 7, + "row": 1, + "width": 6, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.billboard" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT FILTER(COUNT(*), WHERE timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS queries, FILTER(COUNT(*), WHERE status = 'timedout' AND timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS query_timedout, FILTER(COUNT(*), WHERE status = 'error' AND timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS query_error, FILTER(MEDIAN(durationMs) / 1000, WHERE timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS median_duration_sec, FILTER(MEDIAN(timeWindowMinutes) / 60, WHERE timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS median_window_hours, FILTER(MEDIAN(inspectedCount), WHERE timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS inspectedCounts, FILTER(MEDIAN((inspectedCount / 1000000) / (durationMs / 1000)), WHERE timeWindowMinutes < 1440 AND query.eventType LIKE '%') AS meps FROM NrdbQuery LIMIT MAX since 1 month ago compare with 1 month ago UNTIL now" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Timedout Queries (TimeWindow >= 1 day) Past 7 Days", + "layout": { + "column": 1, + "row": 6, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.stacked-bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT count(*) FROM NrdbQuery WHERE status = 'timedout' AND timeWindowMinutes >= 1440 TIMESERIES auto FACET productCapability since 7 days ago" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Timedout Queries (TimeWindow < 1 day) Past 7 Days", + "layout": { + "column": 7, + "row": 6, + "width": 6, + "height": 3 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.stacked-bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "SELECT count(*) FROM NrdbQuery WHERE status = 'timedout' AND timeWindowMinutes < 1440 TIMESERIES auto FACET productCapability since 7 days ago" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Long Running (> 10 secs) Log Queries Stats", + "layout": { + "column": 1, + "row": 9, + "width": 12, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "dataFormatters": [ + { + "name": "Avg Duration in Secs", + "precision": 2, + "type": "decimal" + } + ], + "facet": { + "showOtherSeries": false + }, + "initialSorting": { + "direction": "desc", + "name": "Executions" + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT average(durationMs) / 1000 as 'Avg Duration in Secs', average(inspectedCount) as 'Inspected Count', count(*) as 'Executions', average(timeWindowMinutes)/60/24 as 'Time Window (days)' where durationMs > 10000 and status != 'timedout' FACET query LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + }, + "thresholds": [ + { + "columnName": "Avg Duration in Secs", + "from": 20, + "severity": "warning", + "to": 40 + }, + { + "columnName": "Avg Duration in Secs", + "from": 40, + "severity": "severe", + "to": 70 + }, + { + "columnName": "Avg Duration in Secs", + "from": 70, + "severity": "critical", + "to": 60000 + } + ] + } + }, + { + "title": "", + "layout": { + "column": 1, + "row": 14, + "width": 2, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.markdown" + }, + "rawConfiguration": { + "text": "# Reference\n## Source Types:\n- apm: Application performance monitoring\n- dashboard: Dashboards displayed on the New Relic platform\n- Distributed tracing: Distributed tracing\n- nerdgraph: NerdGraph, our GraphQL API\n- New Relic user interface: A New Relic curated UI\n- query-api: Insights query API\n- query-builder: Query builder\n [more](https://docs.newrelic.com/attribute-dictionary/?event=NrdbQuery&attribute=source.name)\n---\n### Data Types —[data types](https://docs.newrelic.com/attribute-dictionary/).\nlet me know if you have question: srohatgi@newrelic.com" + } + }, + { + "title": "Queries That Timed Out Stats", + "layout": { + "column": 3, + "row": 14, + "width": 10, + "height": 5 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.bar" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT count(*) where status = 'timedout' FACET nr.source, query LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Users with Slow Queries [Duration 20+ seconds & Time window 1+ days]", + "layout": { + "column": 1, + "row": 19, + "width": 12, + "height": 8 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.pie" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "legend": { + "enabled": true + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT average(timeWindowMinutes)/60/24 as 'Time Window (days)' where durationMs > 20000 and status != 'timedout' and query LIKE '%from log%' and timeWindowMinutes > 1440 FACET user LIMIT 10" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + } + ] + }, + { + "name": "Scanned Events", + "description": null, + "widgets": [ + { + "title": "Scanned Events (estimate) By Query and Source URL", + "layout": { + "column": 1, + "row": 1, + "width": 12, + "height": 8 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "dataFormatters": [ + { + "name": "Duration in Secs", + "precision": 2, + "type": "decimal" + }, + { + "name": "Events Scanned", + "type": "humanized" + } + ], + "facet": { + "showOtherSeries": true + }, + "initialSorting": { + "direction": "desc", + "name": "Events Scanned" + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrComputeUsage SELECT sum((usage)*1.21*1000000000) as 'estimated avg. scanned events' facet dimension_productCapability, dimension_sourceUrl, dimension_query where (usage > 1.21) limit MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + } + ] + }, + { + "name": "Dashboard Usage (unsucsessful)", + "description": null, + "widgets": [ + { + "title": "Impacted Dashboards", + "layout": { + "column": 1, + "row": 1, + "width": 12, + "height": 6 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT count(*) WHERE source.dashboardId IS NOT NULL and status != 'successful' FACET status, source.url, source.dashboardId, source.name LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Impacted Dashboard Users", + "layout": { + "column": 1, + "row": 7, + "width": 12, + "height": 6 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT count(*) WHERE source.dashboardId IS NOT NULL and status != 'successful' FACET user, status LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + }, + { + "title": "Impacted Dashboard Queries (By User)", + "layout": { + "column": 1, + "row": 13, + "width": 12, + "height": 6 + }, + "linkedEntityGuids": null, + "visualization": { + "id": "viz.table" + }, + "rawConfiguration": { + "facet": { + "showOtherSeries": false + }, + "nrqlQueries": [ + { + "accountIds": [], + "query": "FROM NrdbQuery SELECT count(*) WHERE source.dashboardId IS NOT NULL and status != 'successful' FACET user, status, source.url, source.dashboardId, source.name, query, query.eventType LIMIT MAX" + } + ], + "platformOptions": { + "ignoreTimeRange": false + } + } + } + ] + } + ], + "variables": [] +} diff --git a/dashboards/slow-nrql-queries/slow-nrql-queries.png b/dashboards/slow-nrql-queries/slow-nrql-queries.png new file mode 100644 index 0000000000..1d94bdc886 Binary files /dev/null and b/dashboards/slow-nrql-queries/slow-nrql-queries.png differ diff --git a/data-sources/bitmovin/config.yml b/data-sources/bitmovin/config.yml new file mode 100644 index 0000000000..161979876a --- /dev/null +++ b/data-sources/bitmovin/config.yml @@ -0,0 +1,21 @@ +id: bitmovin-video +displayName: Bitmovin +description: | + Bitmovin empowers businesses to expand their viewer reach and stream video globally with unmatched reliability, scalability, and the highest quality across the broadest range of devices. +icon: logo.svg +install: + primary: + link: + url: https://github.com/newrelic/newrelic-bitmovin-analytics +keywords: + - bitmovin + - video quality + - video experience + - quality of experience + - video streaming + - video metrics + - featured + - newrelic partner + - NR1_addData +categoryTerms: + - newrelic partner diff --git a/data-sources/bitmovin/logo.svg b/data-sources/bitmovin/logo.svg new file mode 100644 index 0000000000..64d8424410 --- /dev/null +++ b/data-sources/bitmovin/logo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/bitmovin/config.yml b/quickstarts/bitmovin/config.yml new file mode 100644 index 0000000000..55a471c416 --- /dev/null +++ b/quickstarts/bitmovin/config.yml @@ -0,0 +1,34 @@ +id: 249d6a1f-091b-4c63-ad22-d8f1b3a900f9 +slug: bitmovin-video +description: | + ## What is Bitmovin? + + Bitmovin empowers businesses to expand their viewer reach and stream video globally with unmatched reliability, scalability, and the highest quality across the broadest range of devices. + + ### Get started! + + The Bitmovin - New Relic integration utilizes the Bitmovin Analytics API to get data into New Relic. + + Check out the [documentation](https://github.com/newrelic/newrelic-bitmovin-analytics/blob/master/README.md) to learn more about setting up New Relic monitoring for of data from Bitmovin. +summary: | + Bitmovin empowers businesses to expand their viewer reach and stream video globally with unmatched reliability, scalability, and the highest quality across the broadest range of devices. +icon: logo.svg +level: Community +authors: + - Chris McCarthy +title: Bitmovin Analytics +dataSourceIds: + - bitmovin-video +dashboards: + - bitmovin +documentation: + - name: Bitmovin integration installation docs + description: Pull specified metrics and dimensions from the Bitmovin Analytics API into New Relic + url: https://github.com/newrelic/newrelic-bitmovin-analytics/blob/master/README.md +keywords: + - bitmovin + - video + - streaming + - newrelic partner + - NR1_addData + - NR1_sys diff --git a/quickstarts/bitmovin/logo.svg b/quickstarts/bitmovin/logo.svg new file mode 100644 index 0000000000..64d8424410 --- /dev/null +++ b/quickstarts/bitmovin/logo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/slow-nrql-query-analytics/config.yml b/quickstarts/slow-nrql-query-analytics/config.yml new file mode 100644 index 0000000000..085e6c6132 --- /dev/null +++ b/quickstarts/slow-nrql-query-analytics/config.yml @@ -0,0 +1,27 @@ +id: 47436e4a-f74c-4d87-8756-afc203a0f0d9 +slug: slow-nrql-query-analytics +title: Slow Query Analytics +description: | + This quickstart provides resources to better understand your NRQL query performance. +summary: | + Find out what kinds of telemetry and query patterns are responsible for slower queries. The dashbaords in this quickstart will help you pinpoint hot spots and opitimze your queries. +level: New Relic +authors: + - Marcel Schlapfer + - Jim Hagan +keywords: + - NRQL + - Slow Queries + - Query Performance + - Query Optimization + - Query Timeouts +dataSourceIds: + - solutions-hub-dashboards +documentation: + - name: NRQL Query Performance + url: https://docs.newrelic.com/docs/nrql/nrql-syntax-clauses-functions/ + description: Find slow query patterns and optimize your queries. +icon: logo.svg + +dashboards: + - slow-nrql-queries diff --git a/quickstarts/slow-nrql-query-analytics/logo.svg b/quickstarts/slow-nrql-query-analytics/logo.svg new file mode 100644 index 0000000000..ea60419c6b --- /dev/null +++ b/quickstarts/slow-nrql-query-analytics/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/utils/__tests__/build-validate-quickstart-artifact.test.js b/utils/__tests__/build-validate-quickstart-artifact.test.js index 4bb77a6b97..f6a809f757 100644 --- a/utils/__tests__/build-validate-quickstart-artifact.test.js +++ b/utils/__tests__/build-validate-quickstart-artifact.test.js @@ -1,20 +1,36 @@ -import * as fs from 'fs'; +import * as path from 'path'; import Quickstart from '../lib/Quickstart'; import DataSource from '../lib/DataSource'; import Alert from '../lib/Alert'; import Dashboard from '../lib/Dashboard'; - import { getArtifactComponents, - getDataSourceIds, validateArtifact, } from '../build-validate-quickstart-artifact'; -jest.mock('../lib/Quickstart'); -jest.mock('../lib/DataSource'); -jest.mock('../lib/Alert'); -jest.mock('../lib/Dashboard'); -jest.mock('fs'); +const MOCK_FILES_BASEPATH = path.resolve(__dirname, '..', 'mock_files'); + +const mockQuickstart1 = new Quickstart( + '/quickstarts/mock-quickstart-1/config.yml', + MOCK_FILES_BASEPATH +); + +const mockQuickstart2 = new Quickstart( + 'quickstarts/mock-quickstart-2/config.yml', + MOCK_FILES_BASEPATH +); + +const mockDataSource1 = new DataSource( + 'test-data-source', + MOCK_FILES_BASEPATH +) + +const mockAlert1 = new Alert( + 'mock-alert-policy-1', + MOCK_FILES_BASEPATH +); + +const mockDashboard1 = new Dashboard('mock-dashboard-1', MOCK_FILES_BASEPATH); describe('built-validate-quickstart-artifact', () => { beforeEach(() => { @@ -26,58 +42,27 @@ describe('built-validate-quickstart-artifact', () => { it('should find all of the components', () => { Quickstart.getAll = jest .fn() - .mockReturnValueOnce([ - { config: 'test-quickstart-1' }, - { config: 'test-quickstart-2' }, - ]); + .mockReturnValueOnce([mockQuickstart1, mockQuickstart2]); - DataSource.getAll = jest - .fn() - .mockReturnValueOnce([{ config: 'test-dataSource-1' }]); + DataSource.getAll = jest.fn().mockReturnValueOnce([mockDataSource1]); - Alert.getAll = jest - .fn() - .mockReturnValueOnce([{ config: 'test-alert-1' }]); - Dashboard.getAll = jest - .fn() - .mockReturnValueOnce([{ config: 'test-dashboard-1' }]); + Alert.getAll = jest.fn().mockReturnValueOnce([mockAlert1]); + Dashboard.getAll = jest.fn().mockReturnValueOnce([mockDashboard1]); const actual = getArtifactComponents(); expect(actual.quickstarts).toHaveLength(2); - expect(actual.quickstarts[0]).toEqual('test-quickstart-1'); - expect(actual.quickstarts[1]).toEqual('test-quickstart-2'); - expect(actual.dataSources).toHaveLength(1); - expect(actual.dataSources[0]).toEqual('test-dataSource-1'); - expect(actual.alerts).toHaveLength(1); - expect(actual.alerts[0]).toEqual('test-alert-1'); - expect(actual.dashboards).toHaveLength(1); - expect(actual.dashboards[0]).toEqual('test-dashboard-1'); - }); + expect(actual.quickstarts[0].dashboards).toEqual([]); + expect(actual.quickstarts[1].dashboards).toEqual(['mock-dashboard-1']); - it('should produce a complete list of dataSource IDs', () => { - Quickstart.getAll = jest.fn().mockReturnValueOnce([]); - Alert.getAll = jest.fn().mockReturnValueOnce([]); - Dashboard.getAll = jest.fn().mockReturnValueOnce([]); - DataSource.getAll = jest - .fn() - .mockReturnValueOnce([ - { config: { id: 'community-1' } }, - { config: { id: 'community-2' } }, - { config: { id: 'community-3' } }, - ]); - - const { dataSources } = getArtifactComponents(); - fs.readFileSync.mockReturnValueOnce(JSON.stringify(['core-1', 'core-2'])); + expect(actual.dataSources).toHaveLength(1); + expect(actual.dataSources[0].id).toEqual('test-data-source'); - const actual = getDataSourceIds('dummy-file.json', dataSources); + expect(Object.keys(actual.alerts)).toHaveLength(1); + expect(Object.keys(actual.alerts)).toContain('mock-alert-policy-1'); - expect(actual).toHaveLength(5); - expect(actual).toContain('community-1'); - expect(actual).toContain('community-2'); - expect(actual).toContain('community-3'); - expect(actual).toContain('core-1'); - expect(actual).toContain('core-2'); + expect(Object.keys(actual.dashboards)).toHaveLength(1); + expect(Object.keys(actual.dashboards)).toContain('mock-dashboard-1'); }); }); @@ -86,8 +71,8 @@ describe('built-validate-quickstart-artifact', () => { type: 'object', properties: { quickstarts: { type: 'array' }, - alerts: { type: 'array' }, - dashboards: { type: 'array' }, + alerts: { type: 'object' }, + dashboards: { type: 'object' }, dataSources: { type: 'array', items: { @@ -109,20 +94,12 @@ describe('built-validate-quickstart-artifact', () => { DataSource.getAll = jest .fn() .mockReturnValueOnce([ - { config: { id: 'community-1', title: 'DataSource 1' } }, - { config: { id: 'community-2', title: 'DataSource 2' } }, - { config: { id: 'community-3', title: 'DataSource 3' } }, + mockDataSource1 ]); const components = getArtifactComponents(); - fs.readFileSync.mockReturnValueOnce(JSON.stringify(['core-1', 'core-2'])); - const dataSourceIds = getDataSourceIds( - 'dummy-file.json', - components.dataSources - ); - - const artifact = { ...components, dataSourceIds }; + const artifact = { ...components, dataSourceIds: ['core-1', 'core-2'] }; const actual = validateArtifact(TEST_SCHEMA, artifact); @@ -133,23 +110,19 @@ describe('built-validate-quickstart-artifact', () => { Quickstart.getAll = jest.fn().mockReturnValueOnce([]); Alert.getAll = jest.fn().mockReturnValueOnce([]); Dashboard.getAll = jest.fn().mockReturnValueOnce([]); - DataSource.getAll = jest - .fn() - .mockReturnValueOnce([ - { config: { id: 'community-1', title: 'DataSource 1' } }, - { config: { id: false, title: 'DataSource 2' } }, - { config: { id: 'community-3', title: 3 } }, - ]); + DataSource.getAll = jest.fn().mockReturnValueOnce([]); const components = getArtifactComponents(); - fs.readFileSync.mockReturnValueOnce(JSON.stringify(['core-1', 'core-2'])); - const dataSourceIds = getDataSourceIds( - 'dummy-file.json', - components.dataSources - ); - - const artifact = { ...components, dataSourceIds }; + const artifact = { + ...components, + dataSources: [ + { id: 'community-1', title: 'DataSource 1' }, + { id: false, title: 'DataSource 2' }, + { id: 'community-3', title: 3 }, + ], + dataSourceIds: ['core-1', 'core-2'], + }; const actual = validateArtifact(TEST_SCHEMA, artifact); diff --git a/utils/build-validate-quickstart-artifact.ts b/utils/build-validate-quickstart-artifact.ts index 46b88070a2..7a6480c90d 100644 --- a/utils/build-validate-quickstart-artifact.ts +++ b/utils/build-validate-quickstart-artifact.ts @@ -5,11 +5,10 @@ import get from 'lodash/get'; import Quickstart from "./lib/Quickstart"; import DataSource from "./lib/DataSource"; import Alert from "./lib/Alert"; -import Dashboard, { DashboardConfig } from "./lib/Dashboard"; +import Dashboard from "./lib/Dashboard"; import Ajv, { type ErrorObject } from 'ajv'; -import { QuickstartConfig, QuickstartConfigAlert } from './types/QuickstartConfig'; -import { DataSourceConfig } from './types/DataSourceConfig'; import { passedProcessArguments } from './lib/helpers'; +import { ArtifactDataSourceConfig, ArtifactDashboardConfig, ArtifactQuickstartConfig, ArtifactAlertConfig } from './types/Artifact'; type ArtifactSchema = Record; @@ -20,10 +19,10 @@ type InvalidItem = { } type ArtifactComponents = { - quickstarts: QuickstartConfig[], - dataSources: DataSourceConfig[], - alerts: QuickstartConfigAlert[][], - dashboards: DashboardConfig[] + quickstarts: ArtifactQuickstartConfig[], + dataSources: ArtifactDataSourceConfig[], + alerts: ArtifactAlertConfig, + dashboards: ArtifactDashboardConfig } type Artifact = ArtifactComponents | { @@ -38,17 +37,33 @@ const getSchema = (filepath: string): ArtifactSchema => { // NOTE: we could run these in parallel to speed up the script export const getArtifactComponents = (): ArtifactComponents => { - const quickstarts = Quickstart.getAll().map((quickstart) => quickstart.config); + const quickstarts = Quickstart + .getAll() + .map((quickstart) => quickstart.transformForArtifact()); + console.log(`[*] Found ${quickstarts.length} quickstarts`); - const dataSources = DataSource.getAll().map((dataSource) => dataSource.config); + const dataSources = DataSource + .getAll() + .map((dataSource) => dataSource.transformForArtifact()); + console.log(`[*] Found ${dataSources.length} dataSources`); - const alerts = Alert.getAll().map((alert) => alert.config); - console.log(`[*] Found ${alerts.length} alerts`); + const alerts = Alert.getAll().reduce((acc, alert) => { + const conditions = alert.transformForArtifact() + + return { ...acc, ...conditions } + + }, {}); + + console.log(`[*] Found ${Object.keys(alerts).length} alerts`); + + const dashboards = Dashboard.getAll().reduce((acc, dash) => { + const dashboard = dash.transformForArtifact() + return { ...acc, ...dashboard } - const dashboards = Dashboard.getAll().map((dashboard) => dashboard.config); - console.log(`[*] Found ${dashboards.length} dashboards`); + }, {}); + console.log(`[*] Found ${Object.keys(dashboards).length} dashboards`); return { quickstarts, @@ -58,7 +73,7 @@ export const getArtifactComponents = (): ArtifactComponents => { } }; -export const getDataSourceIds = (filepath: string, communityDataSources: DataSourceConfig[]): string[] => { +export const getDataSourceIds = (filepath: string, communityDataSources: ArtifactComponents['dataSources']): string[] => { const coreDataSourceIds = yaml.load( fs.readFileSync(filepath).toString('utf8') ) as string[]; diff --git a/utils/lib/Alert.ts b/utils/lib/Alert.ts index 11c81c0524..c5dadbcdb6 100644 --- a/utils/lib/Alert.ts +++ b/utils/lib/Alert.ts @@ -10,6 +10,7 @@ import type { AlertType, QuickstartAlertInput, } from '../types/QuickstartMutationVariable'; +import type { ArtifactAlertConfig, ArtifactAlertType } from '../types/Artifact'; import type { QuickstartConfigAlert } from '../types/QuickstartConfig'; import type { NerdGraphResponseWithLocalErrors } from '../types/nerdgraph'; @@ -79,7 +80,11 @@ export type SubmitSetRequiredDataSourcesMutationResult = | NerdGraphResponseWithLocalErrors | { errors: ErrorOrNerdGraphError[] }; -class Alert extends Component { +class Alert extends Component< + QuickstartConfigAlert[], + QuickstartAlertInput[], + ArtifactAlertConfig +> { constructor(identifier: string, basePath?: string) { super(identifier, basePath); this.isValid = this.validate(); @@ -134,6 +139,38 @@ class Alert extends Component { } } + /** + * Method extracts criteria from the config and returns an object appropriately + * structured for the artifact. + */ + transformForArtifact() { + if (!this.isValid) { + console.error( + `Alert is invalid.\nPlease check if the path at ${this.identifier} exists.` + ); + return {}; + } + + const alertPolicy = this.config.map((condition) => { + const { description, name, type } = condition; + + return { + description: description && description.trim(), + displayName: name && name.trim(), + rawConfiguration: JSON.stringify(condition), + sourceUrl: Component.getAssetSourceUrl(this.configPath), + type: type && (type.trim().toLowerCase() as ArtifactAlertType), + }; + }); + + return { [this.identifier]: alertPolicy }; + } + + /** + * Method creates mutation variables for a given Alert. + * + * @deprecated This function should be removed once we have finished our new build publishing pipeline + */ getMutationVariables() { if (!this.isValid) { console.error( @@ -190,13 +227,17 @@ class Alert extends Component { return true; } catch (error) { - logger.error(`Alert Condition: Validaiton for ${displayName} failed with an error`); + logger.error( + `Alert Condition: Validaiton for ${displayName} failed with an error` + ); return false; } }); - logger.debug(`Alert condition: Finished validation for alert at ${this.identifier}`); + logger.debug( + `Alert condition: Finished validation for alert at ${this.identifier}` + ); return validations.every(Boolean); } @@ -299,8 +340,10 @@ class Alert extends Component { } static getAll() { - const alertPaths = glob.sync(path.join(__dirname, '..', '..', 'alert-policies', '**', '*.+(yml|yaml)')); - return alertPaths.map(alertPath => { + const alertPaths = glob.sync( + path.join(__dirname, '..', '..', 'alert-policies', '**', '*.+(yml|yaml)') + ); + return alertPaths.map((alertPath) => { // The identifier for alerts is the folder and the file name // e.g. `node-js/HighCpuUtilization.yml` const identifier = path.join(...alertPath.split('/').slice(-2, -1)); diff --git a/utils/lib/Component.ts b/utils/lib/Component.ts index 143ec16262..a6ed66002b 100644 --- a/utils/lib/Component.ts +++ b/utils/lib/Component.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as yaml from 'js-yaml'; import { GITHUB_REPO_BASE_URL } from '../constants'; -abstract class Component { +abstract class Component { public identifier: string; // Local path to the component. Ex: python/flask public configPath: string; // Absolute path to the config file within the repository public config: ConfigType; @@ -23,6 +23,7 @@ abstract class Component { abstract getConfigFilePath(): string; abstract getConfigContent(): ConfigType; abstract getMutationVariables(): MutationVariablesType; + abstract transformForArtifact(): ArtifactType; get fullPath() { return path.join(this.basePath, this.configPath); diff --git a/utils/lib/Dashboard.ts b/utils/lib/Dashboard.ts index 6a846f1516..781b428d36 100644 --- a/utils/lib/Dashboard.ts +++ b/utils/lib/Dashboard.ts @@ -16,6 +16,7 @@ import { fetchNRGraphqlResults, ErrorOrNerdGraphError, } from './nr-graphql-helpers'; +import { ArtifactDashboardConfig } from '../types/Artifact'; export interface DashboardConfig { name: string; @@ -61,7 +62,11 @@ export type SubmitSetRequiredDataSourcesMutationResult = | NerdGraphResponseWithLocalErrors | { errors: ErrorOrNerdGraphError[] }; -class Dashboard extends Component { +class Dashboard extends Component< + DashboardConfig, + QuickstartDashboardInput, + ArtifactDashboardConfig +> { /** * @returns - filepath from top level directory. */ @@ -104,6 +109,34 @@ class Dashboard extends Component { } } + /** + * Method extracts criteria from the config and returns an object appropriately + * structured for the artifact. + */ + transformForArtifact() { + if (!this.isValid) { + console.error( + `Dashboard is invalid.\nPlease check the dashboard at ${this.identifier}\n` + ); + return {}; + } + + const { name, description } = this.config; + const screenshotPaths = this.getScreenshotPaths(); + + return { + [this.identifier]: { + description: description && description.trim(), + displayName: name && name.trim(), + rawConfiguration: JSON.stringify(this.config), + sourceUrl: Component.getAssetSourceUrl(this.configPath), + screenshots: + screenshotPaths && + screenshotPaths.map((s) => this.getScreenshotUrl(s)), + }, + }; + } + /** * Get mutation variables from dashboard config * @returns - mutation variables for dashboard. diff --git a/utils/lib/DataSource.ts b/utils/lib/DataSource.ts index 04ccce4b0b..e19cb65e47 100644 --- a/utils/lib/DataSource.ts +++ b/utils/lib/DataSource.ts @@ -17,6 +17,7 @@ import type { DataSourceInstallDirectiveInput, DataSourceMutationVariable, } from '../types/DataSourceMutationVariable'; +import { ArtifactDataSourceConfig, ArtifactInstall } from '../types/Artifact'; export interface DataSourceMutationResponse { dataSource: { @@ -24,7 +25,11 @@ export interface DataSourceMutationResponse { }; } -class DataSource extends Component { +class DataSource extends Component< + DataSourceConfig, + string, + ArtifactDataSourceConfig +> { /** * @returns Filepath for the configuration file (from top-level directory). */ @@ -73,9 +78,39 @@ class DataSource extends Component { return this._getYamlConfigContent(); } + /** + * Method extracts criteria from the config and returns an object appropriately + * structured for the artifact. + */ + public transformForArtifact() { + const { keywords, description, categoryTerms, icon, ...rest } = this.config; + + return { + ...rest, + iconUrl: this._getIconUrl(), + install: this._parseInstallsForArtifact(), + categoryTerms: categoryTerms ? categoryTerms.map((t) => t.trim()) : [], + keywords: keywords ? keywords.map((k) => k.trim()) : [], + description: description && description.trim(), + }; + } + + private _parseInstallsForArtifact() { + const { install } = this.config; + + return { + primary: this._parseInstallDirectiveForArtifact(install.primary), + fallback: + install.fallback && + this._parseInstallDirectiveForArtifact(install.fallback), + }; + } + /** * Get the variables for the **Quickstart** mutation. * + * @deprecated This function should be removed once we have finished our new build publishing pipeline + * * @returns The ID for this data source */ getMutationVariables(): DataSourceConfig['id'] { @@ -189,6 +224,33 @@ class DataSource extends Component { return directive; } + /** + * Helper method that returns the directive, based on its type. + */ + private _parseInstallDirectiveForArtifact( + directive: DataSourceConfigInstallDirective + ): ArtifactInstall { + if ('link' in directive) { + const { url } = directive.link; + + return { + url: url?.trim() ?? '', + }; + } + + if ('nerdlet' in directive) { + const { nerdletId, nerdletState, requiresAccount } = directive.nerdlet; + + return { + nerdletId: nerdletId?.trim() ?? '', + nerdletState: nerdletState, + requiresAccount: requiresAccount, + }; + } + + return directive; + } + static isDataSource(x: DataSource | undefined): x is DataSource { return x !== undefined; } diff --git a/utils/lib/Quickstart.ts b/utils/lib/Quickstart.ts index c8e04f8239..5bfd59f557 100644 --- a/utils/lib/Quickstart.ts +++ b/utils/lib/Quickstart.ts @@ -23,6 +23,7 @@ import type { QuickstartSupportLevel, } from '../types/QuickstartMutationVariable'; import type { QuickstartConfig } from '../types/QuickstartConfig'; +import { ArtifactQuickstartConfig, ArtifactQuickstartConfigSupportLevel } from '../types/Artifact'; export interface QuickstartMutationResponse { quickstart: { @@ -34,6 +35,9 @@ interface SupportLevelMap { [key: string]: QuickstartSupportLevel; } +// FIXME: We will want to clean this up and conform to the `Artifact` types +// when we go to cleanup the deprecated mutation functionality after we have +// finalized the new quickstart publishing pipeline. const SUPPORT_LEVEL_ENUMS: SupportLevelMap = { 'New Relic': 'NEW_RELIC', Community: 'COMMUNITY', @@ -134,7 +138,11 @@ class Quickstart { /** * Get mutation variables from quickstart config + * + * @deprecated This function should be removed once we have finished our new build publishing pipeline + * * @returns - Promised mutation variables for quickstart + * */ async getMutationVariables( dryRun: boolean @@ -198,6 +206,58 @@ class Quickstart { }; } + /** + * Method extracts criteria from the config and returns an object appropriately + * structured for the artifact. + */ + public transformForArtifact(): ArtifactQuickstartConfig { + const { + description, + title, + slug, + documentation, + icon, + keywords, + summary, + dataSourceIds, + id, + level, + authors = [], + dashboards = [], + alertPolicies = [], + } = this.config; + + const metadata = { + quickstartUuid: id, + description: description && description.trim(), + displayName: title && title.trim(), + slug: slug && slug.trim(), + documentation: + documentation && + documentation.map((doc) => ({ + displayName: doc.name, + url: doc.url, + description: doc.description, + })), + iconUrl: this._constructIconUrl(icon), + keywords: keywords ?? [], + sourceUrl: Component.getAssetSourceUrl( + Component.removeBasePath(path.dirname(this.configPath), this.basePath) + ), + summary: summary && summary.trim(), + supportLevel: SUPPORT_LEVEL_ENUMS[ + level + ]?.toLowerCase() as ArtifactQuickstartConfigSupportLevel, + dataSourceIds: dataSourceIds, + alertConditions: alertPolicies, + dashboards, + authors, + }; + + + return metadata; + } + public async submitMutation(dryRun = true) { logger.info(`Submitting mutation for ${this.identifier}`, { dryRun }); const { data, errors } = await fetchNRGraphqlResults< diff --git a/utils/schema/artifact.json b/utils/schema/artifact.json index 0114436fe4..fb9d5a9674 100644 --- a/utils/schema/artifact.json +++ b/utils/schema/artifact.json @@ -1,27 +1,40 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", "properties": { "quickstarts": { "type": "array", - "items": { "$ref": "#/definitions/quickstart" } + "items": { + "$ref": "#/definitions/quickstart" + } }, "dataSources": { "type": "array", - "items": { "$ref": "#/definitions/dataSource" } + "items": { + "$ref": "#/definitions/dataSource" + } }, "alerts": { - "type": "array", - "items": { "$ref": "#/definitions/alert" } + "type": "object", + "patternProperties": { + ".*": { + "$ref": "#/definitions/alert" + } + } }, "dashboards": { - "type": "array", - "items": { "$ref": "#/definitions/dashboard" } + "type": "object", + "patternProperties": { + ".*": { + "$ref": "#/definitions/dashboard" + } + } }, "dataSourceIds": { "type": "array", - "items": { "$ref": "#/definitions/dataSourceIds" } + "items": { + "$ref": "#/definitions/dataSourceIds" + } } }, "required": [ @@ -32,178 +45,576 @@ "dataSourceIds" ], "additionalProperties": false, - "definitions": { "quickstart": { "type": "object", "properties": { - "id": { "type": "string" }, - "description": { "type": "string" }, - "summary": { "type": "string" }, - "title": { "type": "string" }, + "quickstartUuid": { + "type": "string" + }, + "description": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "displayName": { + "type": "string" + }, "authors": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + "sourceUrl": { + "type": "string" }, "documentation": { "type": "array", "items": { "type": "object", "properties": { - "name": { "type": "string" }, - "description": { "type": "string" }, - "url": { "type": "string" } + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } }, - "required": ["name", "description", "url"], + "required": ["displayName", "description", "url"], "additionalProperties": false } }, - "level": { "enum": ["New Relic", "Community", "Verified"] }, - "icon": { "type": "string" }, + "supportLevel": { + "enum": ["new_relic", "community", "verified", "enterprise"] + }, + "iconUrl": { + "type": "string" + }, "keywords": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, - "slug": { "type": "string" }, - "alertPolicies": { + "slug": { + "type": "string" + }, + "alertConditions": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "dashboards": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "dataSourceIds": { "type": "array", - "items": { "$ref": "#/definitions/dataSourceIds" } + "items": { + "$ref": "#/definitions/dataSourceIds" + } } }, "required": [ "description", "summary", - "title", + "displayName", "authors", - "icon" + "iconUrl" ], "additionalProperties": false }, - "dataSource": { "type": "object", "properties": { - "id": { "type": "string" }, - "displayName": { "type": "string" }, - "description": { "type": "string" }, + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, "install": { "type": "object", "properties": { - "primary": { "$ref": "#/definitions/installDirective" }, - "fallback": { "$ref": "#/definitions/installDirective" } + "primary": { + "$ref": "#/definitions/installDirective" + }, + "fallback": { + "$ref": "#/definitions/installDirective" + } }, "required": ["primary"], "additionalProperties": false }, "keywords": { - "type": "array", "items": { "type": "string" } + "type": "array", + "items": { + "type": "string" + } }, "categoryTerms": { - "type": "array", "items": { "type": "string" } + "type": "array", + "items": { + "type": "string" + } }, - "icon": { "type": "string" } + "iconUrl": { + "type": "string" + } }, "required": ["id", "displayName", "install"], "additionalProperties": false }, - "installDirective": { "type": "object", "oneOf": [ { "type": "object", "properties": { - "link": { - "type": "object", - "properties": { - "url": { "type": "string" } - }, - "required": ["url"], - "additionalProperties": false + "url": { + "type": "string" } }, - "required": ["link"], + "required": ["url"], "additionalProperties": false }, { "type": "object", "properties": { - "nerdlet": { - "type": "object", - "properties": { - "nerdletId": { "type": "string" }, - "nerdletState": { "type": "object" }, - "requiresAccount": { "type": "boolean" } - }, - "required": ["nerdletId", "requiresAccount"], - "additionalProperties": false + "nerdletId": { + "type": "string" + }, + "nerdletState": { + "type": "object" + }, + "requiresAccount": { + "type": "boolean" } }, - "required": ["nerdlet"], + "required": ["nerdletId", "requiresAccount"], "additionalProperties": false } ] }, - "alert": { "type": "array", "items": { "type": "object", "properties": { - "name": { "type": "string" }, - "description": { "type": "string", "nullable": true }, - "type": { "enum": ["BASELINE", "STATIC"], "type": "string", "nullable": true }, - "nrql": { - "type": "object", - "properties": { - "query": { "type": "string" } - }, - "required": ["query"], - "additionalProperties": true + "type": { + "enum": ["baseline", "static"], + "type": "string", + "nullable": true }, - "runbookUrl": { "type": "string", "nullable": true }, - "violationTimeLimitSeconds": { "type": "number" }, - "enabled": { "type": "boolean" }, - "terms": { + "displayName": { + "type": "string" + }, + "description": { + "type": ["string", "null"] + }, + "rawConfiguration": { + "type": "string" + }, + "sourceUrl": { + "type": "string" + }, + "screenshots": { "type": "array", - "minItems": 1, "items": { "type": "object", "properties": { - "duration": { "type": "number", "minimum": 5, "maximum": 120 }, - "priority": { "enum": ["CRITICAL", "WARNING"] }, - "operator": { "enum": ["ABOVE", "BELOW", "EQUALS"] }, - "threshold": { "type": "number", "minimum": 0 }, - "thresholdDuration": { "type": "number", "minimum": 0 }, - "thresholdOccurances": { "enum": ["ALL", "AT_LEAST_ONCE"] } + "url": { + "type": "string" + } }, - "additionalProperties": true + "required": ["url"] } } }, - "required": ["name", "description", "type"], - "additionalProperties": true + "required": ["displayName", "rawConfiguration"], + "additionalProperties": false } }, - "dashboard": { "type": "object", "properties": { - "name": { "type": "string" }, - "pages": { "type": "array", "minItems": 1 }, - "description": { "type": ["string", "null"] }, - "variables": { "type": "array" } + "displayName": { + "type": "string" + }, + "description": { + "type": ["string", "null"] + }, + "rawConfiguration": { + "type": "string" + }, + "sourceUrl": { + "type": "string" + }, + "screenshots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"] + } + } }, - "required": ["name", "pages"], + "required": ["displayName", "rawConfiguration"], "additionalProperties": false + }, + "dataSourceIds": { + "enum": [ + "active-directory", + "ads-web-gpt", + "ads-web-prebid", + "aerospike", + "akamai", + "alert-quality-management", + "amazon-cloudwatch-metric-streams", + "amazon-eks-on-aws-fargate", + "ansible-automation-controller", + "apache-druid", + "apache-flink", + "apache-hadoop", + "apache-mesos", + "apache-traffic-server", + "apache-zookeeper", + "apdex-optimizer", + "apigee-api", + "apm-signals", + "argocd", + "atlassian-jira", + "audit-events-analysis", + "aws-cloudfront-logs", + "azure-monitor", + "azure-virtual-machines", + "battlesnake", + "bitbucket", + "third-party-biztalk360", + "blameless", + "blazemeter", + "blazor-webassembly", + "boomi", + "browser-segment-investigation", + "browser-signals", + "cacti", + "camel", + "catchpoint-quickstart", + "circleci", + "cloud-spanner", + "cloudflare", + "contentsquare", + "conviva", + "cribl", + "customer-experience-bottom-funnel-analysis", + "customer-experience-quality-foundation", + "dapr", + "databricks", + "datazoom", + "dbmarlin", + "dbt-cloud", + "deeper-network", + "delphix", + "docker-otel", + "dojo", + "elasticsearch-query", + "elixir", + "email-notifications", + "envoy", + "event-api", + "fastly", + "fivetran", + "flutter-android", + "flutter-ios", + "flutter-web", + "full-story", + "gatsby-build-newrelic", + "gcp-pubsub", + "gh-gates", + "gigamon-appinsights", + "github-repo", + "gitlab-integration", + "glassbox", + "grafana-dashboard-migration", + "grafana-prometheus-integration", + "gridgain-data-source", + "hardware-sentry", + "hcp-consul-otel", + "hcp-consul", + "hcp-envoy", + "hcp-vault-logs", + "hcp-vault-metrics", + "hivemq-integration-docs", + "ibmmq-integration-docs", + "infrastructure-signals", + "jdbc-executebatch", + "otel-jenkins-integration", + "jfrog-platform-cloud", + "jfrog-platform", + "jira-errors-inbox", + "k6-prometheus", + "k6", + "kentik", + "lacework", + "lambdatest", + "langchain-vectordb", + "langchain", + "launchdarkly", + "lighthouse", + "lighttpd", + "linkerd", + "logs-api", + "logs-default", + "metric-api", + "mobile-getting-started", + "mobile-network-performance-install", + "mobile-signals", + "mongodb-prometheus-integration-docs", + "mule-esb", + "mux-video", + "netlify-builds", + "netlify-logs", + "netscaler", + "network-data-ingest-and-cardinality", + "network-routers-and-switches", + "network-syslog", + "newrelic-cli", + "nextcloud", + "nr-reports", + "nuxtjs", + "nvidia-dcgm", + "nvidia-gpu", + "nvidia-jetson", + "nvidia-triton", + "nvml", + "ocsf", + "okhttp", + "onepane", + "opencensus", + "openstack-controller", + "opslevel", + "pagerduty", + "perfmon", + "pihole", + "port-monitoring", + "postfix", + "postman", + "pulumi", + "quantum-metric", + "rafay", + "ray", + "redis-enterprise", + "redis-otel", + "redmine", + "redpanda", + "releaseiq", + "newrelic-java-rmi", + "roku-http-analytics", + "roku", + "salesforce-eventlog-for-logs", + "sendgrid-integration", + "sendmail", + "servicenow-notifications", + "shopify-hydrogen", + "shopify", + "signl4", + "singlestore", + "skykit", + "slack-notifications", + "snowflake", + "solutions-hub-dashboards", + "sonarqube", + "speedscale", + "split", + "squid-prometheus-integration-docs", + "stripe", + "sybase-integration", + "synthetics-private-locations", + "telemetry-data-platform", + "temporal-cloud", + "temporal", + "terraform", + "tidbcloud", + "trace-and-span-api", + "traceloop", + "trendmicro-cloudone-conformity", + "tvos-mobile", + "unix", + "vercel", + "vertica", + "newrelic-java-vertx", + "newrelic-java-vertx-extensions", + "video-android", + "video-chromecast", + "video-ios-tvos", + "video-web-akamai", + "video-web-html5", + "video-web-jwplayer", + "video-web-theplatform", + "video-web-videojs", + "webhook-notifications", + "windows-certs", + "winservices-integration-docs", + "xmatters", + "zebrium", + "zenduty", + "zipkin", + "activemq", + "amazon-cloudwatch", + "amazon-kinesis", + "amazon-linux", + "amazon-linux-logs", + "amazon-s3", + "amazon-sagemaker", + "amazon-web-services", + "android", + "angular", + "angular-js", + "ansible-install", + "apache", + "apm-logs", + "aporia", + "aws-firelens", + "aws-lambda", + "aws-migrate-metricStream", + "aws-security-hub", + "aws-vpc-flow-logs", + "azure-openai", + "backbone", + "bring-your-own-data", + "capacitor", + "cassandra", + "centos", + "centos-logs", + "change-tracking", + "chef-install", + "comet", + "confluent-cloud", + "consul", + "cordova", + "couchbase", + "dagshub", + "debian", + "debian-logs", + "dependabot", + "distributed-tracing", + "docker", + "dropwizard", + "elasticsearch", + "ember", + "f5", + "fluent-bit", + "fluentd", + "flutter", + "gatsby", + "gcp-vpc-flow-logs", + "golang", + "google-cloud-platform", + "google-cloud-platform-logs", + "govcloud", + "guided-install", + "haproxy", + "headerless-log-api", + "heroku", + "heroku-http", + "ios", + "java", + "java-logs", + "jmx", + "jquery", + "kafka", + "kamon", + "kubernetes", + "kubernetes-logs", + "linux", + "linux-infra", + "linux-logs", + "llm-application", + "logstash", + "macos", + "maui", + "memcached", + "micrometer", + "microsoft-azure", + "microsoft-azure-blob-storage", + "microsoft-azure-event-hub", + "microsoft-sql-server", + "microsoftnet", + "microsoftnet-logs", + "mona", + "mongodb", + "mysql", + "nagios", + "network-devices", + "network-flows", + "network-synthetics", + "new-relic-browser", + "new-relic-infrastructure-agent", + "new-relic-synthetics", + "next-js", + "nginx", + "node-js", + "nr-ai-monitoring", + "nvidia-nim", + "opentelemetry", + "oracle-database", + "php", + "pixie", + "postgresql", + "powerdns", + "prometheus", + "prometheus-agent-mode", + "puppet-install", + "python", + "rabbitmq", + "react", + "react-native", + "redis", + "rhel", + "rhel-logs", + "ruby", + "ruby-logs", + "security-api", + "sles", + "sles-logs", + "snmp", + "snyk", + "statsd", + "super-agent", + "syslog", + "trivy", + "truera", + "ubuntu", + "ubuntu-logs", + "unity", + "unreal", + "varnish", + "vmware-tanzu", + "vmware-vsphere", + "vue", + "windows", + "windows-infra", + "windows-logs", + "xamarin", + "xcframework", + "zepto" + ] } } } + diff --git a/utils/types/Artifact.ts b/utils/types/Artifact.ts new file mode 100644 index 0000000000..88fd0660d7 --- /dev/null +++ b/utils/types/Artifact.ts @@ -0,0 +1,100 @@ +/* -- Data source -- */ + +export interface ArtifactDataSourceConfig { + id: string; + displayName: string; + description?: string; + install: ArtifactInstallDirective; + keywords?: string[]; + categoryTerms?: string[]; + iconUrl?: string; +} + +export type ArtifactInstallDirective = { + primary: ArtifactInstall; + fallback?: ArtifactInstall; +}; + +export type ArtifactInstall = + | ArtifactDataSourceConfigNerdletDirective + | ArtifactDataSourceConfigLinkDirective; + +interface ArtifactDataSourceConfigNerdletDirective { + nerdletId: string; + nerdletState: Record; + requiresAccount: boolean; +} + +interface ArtifactDataSourceConfigLinkDirective { + url: string; +} + +interface ArtifactDataSourceConfigNerdletDirective { + nerdletId: string; + nerdletState: Record; + requiresAccount: boolean; +} + +/* -- Quickstart -- */ + +type QuickstartConfigDocumentation = { + displayName: string; + description: string; + url: string; +}; + +export type ArtifactQuickstartConfigSupportLevel = + | 'new_relic' + | 'community' + | 'verified' + // Enterprise is deprecated. However some quickstarts still have this support + // level within their config. + | 'enterprise'; + +export interface ArtifactQuickstartConfig { + quickstartUuid: string; + description: string; + displayName: string; + slug?: string; + documentation: QuickstartConfigDocumentation[]; + iconUrl: string; + keywords?: string[]; + summary: string; + supportLevel: ArtifactQuickstartConfigSupportLevel; + alertConditions?: string[]; + dashboards?: string[]; + dataSourceIds?: string[]; + authors: string[]; +}; + +/* -- Dashboard -- */ + +type DashboardScreenshot = { + url: string; +}; + +export interface ArtifactDashboardConfig { + [id: string]: { + description?: string; + displayName: string; + rawConfiguration: string; + sourceUrl?: string; + screenshots?: DashboardScreenshot[]; + }; +} + +/* --- Alert --- */ + +export type ArtifactAlertType = 'baseline' | 'static'; + +type ArtifactAlert = { + description?: string; + displayName: string; + rawConfiguration: string; + sourceUrl?: string; + type: ArtifactAlertType; +}; + +export interface ArtifactAlertConfig { + [id: string]: ArtifactAlert[]; +}