diff --git a/deployment/terraform/templates/haystack-ui_json.tpl b/deployment/terraform/templates/haystack-ui_json.tpl index 6dbb7c54..bc04b785 100644 --- a/deployment/terraform/templates/haystack-ui_json.tpl +++ b/deployment/terraform/templates/haystack-ui_json.tpl @@ -9,6 +9,9 @@ "host": "${graphite_hostname}", "port": ${graphite_port} }, + "grpcOptions": { + "grpc.max_receive_message_length": 52428800 + } "connectors": { "traces": { "connectorName": "haystack", @@ -16,9 +19,6 @@ "haystackPort": ${trace_reader_service_port}, "serviceRefreshIntervalInSecs": 60, "fieldKeys": [${whitelisted_fields}], - "grpcOptions": { - "grpc.max_receive_message_length": 52428800 - } }, "trends": { "connectorName": "haystack", diff --git a/haystack-idl b/haystack-idl index 52676770..3da132c3 160000 --- a/haystack-idl +++ b/haystack-idl @@ -1 +1 @@ -Subproject commit 52676770bde1ad4de5067ce2f9687d89e832eea6 +Subproject commit 3da132c3bd9c700b4d6c16aa627737371b43b123 diff --git a/server/config/base.js b/server/config/base.js index 648257cc..4bf08550 100644 --- a/server/config/base.js +++ b/server/config/base.js @@ -29,6 +29,10 @@ module.exports = { // base64 and periodreplacement are supported, default to noop if none provided encoder: 'periodreplacement', + grpcOptions: { + 'grpc.max_receive_message_length': 10485760 + }, + // this list defines subsystems for which UI should be enabled // traces connector must be present in connectors config connectors: { diff --git a/server/connectors/alerts/haystack/alertsConnector.js b/server/connectors/alerts/haystack/alertsConnector.js index d9c7cad5..d818a609 100644 --- a/server/connectors/alerts/haystack/alertsConnector.js +++ b/server/connectors/alerts/haystack/alertsConnector.js @@ -19,7 +19,7 @@ const _ = require('lodash'); const grpc = require('grpc'); const config = require('../../../config/config'); -const servicesConnector = require('../../services/servicesConnector'); +const servicesConnector = config.connectors.traces && require('../../services/servicesConnector'); // eslint-disable-line const fetcher = require('../../operations/grpcFetcher'); const services = require('../../../../static_codegen/anomaly/anomalyReader_grpc_pb'); @@ -28,83 +28,120 @@ const MetricpointNameEncoder = require('../../utils/encoders/MetricpointNameEnco const metricpointNameEncoder = new MetricpointNameEncoder(config.encoder); -const grpcOptions = { - 'grpc.max_receive_message_length': 10485760, // todo: do I need these? - ...config.connectors.traces.grpcOptions -}; +const grpcOptions = config.grpcOptions || {}; const connector = {}; const client = new services.AnomalyReaderClient( `${config.connectors.alerts.haystackHost}:${config.connectors.alerts.haystackPort}`, grpc.credentials.createInsecure(), grpcOptions); // TODO make client secure -const alertTypes = ['durationTP99', 'failureCount']; +const alertTypes = ['duration', 'failure-span']; const getAnomaliesFetcher = fetcher('getAnomalies', client); -const alertFreqInSec = config.connectors.alerts.alertFreqInSec; // TODO make this based on alert type +const alertFreqInSec = config.connectors.alerts.alertFreqInSec || 300; // TODO make this based on alert type function fetchOperations(serviceName) { - return servicesConnector.getOperations(serviceName); + return servicesConnector && servicesConnector.getOperations(serviceName); +} + +function sameOperationAndType(alertToCheck, operationName, type) { + if (!alertToCheck) { + return false; + } + const operationToCheck = alertToCheck.labelsMap.find(label => label[0] === 'operationName'); + const typeToCheck = alertToCheck.labelsMap.find(label => label[0] === 'metric_key'); + return ((operationToCheck && operationToCheck[1] === operationName) && typeToCheck && typeToCheck[1] === type); } function parseOperationAlertsResponse(data) { - return data.searchanomalyresponseList.map((anomalyResponse) => { - const labels = anomalyResponse.labels; - - const operationName = labels.operationName; - const alertType = labels.alertType; - const latestUnhealthy = _.maxBy(anomalyResponse.anomalies, anomaly => anomaly.timestamp); - - const isUnhealthy = (latestUnhealthy && latestUnhealthy.timestamp >= (Date.now() - alertFreqInSec)); - const timestamp = latestUnhealthy && latestUnhealthy.timestamp; - return { - operationName, - alertType, - isUnhealthy, - timestamp - }; + const fullAnomalyList = data.searchanomalyresponseList; + const mappedAndMergedResponse = fullAnomalyList.map((anomalyResponse, baseIterationIndex) => { + if (anomalyResponse === null) return null; + const operationLabel = anomalyResponse.labelsMap.find(label => label[0] === 'operationName'); + if (operationLabel) { + const operationName = operationLabel[1]; + const type = anomalyResponse.labelsMap.find(label => label[0] === 'metric_key')[1]; + let anomaliesList = anomalyResponse.anomaliesList; + + fullAnomalyList.slice(baseIterationIndex + 1, fullAnomalyList.length).forEach((alertToCheck, checkIndex) => { + if (sameOperationAndType(alertToCheck, operationName, type)) { + anomaliesList = _.merge(anomaliesList, alertToCheck.anomaliesList); + fullAnomalyList[baseIterationIndex + checkIndex + 1] = null; + } + }); + + const latestUnhealthy = _.maxBy(anomaliesList, anomaly => anomaly.timestamp); + const timestamp = latestUnhealthy && latestUnhealthy.timestamp * 1000; + const isUnhealthy = (timestamp && timestamp >= (Date.now() - (alertFreqInSec * 1000))); + + return { + operationName, + type, + isUnhealthy, + timestamp + }; + } + + return null; }); + + return _.filter(mappedAndMergedResponse, a => a !== null); } -function fetchOperationAlerts(serviceName, interval, from) { +function fetchAlerts(serviceName, interval, from, stat, key) { const request = new messages.SearchAnamoliesRequest(); request.getLabelsMap() .set('serviceName', metricpointNameEncoder.encodeMetricpointName(decodeURIComponent(serviceName))) .set('interval', interval) .set('mtype', 'gauge') - .set('product', 'haystack'); - request.setStarttime(from); - request.setEndtime(Date.now()); + .set('product', 'haystack') + .set('stat', stat) + .set('metric_key', key); + request.setStarttime(Math.trunc(from / 1000)); + request.setEndtime(Math.trunc(Date.now() / 1000)); + request.setSize(-1); return getAnomaliesFetcher .fetch(request) .then(pbResult => parseOperationAlertsResponse(messages.SearchAnomaliesResponse.toObject(false, pbResult))); } -function mergeOperationsWithAlerts({operationAlerts, operations}) { - return _.flatten(operations.map(operation => alertTypes.map((alertType) => { - const operationAlert = operationAlerts.find(alert => (alert.operationName.toLowerCase() === operation.toLowerCase() && alert.type === alertType)); +function fetchOperationAlerts(serviceName, interval, from) { + return Q.all([fetchAlerts(serviceName, interval, from, '*_99', 'duration'), fetchAlerts(serviceName, interval, from, 'count', 'failure-span')]) + .then(stats => (_.merge(stats[0], stats[1]))); +} - if (operationAlert !== undefined) { +function mergeOperationsWithAlerts({operationAlerts, operations}) { + if (operations && operations.length) { + return _.flatten(operations.map(operation => alertTypes.map((alertType) => { + const operationAlert = operationAlerts.find(alert => (alert.operationName.toLowerCase() === operation.toLowerCase() && alert.type === alertType)); + + if (operationAlert !== undefined) { + return { + ...operationAlert + }; + } return { - ...operationAlert + operationName: operation, + type: alertType, + isUnhealthy: false, + timestamp: null }; - } - return { - operationName: operation, - type: alertType, - isUnhealthy: false, - timestamp: null - }; - }))); + }))); + } + + return _.flatten(alertTypes.map(alertType => (_.filter(operationAlerts, alert => (alert.type === alertType))))); } function returnAnomalies(data) { - if (!data || !data.length || !data[0].length) { + if (!data || !data.length || !data[0].anomaliesList.length) { return []; } - return data[0].anomalies; + return _.flatten(data.map((anomaly) => { + const strength = anomaly.labelsMap.find(label => label[0] === 'anomalyLevel')[1]; + return anomaly.anomaliesList.map(a => ({strength, ...a})); + })); } function getActiveAlertCount(operationAlerts) { @@ -113,7 +150,7 @@ function getActiveAlertCount(operationAlerts) { connector.getServiceAlerts = (serviceName, interval) => { // todo: calculate "from" value based on selected interval - const oneDayAgo = Math.trunc(Date.now() - (24 * 60 * 60 * 1000)); + const oneDayAgo = Math.trunc((Date.now() - (24 * 60 * 60 * 1000))); return Q.all([fetchOperations(serviceName), fetchOperationAlerts(serviceName, interval, oneDayAgo)]) .then(stats => mergeOperationsWithAlerts({ operations: stats[0], @@ -130,20 +167,21 @@ connector.getAnomalies = (serviceName, operationName, alertType, from, interval) .set('serviceName', metricpointNameEncoder.encodeMetricpointName(decodeURIComponent(serviceName))) .set('operationName', metricpointNameEncoder.encodeMetricpointName(decodeURIComponent(operationName))) .set('product', 'haystack') - .set('name', alertType) + .set('metric_key', alertType) .set('stat', stat) .set('interval', interval) .set('mtype', 'gauge'); - request.setStarttime(from); - request.setEndtime(Date.now()); + request.setStarttime(Math.trunc(from / 1000)); + request.setEndtime(Math.trunc(Date.now() / 1000)); + request.setSize(-1); return getAnomaliesFetcher .fetch(request) - .then(pbResult => returnAnomalies(messages.SearchAnomaliesResponse.toObject(false, pbResult))); + .then(pbResult => returnAnomalies(messages.SearchAnomaliesResponse.toObject(false, pbResult).searchanomalyresponseList)); }; -connector.getServiceUnhealthyAlertCount = serviceName => - fetchOperationAlerts(serviceName, '5m', Math.trunc(Date.now() - (5 * 60 * 1000))) +connector.getServiceUnhealthyAlertCount = (serviceName, interval) => + fetchOperationAlerts(serviceName, interval, Math.trunc((Date.now() - (5 * 60 * 1000)))) .then(result => getActiveAlertCount(result)); module.exports = connector; diff --git a/server/connectors/alerts/haystack/subscriptionsConnector.js b/server/connectors/alerts/haystack/subscriptionsConnector.js index 6f0c4d0a..a16d8375 100644 --- a/server/connectors/alerts/haystack/subscriptionsConnector.js +++ b/server/connectors/alerts/haystack/subscriptionsConnector.js @@ -24,11 +24,11 @@ const putter = require('../../operations/grpcPutter'); const deleter = require('../../operations/grpcDeleter'); const poster = require('../../operations/grpcPoster'); -const grpcOptions = { - 'grpc.max_receive_message_length': 10485760, // todo: do I need these? - ...config.connectors.traces.grpcOptions -}; +const grpcOptions = config.grpcOptions || {}; + +const MetricpointNameEncoder = require('../../utils/encoders/MetricpointNameEncoder'); +const metricpointNameEncoder = new MetricpointNameEncoder(config.encoder); const client = new services.SubscriptionManagementClient( `${config.connectors.alerts.haystackHost}:${config.connectors.alerts.haystackPort}`, @@ -84,9 +84,9 @@ connector.searchSubscriptions = (serviceName, operationName, alertType, interval const request = new messages.SearchSubscriptionRequest(); request.getLabelsMap() - .set('serviceName', decodeURIComponent(serviceName)) - .set('operationName', decodeURIComponent(operationName)) - .set('type', alertType) + .set('serviceName', metricpointNameEncoder.encodeMetricpointName(decodeURIComponent(serviceName))) + .set('operationName', metricpointNameEncoder.encodeMetricpointName(decodeURIComponent(operationName))) + .set('metric_key', alertType) .set('stat', stat) .set('interval', interval) .set('product', 'haystack') @@ -96,7 +96,6 @@ connector.searchSubscriptions = (serviceName, operationName, alertType, interval .fetch(request) .then((result) => { const pbResult = messages.SearchSubscriptionResponse.toObject(false, result); - console.log(pbResult.subscriptionresponseList.map(pbSubResponse => converter.toSubscriptionJson(pbSubResponse))); return pbResult.subscriptionresponseList.map(pbSubResponse => converter.toSubscriptionJson(pbSubResponse)); }); }; diff --git a/server/connectors/alerts/stub/alertsConnector.js b/server/connectors/alerts/stub/alertsConnector.js index cf13ae90..82b16d7d 100644 --- a/server/connectors/alerts/stub/alertsConnector.js +++ b/server/connectors/alerts/stub/alertsConnector.js @@ -17,19 +17,20 @@ const Q = require('q'); function getRandomTimeStamp() { - const currentTime = ((new Date()).getTime()) * 1000; - return (currentTime - Math.floor((Math.random() * 5000 * 60 * 1000))); + const currentTime = ((new Date()).getTime()); + return (currentTime - Math.floor((Math.random() * 5000 * 60))); } function generateAnomaly() { - const currentTime = ((new Date()).getTime()) * 1000; - const timestamp = (currentTime - Math.floor((Math.random() * 2000000 * 60 * 1000))); - const expectedValue = Math.floor(Math.random() * 100000); - const observedValue = Math.floor(expectedValue * (Math.random() * 100)); + const currentTime = ((new Date()).getTime() / 1000); + const timestamp = (currentTime - Math.floor((Math.random() * 2000 * 60))); + const expectedvalue = Math.floor(Math.random() * 100000); + const observedvalue = Math.floor(expectedvalue * (Math.random() * 100)); return { - observedValue, - expectedValue, - timestamp + observedvalue, + expectedvalue, + timestamp, + strength: observedvalue % 2 ? 'STRONG' : 'WEAK' }; } @@ -37,55 +38,48 @@ function getAlerts() { return [ { operationName: 'tarley-1', - type: 'count', + type: 'duration', isUnhealthy: true, timestamp: getRandomTimeStamp() }, { operationName: 'tarley-1', - type: 'durationTP99', + type: 'failure-span', isUnhealthy: true, timestamp: getRandomTimeStamp() }, { - operationName: 'tarley-1', - type: 'failureCount', + operationName: 'tully-1', + type: 'duration', isUnhealthy: false, timestamp: getRandomTimeStamp() }, { operationName: 'tully-1', - type: 'count', + type: 'failure-span', isUnhealthy: false, timestamp: getRandomTimeStamp() }, { operationName: 'tully-1', - type: 'durationTP99', + type: 'duration', isUnhealthy: false, timestamp: getRandomTimeStamp() - }, - { + }, { operationName: 'tully-1', - type: 'failureCount', + type: 'failure-span', isUnhealthy: false, timestamp: getRandomTimeStamp() }, { operationName: 'dondarrion-1', - type: 'count', - isUnhealthy: true, - timestamp: getRandomTimeStamp() - }, - { - operationName: 'dondarrion-1', - type: 'durationTP99', + type: 'duration', isUnhealthy: false, timestamp: getRandomTimeStamp() }, { operationName: 'dondarrion-1', - type: 'failureCount', + type: 'failure-span', isUnhealthy: false, timestamp: getRandomTimeStamp() } diff --git a/server/connectors/alerts/stub/subscriptionsConnector.js b/server/connectors/alerts/stub/subscriptionsConnector.js index b7ef2d41..301434be 100644 --- a/server/connectors/alerts/stub/subscriptionsConnector.js +++ b/server/connectors/alerts/stub/subscriptionsConnector.js @@ -22,10 +22,6 @@ const subscriptions = (serviceName, operationName, alertType, interval) => ( subscriptionId: 101, user: {userName: 'haystack-team'}, dispatchersList: [ - { - type: 0, - endpoint: 'haystack@expedia.com' - }, { type: 1, endpoint: '#haystack' @@ -34,7 +30,7 @@ const subscriptions = (serviceName, operationName, alertType, interval) => ( expressionTree: { serviceName, operationName, - name: alertType, + metric_key: alertType, interval, stat: alertType === 'failure-span' ? 'count' : '*_99', mtype: 'gauge', @@ -48,16 +44,12 @@ const subscriptions = (serviceName, operationName, alertType, interval) => ( { type: 0, endpoint: 'haystack@opentracing.io' - }, - { - type: 1, - endpoint: '#haystack-tracing' } ], expressionTree: { serviceName, operationName, - name: alertType, + metric_key: alertType, interval, stat: alertType === 'failure-span' ? 'count' : '*_99', mtype: 'gauge', diff --git a/server/connectors/traces/haystack/tracesConnector.js b/server/connectors/traces/haystack/tracesConnector.js index d073f305..b22b38c4 100644 --- a/server/connectors/traces/haystack/tracesConnector.js +++ b/server/connectors/traces/haystack/tracesConnector.js @@ -34,10 +34,7 @@ const trendsConnector = config.connectors.trends && require(`../../trends/${conf const services = require('../../../../static_codegen/traceReader_grpc_pb'); -const grpcOptions = { - 'grpc.max_receive_message_length': 10485760, - ...config.connectors.traces.grpcOptions -}; +const grpcOptions = config.grpcOptions || {}; const client = new services.TraceReaderClient( diff --git a/server/routes/alertsApi.js b/server/routes/alertsApi.js index ecb3be96..afb12de8 100644 --- a/server/routes/alertsApi.js +++ b/server/routes/alertsApi.js @@ -31,7 +31,7 @@ router.get('/alerts/:serviceName', (req, res, next) => { router.get('/alerts/:serviceName/unhealthyCount', (req, res, next) => { handleResponsePromise(res, next, 'alerts_SVC_unhealthyCount')( - () => alertsConnector.getServiceUnhealthyAlertCount(req.params.serviceName) + () => alertsConnector.getServiceUnhealthyAlertCount(req.params.serviceName, req.query.interval) ); }); diff --git a/server/routes/index.js b/server/routes/index.js index 9ae03a21..9be79a40 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -40,7 +40,7 @@ router.get('*', (req, res) => { enableServiceLevelTrends: config.connectors.trends && config.connectors.trends.enableServiceLevelTrends, enableSSO: config.enableSSO, refreshInterval: config.refreshInterval, - enableAlertSubscriptions: config.connectors.alerts && config.connectors.alerts.subscriptions.enabled, + enableAlertSubscriptions: config.connectors.alerts && config.connectors.alerts.subscriptions, tracesTimePresetOptions: config.connectors.traces && config.connectors.traces.timePresetOptions, timeWindowPresetOptions: config.timeWindowPresetOptions, tracesTTL: config.connectors.traces && config.connectors.traces.ttl, diff --git a/src/components/alerts/alertCounter.jsx b/src/components/alerts/alertCounter.jsx index e48b4430..8128d202 100644 --- a/src/components/alerts/alertCounter.jsx +++ b/src/components/alerts/alertCounter.jsx @@ -27,19 +27,24 @@ const alertsRefreshInterval = (window.haystackUiConfig && window.haystackUiConfi @observer export default class AlertCounter extends React.Component { static propTypes = { - serviceName: PropTypes.string.isRequired + serviceName: PropTypes.string.isRequired, + interval: PropTypes.string + }; + + static defaultProps = { + interval: 'FiveMinute' }; componentDidMount() { - alertsStore.fetchUnhealthyAlertCount(this.props.serviceName); + alertsStore.fetchUnhealthyAlertCount(this.props.serviceName, this.props.interval); this.timerID = setInterval( - () => alertsStore.fetchUnhealthyAlertCount(this.props.serviceName), + () => alertsStore.fetchUnhealthyAlertCount(this.props.serviceName, this.props.interval), alertsRefreshInterval ); } componentWillReceiveProps(nextProp) { - alertsStore.fetchUnhealthyAlertCount(nextProp.serviceName); + alertsStore.fetchUnhealthyAlertCount(nextProp.serviceName, nextProp.interval); } componentWillUnmount() { diff --git a/src/components/alerts/alertTabs.jsx b/src/components/alerts/alertTabs.jsx index 5ee32350..e2a4fcd6 100644 --- a/src/components/alerts/alertTabs.jsx +++ b/src/components/alerts/alertTabs.jsx @@ -32,16 +32,16 @@ export default class AlertTabs extends React.Component { static tabViewer(tabSelected, groupedAlerts, serviceName, location, defaultPreset, interval) { switch (tabSelected) { case 2: - return ; + return ; default: - return ; + return ; } } constructor(props) { super(props); const query = toQuery(this.props.location.search); - const tabSelected = (query.type === 'durationTP99') ? 2 : 1; + const tabSelected = (query.type === 'duration') ? 2 : 1; this.state = { tabSelected @@ -63,8 +63,8 @@ export default class AlertTabs extends React.Component { } = this.props; const groupedAlerts = _.groupBy(this.props.alertsStore.alerts, _.property('type')); - const unhealthyFailureCountAlerts = groupedAlerts.failureCount.filter(alert => alert.isUnhealthy).length; - const unhealthyDurationTP99Alerts = groupedAlerts.durationTP99.filter(alert => alert.isUnhealthy).length; + const unhealthyFailureCountAlerts = groupedAlerts['failure-span'] && groupedAlerts['failure-span'].filter(alert => alert.isUnhealthy).length; + const unhealthyDurationTP99Alerts = groupedAlerts.duration && groupedAlerts.duration.filter(alert => alert.isUnhealthy).length; return (
@@ -73,13 +73,17 @@ export default class AlertTabs extends React.Component {
  • this.toggleTab(1)} > Failure Count - {unhealthyFailureCountAlerts} + + {unhealthyFailureCountAlerts > 0 && unhealthyFailureCountAlerts} +
  • this.toggleTab(2)} > Duration TP99 - {unhealthyDurationTP99Alerts} + + {unhealthyDurationTP99Alerts > 0 && unhealthyDurationTP99Alerts} +
  • diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx index 27dbcf78..f1fe6247 100644 --- a/src/components/alerts/alerts.jsx +++ b/src/components/alerts/alerts.jsx @@ -28,11 +28,15 @@ export default class Alerts extends React.Component { static propTypes = { location: PropTypes.object.isRequired, alertsStore: PropTypes.object.isRequired, - serviceName: PropTypes.string.isRequired, history: PropTypes.object.isRequired, defaultPreset: PropTypes.object.isRequired, - interval: PropTypes.string.isRequired, - updateInterval: PropTypes.func.isRequired + serviceName: PropTypes.string.isRequired, + interval: PropTypes.string + }; + + // Default interval to FiveMinute if none specified in search + static defaultProps = { + interval: 'FiveMinute' }; render() { @@ -45,7 +49,6 @@ export default class Alerts extends React.Component { location={this.props.location} serviceName={this.props.serviceName} interval={this.props.interval} - updateInterval={this.props.updateInterval} /> { this.props.alertsStore.promiseState && this.props.alertsStore.promiseState.case({ pending: () => , diff --git a/src/components/alerts/alerts.less b/src/components/alerts/alerts.less index b8fa2a38..d9772f91 100644 --- a/src/components/alerts/alerts.less +++ b/src/components/alerts/alerts.less @@ -168,3 +168,33 @@ margin-right: 15px; } + +.dispatcher-header { + display: inline; +} + +.dispatcher-row { + margin-top: 5px; + margin-bottom: 5px; +} + +.subscription-select { + margin-right: 15px; + padding-left: 4px; + width: 80px !important; // Override bootstrap +} + +.dispatcher-input { + margin-right: 15px; + width: 55% !important; // Override bootstrap +} + +.subscription-form { + // bootstrap overrides + display: inline !important; + color: @gray-darker !important; +} + +.default-cursor { + cursor: default !important; +} \ No newline at end of file diff --git a/src/components/alerts/alertsTable.jsx b/src/components/alerts/alertsTable.jsx index 5972a3c3..7a446379 100644 --- a/src/components/alerts/alertsTable.jsx +++ b/src/components/alerts/alertsTable.jsx @@ -68,7 +68,7 @@ export default class AlertsTable extends React.Component { static timestampColumnFormatter(timestamp) { if (timestamp) { - return `
    ${formatters.toTimeago(timestamp)} at ${formatters.toTimestring(timestamp)}
    `; + return `
    ${formatters.toTimeago(timestamp * 1000)} at ${formatters.toTimestring(timestamp * 1000)}
    `; } return '
    '; } @@ -113,11 +113,9 @@ export default class AlertsTable extends React.Component { } static toAlertTypeString = (num) => { - if (num === 'count') { - return 'Total Count'; - } else if (num === 'durationTP99') { + if (num === 'duration') { return 'Duration TP99'; - } else if (num === 'failureCount') { + } else if (num === 'failure-span') { return 'Failure Count'; } @@ -125,13 +123,12 @@ export default class AlertsTable extends React.Component { }; static toTrendTypeString = (alertType) => { - if (alertType === 'count') { - return 'count'; - } else if (alertType === 'durationTP99') { + if (alertType === 'duration') { return 'tp99Duration'; - } else if (alertType === 'failureCount') { + } else if (alertType === 'failure-span') { return 'failureCount'; } + return null; }; diff --git a/src/components/alerts/alertsToolbar.jsx b/src/components/alerts/alertsToolbar.jsx index 8d2ef8b2..fb057879 100644 --- a/src/components/alerts/alertsToolbar.jsx +++ b/src/components/alerts/alertsToolbar.jsx @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import {observer} from 'mobx-react'; import timeWindow from '../../utils/timeWindow'; +import granularityMetrics from '../trends/utils/metricGranularity'; import {toQuery, toQueryUrlString} from '../../utils/queryParser'; import './alerts'; @@ -33,8 +34,7 @@ export default class AlertsToolbar extends React.Component { alertsStore: PropTypes.object.isRequired, history: PropTypes.object.isRequired, defaultPreset: PropTypes.object.isRequired, - interval: PropTypes.string.isRequired, - updateInterval: PropTypes.func.isRequired + interval: PropTypes.string.isRequired }; constructor(props) { @@ -43,6 +43,7 @@ export default class AlertsToolbar extends React.Component { const query = toQuery(this.props.location.search); const activeWindow = query.preset ? timeWindow.presets.find(presetItem => presetItem.shortName === query.preset) : this.props.defaultPreset; this.state = { + interval: this.props.interval, options: timeWindow.presets, activeWindow, autoRefreshTimer: new Date(), @@ -61,7 +62,7 @@ export default class AlertsToolbar extends React.Component { componentWillReceiveProps(nextProps) { const query = toQuery(nextProps.location.search); const activeWindow = query.preset ? timeWindow.presets.find(presetItem => presetItem.shortName === query.preset) : nextProps.defaultPreset; - this.setState({activeWindow, autoRefresh: false}); + this.setState({activeWindow, autoRefresh: false, interval: nextProps.interval || 'FiveMinute'}); this.stopRefresh(); } @@ -131,27 +132,33 @@ export default class AlertsToolbar extends React.Component { } handleIntervalChange(event) { - const newInterval = event.target.value; - this.props.updateInterval(newInterval); + const query = toQuery(this.props.location.search); + query.interval = event.target.value; + + const queryUrl = toQueryUrlString(query); + if (queryUrl !== this.props.location.search) { + this.props.history.push({ + search: queryUrl + }); + } } render() { const countDownMiliSec = (this.state.countdownTimer && this.state.autoRefreshTimer) && (refreshInterval - (this.state.countdownTimer.getTime() - this.state.autoRefreshTimer.getTime())); + return (
    - Alert Interval + Metric Interval
    diff --git a/src/components/alerts/details/alertDetails.jsx b/src/components/alerts/details/alertDetails.jsx index 8f2b2c64..b52662cd 100644 --- a/src/components/alerts/details/alertDetails.jsx +++ b/src/components/alerts/details/alertDetails.jsx @@ -42,6 +42,13 @@ export default class AlertDetails extends React.Component { this.props.alertDetailsStore.fetchAlertHistory(this.props.serviceName, this.props.operationName, this.props.type, Date.now() - AlertDetails.historyWindow, this.props.interval); } + componentWillReceiveProps(nextProps) { + if (nextProps.type !== this.props.type) { + this.props.alertDetailsStore.fetchAlertSubscriptions(nextProps.serviceName, nextProps.operationName, nextProps.type, nextProps.interval); + this.props.alertDetailsStore.fetchAlertHistory(nextProps.serviceName, nextProps.operationName, nextProps.type, Date.now() - AlertDetails.historyWindow, nextProps.interval); + } + } + render() { const enableAlertSubscriptions = window.haystackUiConfig.enableAlertSubscriptions; @@ -59,6 +66,7 @@ export default class AlertDetails extends React.Component { fulfilled: () => () diff --git a/src/components/alerts/details/alertDetailsToolbar.jsx b/src/components/alerts/details/alertDetailsToolbar.jsx index 85ae9526..d490a25a 100644 --- a/src/components/alerts/details/alertDetailsToolbar.jsx +++ b/src/components/alerts/details/alertDetailsToolbar.jsx @@ -23,6 +23,8 @@ import Clipboard from 'react-copy-to-clipboard'; import linkBuilder from '../../../utils/linkBuilder'; +const tracesEnabled = window.haystackUiConfig.subsystems.includes('traces'); + @observer export default class AlertDetailsToolbar extends React.Component { static propTypes = { @@ -68,13 +70,15 @@ export default class AlertDetailsToolbar extends React.Component {
    - - See Traces - + { tracesEnabled && + + See Traces + + } alert.timestamp, ['desc']); return ( -
    +
    -

    History - ({sortedHistoryResults.length} times unhealthy in last {this.props.historyWindow / 86400000} day) +

    Anomaly History + ({sortedHistoryResults.length} anomalies in the last {this.props.historyWindow / 86400000} day)

    - - - - + + + + + {sortedHistoryResults.length ? sortedHistoryResults.map(alert => ( - - - + + + + )) diff --git a/src/components/alerts/details/alertSubscriptions.jsx b/src/components/alerts/details/alertSubscriptions.jsx index 9e2984a2..ed78fba6 100644 --- a/src/components/alerts/details/alertSubscriptions.jsx +++ b/src/components/alerts/details/alertSubscriptions.jsx @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import {observer} from 'mobx-react'; import SubscriptionRow from './subscriptionRow'; -import SubscriptionModal from './subscriptionModal'; +import newSubscriptionConstructor from '../utils/subscriptionConstructor'; @observer export default class AlertSubscriptions extends React.Component { @@ -32,27 +32,56 @@ export default class AlertSubscriptions extends React.Component { interval: PropTypes.string.isRequired }; + static handleInputKeypress(e, escapeCallback, enterCallback) { + if (e.keyCode === 27) { + escapeCallback(); + } else if (e.keyCode === 13) { + enterCallback(); + } + } + constructor(props) { super(props); this.state = { - modalIsOpen: false, - subscriptionError: false + showNewSubscriptionBox: false, + subscriptionError: false, + // subscriptionErrorMessage: null, + selectValue: '1', + inputValue: '' }; - this.openModal = this.openModal.bind(this); - this.closeModal = this.closeModal.bind(this); this.submitNewSubscription = this.submitNewSubscription.bind(this); this.handleSubscriptionSuccess = this.handleSubscriptionSuccess.bind(this); this.handleSubscriptionError = this.handleSubscriptionError.bind(this); + this.toggleNewSubscriptionBox = this.toggleNewSubscriptionBox.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleSelectChange = this.handleSelectChange.bind(this); } - openModal() { - this.setState({modalIsOpen: true}); + + handleInputChange(e) { + this.setState({ + inputValue: e.target.value + }); } - closeModal() { - this.setState({modalIsOpen: false}); + handleSelectChange(e) { + this.setState({ + selectValue: e.target.value + }); + } + + + toggleNewSubscriptionBox() { + this.setState(prevState => ({ + showNewSubscriptionBox: !prevState.showNewSubscriptionBox, + subscriptionError: false + })); + this.setState({ + inputValue: '', + selectValue: '1' + }); } handleSubscriptionSuccess() { @@ -63,29 +92,47 @@ export default class AlertSubscriptions extends React.Component { }); } - handleSubscriptionError() { + handleSubscriptionError(subscriptionErrorMessage) { this.setState({ - subscriptionError: true + subscriptionError: true, + subscriptionErrorMessage }); - setTimeout(() => this.setState({subscriptionError: false}), 4000); + setTimeout(() => this.setState({subscriptionError: false, subscriptionErrorMessage: null}), 4000); } - submitNewSubscription(subscription) { + submitNewSubscription() { + const trimmedInput = this.state.inputValue.trim(); + if (!trimmedInput.length) { + this.handleSubscriptionError('Dispatcher endpoint cannot be empty.'); + return; + } + const dispatchers = [{type: this.state.selectValue, endpoint: trimmedInput}]; + + const subscription = newSubscriptionConstructor( + this.props.serviceName, + this.props.operationName, + this.props.type, + this.props.interval, + dispatchers, + ); + this.props.alertDetailsStore.addNewSubscription( subscription, this.handleSubscriptionSuccess, - this.handleSubscriptionError + this.handleSubscriptionError('Error creating subscription.') ); } render() { const { - subscriptionError + subscriptionError, + showNewSubscriptionBox, + subscriptionErrorMessage } = this.state; const alertSubscriptions = this.props.alertDetailsStore.alertSubscriptions; return ( -
    +

    Subscriptions

    TimestampObserved ValueExpected ValueTrends & TracesTimestampAnomaly StrengthObserved ValueExpected Value{tracesEnabled ? 'Trends & Traces' : 'Trends'}
    {AlertHistory.timeAgoFormatter(alert.timestamp)} at {AlertHistory.timestampFormatter(alert.timestamp)}{alert.observedValue}{alert.expectedValue}{AlertHistory.timeAgoFormatter(alert.timestamp * 1000)} at {AlertHistory.timestampFormatter(alert.timestamp * 1000)}{alert.strength}{alert.observedvalue && AlertHistory.valueFormatter(alert.observedvalue, this.props.type)}{alert.expectedvalue && AlertHistory.valueFormatter(alert.expectedvalue, this.props.type)}
    - + - - - + { tracesEnabled && + + + + }
    @@ -105,28 +152,46 @@ export default class AlertSubscriptions extends React.Component { successCallback={this.handleSubscriptionSuccess} errorCallback={this.handleSubscriptionError} />)) - : + : } - + + + +
    No Subscriptions Found
    No Subscriptions Found
    + + AlertSubscriptions.handleInputKeypress(event, this.toggleNewSubscriptionBox, this.handleSubmit)} + placeholder={this.state.selectValue === '1' ? 'Public Slack Channel' : 'Email Address'} + name="endpoint" + /> + +
    + +
    +
    - + {showNewSubscriptionBox ? + : + + }
    - {/* TODO: more verbose errors */} - Could not process subscription. Please try again. + {subscriptionErrorMessage || 'Could not process subscription. Please try again.'}
    ); diff --git a/src/components/alerts/details/subscriptionModal.jsx b/src/components/alerts/details/subscriptionModal.jsx deleted file mode 100644 index 32e2651d..00000000 --- a/src/components/alerts/details/subscriptionModal.jsx +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2018 Expedia Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import {observer} from 'mobx-react'; - -import Modal from '../../common/modal'; -import newSubscriptionConstructor from '../utils/subscriptionConstructor'; -import './subscriptionModal.less'; - -@observer -export default class SubscriptionModal extends React.Component { - static propTypes = { - isOpen: PropTypes.bool.isRequired, - closeModal: PropTypes.func.isRequired, - serviceName: PropTypes.string.isRequired, - operationName: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - submitCallback: PropTypes.func.isRequired, - interval: PropTypes.string.isRequired, - dispatchers: PropTypes.array - }; - - static defaultProps = { - dispatchers: [] - }; - - constructor(props) { - super(props); - this.state = { - dispatchers: this.props.dispatchers - }; - - this.editDispatcher = this.editDispatcher.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.addDispatcher = this.addDispatcher.bind(this); - this.deleteDispatcher = this.deleteDispatcher.bind(this); - } - - editDispatcher(event, dispatcherIndex) { - const target = event.target; - const value = target.value; - const name = target.name; - const dispatchers = this.state.dispatchers; - - dispatchers[dispatcherIndex][name] = value; - - this.setState({ - dispatchers - }); - } - - handleSubmit() { - const dispatchers = this.state.dispatchers; - - const subscription = newSubscriptionConstructor( - this.props.serviceName, - this.props.operationName, - this.props.type, - this.props.interval, - dispatchers, - ); - - this.props.submitCallback(subscription); - } - - addDispatcher(e) { - e.preventDefault(); - const dispatchers = this.state.dispatchers; - if (dispatchers.length && dispatchers[dispatchers.length - 1].endpoint === '') { - return; // prevents multiple empty dispatcher boxes - } - dispatchers.push({type: '1', endpoint: ''}); - - this.setState({ - dispatchers - }); - } - - deleteDispatcher(e, index) { - e.preventDefault(); - const dispatchers = this.state.dispatchers; - dispatchers.splice(index, 1); - - this.setState({ - dispatchers - }); - } - - render() { - const {isOpen, closeModal, title} = this.props; - - return ( - -
    -
    -
    Dispatchers:
    - - {this.state.dispatchers.map((dispatcher, index) => ( -
    - - this.editDispatcher(e, index)} - name="endpoint" - /> - -
    - ) - )} -
    -
    - -
    -
    -
    ); - } -} diff --git a/src/components/alerts/details/subscriptionModal.less b/src/components/alerts/details/subscriptionModal.less deleted file mode 100644 index ec40274b..00000000 --- a/src/components/alerts/details/subscriptionModal.less +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2018 Expedia Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@import (reference) '../../../app'; - -.dispatcher-header { - display: inline; -} - -.dispatcher-row { - margin-top: 5px; - margin-bottom: 5px; -} - -.subscription-select { - margin-right: 15px; - padding-left: 4px; - width: 80px !important; // Override bootstrap -} - -.dispatcher-input { - margin-right: 15px; - width: 55% !important; // Override bootstrap - -} - -.subscription-form { - // bootstrap overrides - display: inline !important; - color: @gray-darker !important; -} \ No newline at end of file diff --git a/src/components/alerts/details/subscriptionRow.jsx b/src/components/alerts/details/subscriptionRow.jsx index fcc71255..f3870c78 100644 --- a/src/components/alerts/details/subscriptionRow.jsx +++ b/src/components/alerts/details/subscriptionRow.jsx @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {observer} from 'mobx-react'; -import SubscriptionModal from './subscriptionModal'; +import newSubscriptionConstructor from '../utils/subscriptionConstructor'; @observer export default class SubscriptionRow extends React.Component { @@ -31,90 +31,128 @@ export default class SubscriptionRow extends React.Component { errorCallback: PropTypes.func.isRequired }; - - static getDispatcherType(dispatcher) { - if (dispatcher.type === 1) { - return
    Slack Slack
    ; - } - return
    Email
    ; - } - constructor(props) { super(props); this.state = { - modalIsOpen: false + modifySubscription: false, + dispatcher: JSON.parse(JSON.stringify(this.props.subscription.dispatchersList[0])) // deep copy, so client side changes don't affect store }; - this.openModal = this.openModal.bind(this); - this.closeModal = this.closeModal.bind(this); this.handleDeleteSubscription = this.handleDeleteSubscription.bind(this); this.handleSubmitModifiedSubscription = this.handleSubmitModifiedSubscription.bind(this); + this.handleCancelModifiedSubscription = this.handleCancelModifiedSubscription.bind(this); + this.handleModifySubscription = this.handleModifySubscription.bind(this); + this.updateDispatcherType = this.updateDispatcherType.bind(this); + this.updateDispatcherEndpoint = this.updateDispatcherEndpoint.bind(this); } - openModal() { - this.setState({modalIsOpen: true}); - } + handleSubmitModifiedSubscription() { + const oldSubscription = this.props.subscription; + const dispatcher = this.state.dispatcher; + dispatcher.endpoint = dispatcher.endpoint.trim(); + if (!dispatcher.endpoint.length) { + this.props.errorCallback('Updated endpoint cannot be empty.'); + return; + } + const dispatchers = [dispatcher]; - closeModal() { - this.setState({modalIsOpen: false}); - } + const {serviceName, operationName, type, interval} = this.props.subscription.expressionTree; - handleSubmitModifiedSubscription(modifiedSubscription) { - const oldSubscription = this.props.subscription; + const modifiedSubscription = newSubscriptionConstructor( + serviceName, + operationName, + type, + interval, + dispatchers, + ); const subscriptions = {old: oldSubscription, modified: modifiedSubscription}; this.props.alertDetailsStore.updateSubscription( subscriptions, this.props.successCallback, - this.props.errorCallback + this.props.errorCallback('Error updating subscription.') ); this.setState({activeModifyInput: null}); } + handleCancelModifiedSubscription() { + this.setState({ + modifySubscription: false, + dispatcher: JSON.parse(JSON.stringify(this.props.subscription.dispatchersList[0])) + }); + } + + handleModifySubscription() { + this.setState({ + modifySubscription: true + }); + } + handleDeleteSubscription(subscriptionId) { this.props.alertDetailsStore.deleteSubscription( subscriptionId ); } + updateDispatcherType(e) { + this.setState({dispatcher: {type: e.target.value, endpoint: this.state.dispatcher.endpoint}}); + } + + updateDispatcherEndpoint(e) { + this.setState({dispatcher: {type: this.state.dispatcher.type, endpoint: e.target.value}}); + } + + render() { - const subscription = JSON.parse(JSON.stringify(this.props.subscription)); // deep copy, so client side changes don't affect store - const {serviceName, operationName, type, interval} = subscription.expressionTree; - - const SubscriptionButtons = () => ( -
    - - -
    ); + const dispatcher = this.state.dispatcher; + + const HandleSubscriptionModifyButtons = () => (
    + + +
    ); + + const DefaultSubscriptionButtons = () => (
    + + +
    ); return ( - - {subscription.dispatchersList.map(dispatcher => ( -
    - {SubscriptionRow.getDispatcherType(dispatcher)}: {dispatcher.endpoint} -
    -
    - ) - )} + +
    + + +
    +
    - - + { + this.state.modifySubscription ? : + } diff --git a/src/components/alerts/stores/serviceAlertsStore.js b/src/components/alerts/stores/serviceAlertsStore.js index 9d7fab27..f5aba815 100644 --- a/src/components/alerts/stores/serviceAlertsStore.js +++ b/src/components/alerts/stores/serviceAlertsStore.js @@ -25,9 +25,9 @@ export class ServiceAlertsStore extends ErrorHandlingStore { @observable alerts = []; @observable promiseState = null; - @action fetchUnhealthyAlertCount(serviceName) { + @action fetchUnhealthyAlertCount(serviceName, interval) { axios - .get(`/api/alerts/${encodeURIComponent(serviceName)}/unhealthyCount`) + .get(`/api/alerts/${encodeURIComponent(serviceName)}/unhealthyCount?interval=${interval}`) .then((result) => { this.unhealthyAlertCount = result.data; }) @@ -43,7 +43,7 @@ export class ServiceAlertsStore extends ErrorHandlingStore { this.promiseState = fromPromise( axios .get(`/api/alerts/${serviceName}?${timeFrameString}`) - .then((result) => { + .then((result) => { this.alerts = result.data; }) .catch((result) => { diff --git a/src/components/alerts/utils/subscriptionConstructor.js b/src/components/alerts/utils/subscriptionConstructor.js index cdf0f06b..f72a03d6 100644 --- a/src/components/alerts/utils/subscriptionConstructor.js +++ b/src/components/alerts/utils/subscriptionConstructor.js @@ -21,8 +21,8 @@ export default function newSubscriptionConstructor(serviceName, operationName, t expressionTree: { serviceName, operationName, - type, - stat: name === 'failure-span' ? 'count' : '*_99', + metric_key: type, + stat: type === 'failure-span' ? 'count' : '*_99', interval, mtype: 'gauge', product: 'haystack' diff --git a/src/components/trends/details/trendDetailsToolbar.jsx b/src/components/trends/details/trendDetailsToolbar.jsx index 0dfc6bc0..94d8ff5d 100644 --- a/src/components/trends/details/trendDetailsToolbar.jsx +++ b/src/components/trends/details/trendDetailsToolbar.jsx @@ -26,6 +26,7 @@ import './trendDetailsToolbar.less'; import TrendTimeRangePicker from '../../common/timeRangePicker'; const refreshInterval = (window.haystackUiConfig && window.haystackUiConfig.refreshInterval); +const tracesEnabled = window.haystackUiConfig.subsystems.includes('traces'); export default class TrendDetailsToolbar extends React.Component { static propTypes = { @@ -323,7 +324,7 @@ export default class TrendDetailsToolbar extends React.Component { ) } { - this.props.serviceSummary === false && ( + (this.props.serviceSummary === false && tracesEnabled) && ( { this.props.options.operationName = this.props.operationStore.operations; }); @@ -284,7 +287,7 @@ export default class Autocomplete extends React.Component { const kvPair = splitInput[splitInput.length - 1]; const chipKey = kvPair.substring(0, kvPair.indexOf('=')).trim(); const chipValue = kvPair.substring(kvPair.indexOf('=') + 1, kvPair.length).trim(); - if (chipKey.includes('serviceName')) { + if (chipKey.includes('serviceName') && tracesEnabled) { this.props.options.operationName = []; this.props.operationStore.fetchOperations(chipValue, () => { this.props.options.operationName = this.props.operationStore.operations; @@ -464,7 +467,7 @@ export default class Autocomplete extends React.Component { chipValue = kvPair.substring(kvPair.indexOf('=') + 1, kvPair.length).trim().replace(/"/g, ''); } this.props.uiState.chips[chipKey] = chipValue; - if (chipKey.includes('serviceName')) { + if (chipKey.includes('serviceName') && tracesEnabled) { this.props.options.operationName = []; this.props.operationStore.fetchOperations(chipValue, () => { this.props.options.operationName = this.props.operationStore.operations; diff --git a/src/components/universalSearch/searchBar/searchBar.jsx b/src/components/universalSearch/searchBar/searchBar.jsx index 29e174cc..d026608b 100644 --- a/src/components/universalSearch/searchBar/searchBar.jsx +++ b/src/components/universalSearch/searchBar/searchBar.jsx @@ -27,6 +27,8 @@ import uiState from './stores/searchBarUiStateStore'; import OperationStore from '../../../stores/operationStore'; import ServiceStore from '../../../stores/serviceStore'; +const subsystems = (window.haystackUiConfig && window.haystackUiConfig.subsystems) || []; + @observer export default class SearchBar extends React.Component { static propTypes = { @@ -45,8 +47,10 @@ export default class SearchBar extends React.Component { componentDidMount() { // TODO move this inside state store's init maybe? - SearchableKeysStore.fetchKeys(); - ServiceStore.fetchServices(); + if (subsystems.includes('traces')) { + SearchableKeysStore.fetchKeys(); + ServiceStore.fetchServices(); + } } componentWillReceiveProps(next) { diff --git a/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.js b/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.js index 214d3f05..d6f21b01 100644 --- a/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.js +++ b/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.js @@ -22,6 +22,7 @@ export class SearchBarUiStateStore { @observable operationName = null; @observable fieldsKvString = null; @observable timeWindow = null; + @observable interval = 'FiveMinute'; @observable chips = []; @observable displayErrors = {}; @observable tabId = null; @@ -61,7 +62,7 @@ export class SearchBarUiStateStore { } else if (key === 'tabId') { this.tabId = search[key]; // url query keys that we don't want as chips - } else if (key !== 'type' && key !== 'useExpressionTree' && key !== 'spanLevelFilters') { + } else if (key !== 'type' && key !== 'useExpressionTree' && key !== 'spanLevelFilters' && key !== 'interval') { this.chips[key] = search[key]; } }); diff --git a/src/components/universalSearch/tabs/tabStores/alertsTabStateStore.js b/src/components/universalSearch/tabs/tabStores/alertsTabStateStore.js index 0bc05aef..981539bd 100644 --- a/src/components/universalSearch/tabs/tabStores/alertsTabStateStore.js +++ b/src/components/universalSearch/tabs/tabStores/alertsTabStateStore.js @@ -19,9 +19,12 @@ import alertsStore from '../../../alerts/stores/serviceAlertsStore'; const subsystems = (window.haystackUiConfig && window.haystackUiConfig.subsystems) || []; const isAlertsEnabled = subsystems.includes('alerts'); +const oneDayAgo = 24 * 60 * 60 * 1000; + export class AlertsTabStateStore { search = null; isAvailable = false; + interval = null; init(search) { // initialize observables using search object @@ -30,14 +33,15 @@ export class AlertsTabStateStore { // check all keys except time // eslint-disable-next-line no-unused-vars - const {time, tabId, type, ...kv} = search; + const {time, tabId, type, interval, ...kv} = search; + this.interval = interval || 'FiveMinute'; const keys = Object.keys(kv); this.isAvailable = isAlertsEnabled && keys.length && keys.every(key => key === 'serviceName' || key === 'operationName'); } fetch() { // todo: fetch service alerts based on search time frame - alertsStore.fetchServiceAlerts(this.search.serviceName, '5m', 24 * 60 * 60 * 1000); + alertsStore.fetchServiceAlerts(this.search.serviceName, this.interval, oneDayAgo); return alertsStore; } } diff --git a/src/components/universalSearch/tabs/tabStores/serviceGraphStateStore.js b/src/components/universalSearch/tabs/tabStores/serviceGraphStateStore.js index d3fbe708..93775820 100644 --- a/src/components/universalSearch/tabs/tabStores/serviceGraphStateStore.js +++ b/src/components/universalSearch/tabs/tabStores/serviceGraphStateStore.js @@ -31,7 +31,7 @@ export class ServiceGraphStateStore { // check all keys except time // eslint-disable-next-line no-unused-vars - const {time, tabId, type, ...kv} = search; + const {time, tabId, type, interval, ...kv} = search; const keys = Object.keys(kv); const serviceKey = keys.length && keys.every(key => key === 'serviceName'); this.isAvailable = enabled && (serviceKey || !keys.length); diff --git a/src/components/universalSearch/tabs/tabStores/servicePerformanceStateStore.js b/src/components/universalSearch/tabs/tabStores/servicePerformanceStateStore.js index 9cc3daa5..b49adae3 100644 --- a/src/components/universalSearch/tabs/tabStores/servicePerformanceStateStore.js +++ b/src/components/universalSearch/tabs/tabStores/servicePerformanceStateStore.js @@ -29,7 +29,7 @@ export class ServicePerformanceStateStore { // check all keys except time // eslint-disable-next-line no-unused-vars - const {time, tabId, type, ...kv} = search; + const {time, tabId, type, interval, ...kv} = search; const keys = Object.keys(kv); this.isAvailable = enabled && !keys.length; } diff --git a/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.js b/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.js index 63101ec0..9c2e8b35 100644 --- a/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.js +++ b/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.js @@ -41,7 +41,7 @@ export class TracesTabStateStore { // TODO acting as a wrapper for older stores for now, // TODO fetch logic here // eslint-disable-next-line no-unused-vars - const { time, tabId, type, serviceName, ...traceSearch } = this.search; + const { time, tabId, type, interval, serviceName, ...traceSearch } = this.search; const filteredNames = Object.keys(traceSearch).filter(name => /nested_[0-9]/.test(name)); diff --git a/src/components/universalSearch/tabs/tabStores/trendsTabStateStore.js b/src/components/universalSearch/tabs/tabStores/trendsTabStateStore.js index 9601378c..4198f7b9 100644 --- a/src/components/universalSearch/tabs/tabStores/trendsTabStateStore.js +++ b/src/components/universalSearch/tabs/tabStores/trendsTabStateStore.js @@ -49,7 +49,7 @@ export class TrendsTabStateStore { // check all keys except time // eslint-disable-next-line no-unused-vars - const {time, tabId, type, ...kv} = search; + const {time, tabId, type, interval, ...kv} = search; const keys = Object.keys(kv); this.isAvailable = enabled && keys.length && keys.every(key => key === 'serviceName' || key === 'operationName'); } diff --git a/src/components/universalSearch/tabs/tabs.jsx b/src/components/universalSearch/tabs/tabs.jsx index 5e6038a5..6c12c838 100644 --- a/src/components/universalSearch/tabs/tabs.jsx +++ b/src/components/universalSearch/tabs/tabs.jsx @@ -81,11 +81,7 @@ export default class Tabs extends React.Component { constructor(props) { super(props); - // state for interval, used in alert counter and alert tab - this.state = {interval: '5m'}; - // bindings - this.updateInterval = this.updateInterval.bind(this); this.TabViewer = this.TabViewer.bind(this); // init state stores for tabs @@ -96,12 +92,6 @@ export default class Tabs extends React.Component { Tabs.initTabs(nextProps.search); } - updateInterval(newInterval) { - this.setState({ - interval: newInterval - }); - } - TabViewer({tabId, history, location}) { // trigger fetch request on store for the tab // TODO getting a nested store used by original non-usb components, instead pass results object @@ -113,7 +103,7 @@ export default class Tabs extends React.Component { case 'trends': return ; case 'alerts': - return ; + return ; case 'serviceGraph': return ; case 'servicePerformance': @@ -138,7 +128,7 @@ export default class Tabs extends React.Component { {tab.displayName} {tab.tabId === 'alerts' ?
    - +
    : null} diff --git a/src/utils/formatters.js b/src/utils/formatters.js index 00ad1358..d555cbf6 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -22,6 +22,8 @@ const formatters = {}; formatters.toTimestring = startTime => moment(Math.floor(startTime / 1000)).format('kk:mm:ss, DD MMM YY'); +formatters.toShortTimestring = startTime => moment(Math.floor(startTime / 1000)).format('kk:mm:ss'); + formatters.toTimestringWithMs = startTime => moment(Math.floor(startTime / 1000)).format('kk:mm:ss.SSS, DD MMM YY'); formatters.toTimeago = startTime => timeago().format(Math.floor(startTime / 1000)); diff --git a/test/src/components/alerts.spec.jsx b/test/src/components/alerts.spec.jsx index 162acf04..77310e38 100644 --- a/test/src/components/alerts.spec.jsx +++ b/test/src/components/alerts.spec.jsx @@ -78,9 +78,9 @@ const stubSubscriptions = [ expressionTree: { serviceName: 'test', operationName: 'test', - name: 'failureCount', + metric_key: 'failure-span', interval: '5m', - stat: 'failure-span', + stat: 'count', mtype: 'gauge', product: 'haystack' } @@ -101,9 +101,9 @@ const stubSubscriptions = [ expressionTree: { serviceName: 'test', operationName: 'test', - name: 'failureCount', + name: 'failure-span', interval: '5m', - stat: 'failure-span', + stat: 'count', mtype: 'gauge', product: 'haystack' } @@ -134,6 +134,8 @@ const stubAddedSubscription = [ } ]; +const fiveMinuteInterval = 'FiveMinute'; + function getValue(min, max) { return _.round((Math.random() * (max - min)) + min, 0); } @@ -162,28 +164,14 @@ function getAlertHistoryTimestamps() { const stubAlerts = [ { operationName: 'test', - type: 'durationTP99', + type: 'duration', isHealthy: false, timestamp: getRandomTimeStamp(), trend: getRandomValues() }, { operationName: 'test', - type: 'failureCount', - isHealthy: true, - timestamp: getRandomTimeStamp(), - trend: getRandomValues() - }, - { - operationName: 'test', - type: 'count', - isHealthy: true, - timestamp: getRandomTimeStamp(), - trend: getRandomValues() - }, - { - operationName: 'test', - type: 'AADuration', + type: 'failure-span', isHealthy: true, timestamp: getRandomTimeStamp(), trend: getRandomValues() @@ -252,7 +240,7 @@ describe('', () => { it('should render error if promise is rejected', () => { const alertsStore = createStubServiceAlertsStore(stubAlerts, rejectedPromise); alertsStore.fetchServiceAlerts(); - const wrapper = mount(); + const wrapper = mount( {}} location={stubLocation} defaultPreset={stubDefaultPreset} alertsStore={alertsStore} serviceName={stubService} />); expect(wrapper.find('.error-message_text')).to.have.length(1); expect(wrapper.find('.tr-no-border')).to.have.length(0); @@ -261,7 +249,7 @@ describe('', () => { it('should render loading if promise is pending', () => { const alertsStore = createStubServiceAlertsStore(stubAlerts, pendingPromise); alertsStore.fetchServiceAlerts(); - const wrapper = mount(); + const wrapper = mount( {}} location={stubLocation} defaultPreset={stubDefaultPreset} alertsStore={alertsStore} serviceName={stubService} />); expect(wrapper.find('.loading')).to.have.length(1); expect(wrapper.find('.error-message_text')).to.have.length(0); @@ -271,7 +259,7 @@ describe('', () => { it('should render the Active Alerts Table', () => { const alertsStore = createStubServiceAlertsStore(stubAlerts, fulfilledPromise); alertsStore.fetchServiceAlerts(); - const wrapper = mount(); + const wrapper = mount( {}} location={stubLocation} defaultPreset={stubDefaultPreset} alertsStore={alertsStore} serviceName={stubService} />); expect(wrapper.find('.loading')).to.have.length(0); expect(wrapper.find('.error-message_text')).to.have.length(0); @@ -282,7 +270,7 @@ describe('', () => { describe('', () => { it('should render error if promise is rejected', () => { const detailsStore = createStubAlertDetailsStore(stubDetails, rejectedPromise, stubSubscriptions); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('.error-message_text')).to.have.length(2); expect(wrapper.find('.loading')).to.have.length(0); @@ -292,7 +280,7 @@ describe('', () => { it('should render loading if promise is pending', () => { const detailsStore = createStubAlertDetailsStore(stubDetails, pendingPromise, stubSubscriptions); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('.loading')).to.have.length(2); expect(wrapper.find('.error-message_text')).to.have.length(0); @@ -302,30 +290,11 @@ describe('', () => { it('should render the alert details with successful details promise', () => { const detailsStore = createStubAlertDetailsStore(stubDetails, fulfilledPromise, stubSubscriptions); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('.loading')).to.have.length(0); expect(wrapper.find('.error-message_text')).to.have.length(0); expect(wrapper.find('.subscription-row')).to.have.length(2); expect(wrapper.find('.alert-history')).to.have.length(1); }); - - it('should successfully bring up the subscription modal and add dispatchers', () => { - const detailsStore = createStubAlertDetailsStore(stubDetails, fulfilledPromise, stubSubscriptions); - const wrapper = mount(); - - expect(wrapper.find('.subscription-row')).to.have.length(2); - - wrapper.find('.btn-success').first().simulate('click'); - wrapper.find('.btn-info').simulate('click'); - expect(wrapper.find('.dispatcher-input')).to.have.length(1); - }); - - it('should load subscription modal with values filled in when subscription modify button is clicked', () => { - const detailsStore = createStubAlertDetailsStore(stubDetails, fulfilledPromise, stubSubscriptions); - const wrapper = mount(); - - wrapper.find('.alert-modify').first().simulate('click'); - expect(wrapper.find('.dispatcher-input')).to.have.length(2); - }); });