mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Add support for geo_shape fields as the entity geospatial field when creating tracking containment alerts (#164100)
Closes https://github.com/elastic/kibana/issues/163996
### To test
1) Checkout [fake tracks geo_shape
branch](https://github.com/nreese/faketracks/tree/geo_shape)
2) run npm install
3) run `node ./generate_tracks.js`
4) in kibana, create `tracks*` data view
5) create map, use "create index" and draw boundaries that intersect
tracks. See screen shot
<img width="500" alt="Screen Shot 2023-08-17 at 2 49 52 PM"
src="5f1444d7
-2e12-4dd2-99c1-c730c2157e04">
6) create geo containment alert where entity index is `tracks*` and
boundaries index is `boundaries`.
7) Verify alerts get generated with entity geo_shape locations
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e651a6f875
commit
3393d87959
16 changed files with 148 additions and 91 deletions
|
@ -10,7 +10,7 @@ In the event that an entity is contained within a boundary, an alert may be gene
|
|||
=== Requirements
|
||||
To create a tracking containment rule, the following requirements must be present:
|
||||
|
||||
- *Tracks index or data view*: An index containing a `geo_point` field, `date` field,
|
||||
- *Tracks index or data view*: An index containing a `geo_point` or `geo_shape` field, `date` field,
|
||||
and some form of entity identifier. An entity identifier is a `keyword` or `number`
|
||||
field that consistently identifies the entity to be tracked. The data in this index should be dynamically
|
||||
updating so that there are entity movements to alert upon.
|
||||
|
@ -36,12 +36,7 @@ as well as two Kuery bars used to provide additional filtering context for each
|
|||
image::user/alerting/images/alert-types-tracking-containment-conditions.png[Define the condition to detect,width=75%]
|
||||
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
|
||||
|
||||
Index (entity):: This clause requires an *index or data view*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking.
|
||||
When entity:: This clause specifies which crossing option to track. The values
|
||||
*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions
|
||||
should trigger a rule. *Entered* alerts on entry into a boundary, *Exited* alerts on exit
|
||||
from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances
|
||||
or exits.
|
||||
Index (entity):: This clause requires an *index or data view*, a *time field* that will be used for the *time window*, and a *`geo_point` or `geo_shape` field* for tracking.
|
||||
Index (Boundary):: This clause requires an *index or data view*, a *`geo_shape` field*
|
||||
identifying boundaries, and an optional *Human-readable boundary name* for better alerting
|
||||
messages.
|
||||
|
|
|
@ -89,7 +89,7 @@ exports[`should render BoundaryIndexExpression 1`] = `
|
|||
|
||||
exports[`should render EntityIndexExpression 1`] = `
|
||||
<ExpressionWithPopover
|
||||
defaultValue="Select a data view and geo point field"
|
||||
defaultValue="Select a data view and geospatial field"
|
||||
expressionDescription="index"
|
||||
isInvalid={false}
|
||||
popoverContent={
|
||||
|
@ -108,6 +108,7 @@ exports[`should render EntityIndexExpression 1`] = `
|
|||
includedGeoTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternService={
|
||||
|
@ -183,7 +184,7 @@ exports[`should render EntityIndexExpression 1`] = `
|
|||
|
||||
exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
|
||||
<ExpressionWithPopover
|
||||
defaultValue="Select a data view and geo point field"
|
||||
defaultValue="Select a data view and geospatial field"
|
||||
expressionDescription="index"
|
||||
isInvalid={true}
|
||||
popoverContent={
|
||||
|
@ -202,6 +203,7 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
|
|||
includedGeoTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternService={
|
||||
|
|
|
@ -161,7 +161,7 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
|
|||
isInvalid={isInvalid}
|
||||
value={indexPattern.title}
|
||||
defaultValue={i18n.translate('xpack.stackAlerts.geoContainment.entityIndexSelect', {
|
||||
defaultMessage: 'Select a data view and geo point field',
|
||||
defaultMessage: 'Select a data view and geospatial field',
|
||||
})}
|
||||
popoverContent={indexPopover}
|
||||
expressionDescription={i18n.translate('xpack.stackAlerts.geoContainment.entityIndexLabel', {
|
||||
|
|
|
@ -4,7 +4,7 @@ There are several steps required to set up geo containment alerts for testing in
|
|||
that allows you to view triggered alerts as they happen. These instructions outline
|
||||
how to load test data, but really these steps can be used to load any data for geo
|
||||
containment alerts so long as you have the following data:
|
||||
- An index containing a`geo_point` field and a `date` field. This data is presumed to
|
||||
- An index containing a`geo_point` or `geo_shape` field and a `date` field. This data is presumed to
|
||||
be dynamic (updated).
|
||||
- An index containing `geo_shape` data, such as boundary data, bounding box data, etc.
|
||||
This data is presumed to be static (not updated). Shape data matching the query is
|
||||
|
|
|
@ -23,6 +23,5 @@ export interface GeoContainmentAlertParams extends RuleTypeParams {
|
|||
boundaryIndexQuery?: Query;
|
||||
}
|
||||
|
||||
// Will eventually include 'geo_shape'
|
||||
export const ES_GEO_FIELD_TYPES = ['geo_point'];
|
||||
export const ES_GEO_FIELD_TYPES = ['geo_point', 'geo_shape'];
|
||||
export const ES_GEO_SHAPE_TYPES = ['geo_shape'];
|
||||
|
|
|
@ -220,7 +220,8 @@ describe('getGeoContainmentExecutor', () => {
|
|||
{
|
||||
dateInShape: '2021-04-28T16:56:11.923Z',
|
||||
docId: 'ZVBoGXkBsFLYN2Tj1wmV',
|
||||
location: [-73.99018926545978, 40.751759740523994],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.99018926545978 40.751759740523994)',
|
||||
shapeLocationId: 'kFATGXkBsFLYN2Tj6AAk',
|
||||
},
|
||||
],
|
||||
|
@ -228,7 +229,8 @@ describe('getGeoContainmentExecutor', () => {
|
|||
{
|
||||
dateInShape: '2021-04-28T16:56:11.923Z',
|
||||
docId: 'ZlBoGXkBsFLYN2Tj1wmV',
|
||||
location: [-73.99561604484916, 40.75449890457094],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.99561604484916 40.75449890457094)',
|
||||
shapeLocationId: 'kFATGXkBsFLYN2Tj6AAk',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -9,6 +9,31 @@ import { getContainedAlertContext, getRecoveredAlertContext } from './alert_cont
|
|||
import { OTHER_CATEGORY } from '../constants';
|
||||
|
||||
test('getContainedAlertContext', () => {
|
||||
expect(
|
||||
getContainedAlertContext({
|
||||
entityName: 'entity1',
|
||||
containment: {
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (100 0)',
|
||||
shapeLocationId: 'boundary1Id',
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
},
|
||||
shapesIdsNamesMap: { boundary1Id: 'boundary1Name' },
|
||||
windowEnd: new Date('2022-06-21T17:00:00.000Z'),
|
||||
})
|
||||
).toEqual({
|
||||
containingBoundaryId: 'boundary1Id',
|
||||
containingBoundaryName: 'boundary1Name',
|
||||
detectionDateTime: '2022-06-21T17:00:00.000Z',
|
||||
entityDateTime: '2022-06-21T16:56:11.923Z',
|
||||
entityDocumentId: 'docId',
|
||||
entityId: 'entity1',
|
||||
entityLocation: 'POINT (100 0)',
|
||||
});
|
||||
});
|
||||
|
||||
test('getContainedAlertContext for backwards compatible number[] location format', () => {
|
||||
expect(
|
||||
getContainedAlertContext({
|
||||
entityName: 'entity1',
|
||||
|
@ -37,7 +62,8 @@ describe('getRecoveredAlertContext', () => {
|
|||
const activeEntities = new Map();
|
||||
activeEntities.set('entity1', [
|
||||
{
|
||||
location: [100, 0],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (100 0)',
|
||||
shapeLocationId: 'boundary1Id',
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
|
@ -63,7 +89,8 @@ describe('getRecoveredAlertContext', () => {
|
|||
const inactiveEntities = new Map();
|
||||
inactiveEntities.set('entity1', [
|
||||
{
|
||||
location: [100, 0],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (100 0)',
|
||||
shapeLocationId: OTHER_CATEGORY,
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
|
|
|
@ -50,7 +50,10 @@ function getAlertContext({
|
|||
entityId: entityName,
|
||||
entityDateTime: containment.dateInShape || null,
|
||||
entityDocumentId: containment.docId,
|
||||
entityLocation: `POINT (${containment.location[0]} ${containment.location[1]})`,
|
||||
entityLocation:
|
||||
containment.locationWkt !== undefined
|
||||
? containment.locationWkt
|
||||
: `POINT (${containment.location[0]} ${containment.location[1]})`,
|
||||
detectionDateTime: new Date(windowEnd).toISOString(),
|
||||
};
|
||||
if (!isRecovered) {
|
||||
|
|
|
@ -63,13 +63,16 @@ export async function executeEsQuery(
|
|||
},
|
||||
},
|
||||
],
|
||||
docvalue_fields: [
|
||||
fields: [
|
||||
entity,
|
||||
{
|
||||
field: dateField,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
geoField,
|
||||
{
|
||||
field: geoField,
|
||||
format: 'wkt',
|
||||
},
|
||||
],
|
||||
_source: false,
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '123',
|
||||
dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
@ -65,6 +66,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '456',
|
||||
dateInShape: 'Wed Dec 16 2020 15:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId2',
|
||||
|
@ -76,6 +78,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '789',
|
||||
dateInShape: 'Wed Dec 23 2020 16:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId3',
|
||||
|
@ -141,6 +144,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '999',
|
||||
dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId7',
|
||||
|
@ -166,6 +170,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '999',
|
||||
dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId7',
|
||||
|
@ -204,6 +209,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: OTHER_CATEGORY,
|
||||
dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
@ -225,6 +231,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: 'other',
|
||||
dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
@ -241,18 +248,21 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
const currLocationMapWithThreeMore = new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '789',
|
||||
dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '123',
|
||||
dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId2',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '456',
|
||||
dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId3',
|
||||
|
@ -277,18 +287,21 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: OTHER_CATEGORY,
|
||||
dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '123',
|
||||
dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '456',
|
||||
dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
@ -310,18 +323,21 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '123',
|
||||
dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: OTHER_CATEGORY,
|
||||
dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '456',
|
||||
dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
@ -338,6 +354,7 @@ describe('getEntitiesAndGenerateAlerts', () => {
|
|||
new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (0 0)',
|
||||
shapeLocationId: '123',
|
||||
dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
|
|
|
@ -56,6 +56,11 @@ export function getEntitiesAndGenerateAlerts(
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO remove otherCatIndex check
|
||||
// Elasticsearch filters aggregation is used to group results into buckets matching entity locations intersecting boundary shapes
|
||||
// filters.other_bucket_key returns bucket with entities that did not intersect any boundary shape.
|
||||
// shapeLocationId === OTHER_CATEGORY can only occur when containments.length === 1
|
||||
// test data does not follow this pattern and needs to be updated.
|
||||
const otherCatIndex = containments.findIndex(
|
||||
({ shapeLocationId }) => shapeLocationId === OTHER_CATEGORY
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { transformResults } from './transform_results';
|
|||
describe('transformResults', () => {
|
||||
const dateField = '@timestamp';
|
||||
const geoField = 'location';
|
||||
test('should correctly transform expected results', async () => {
|
||||
test('should correctly transform expected results', () => {
|
||||
const transformedResults = transformResults(
|
||||
// @ts-ignore
|
||||
sampleAggsJsonResponse.body,
|
||||
|
@ -27,13 +27,15 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2021-04-28T16:56:11.923Z',
|
||||
docId: 'ZVBoGXkBsFLYN2Tj1wmV',
|
||||
location: [-73.99018926545978, 40.751759740523994],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.99018926545978 40.751759740523994)',
|
||||
shapeLocationId: 'kFATGXkBsFLYN2Tj6AAk',
|
||||
},
|
||||
{
|
||||
dateInShape: '2021-04-28T16:56:01.896Z',
|
||||
docId: 'YlBoGXkBsFLYN2TjsAlp',
|
||||
location: [-73.98968475870788, 40.7506317878142],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.98968475870788 40.7506317878142)',
|
||||
shapeLocationId: 'other',
|
||||
},
|
||||
],
|
||||
|
@ -44,13 +46,15 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2021-04-28T16:56:11.923Z',
|
||||
docId: 'ZlBoGXkBsFLYN2Tj1wmV',
|
||||
location: [-73.99561604484916, 40.75449890457094],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.99561604484916 40.75449890457094)',
|
||||
shapeLocationId: 'kFATGXkBsFLYN2Tj6AAk',
|
||||
},
|
||||
{
|
||||
dateInShape: '2021-04-28T16:56:01.896Z',
|
||||
docId: 'Y1BoGXkBsFLYN2TjsAlp',
|
||||
location: [-73.99459345266223, 40.755913141183555],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.99459345266223 40.755913141183555)',
|
||||
shapeLocationId: 'other',
|
||||
},
|
||||
],
|
||||
|
@ -61,7 +65,8 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2021-04-28T16:56:11.923Z',
|
||||
docId: 'Z1BoGXkBsFLYN2Tj1wmV',
|
||||
location: [-73.98662586696446, 40.7667087810114],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-73.98662586696446 40.7667087810114)',
|
||||
shapeLocationId: 'other',
|
||||
},
|
||||
],
|
||||
|
@ -72,7 +77,7 @@ describe('transformResults', () => {
|
|||
|
||||
const nestedDateField = 'time_data.@timestamp';
|
||||
const nestedGeoField = 'geo.coords.location';
|
||||
test('should correctly transform expected results if fields are nested', async () => {
|
||||
test('should correctly transform expected results if fields are nested', () => {
|
||||
const transformedResults = transformResults(
|
||||
// @ts-ignore
|
||||
sampleAggsJsonResponseWithNesting.body,
|
||||
|
@ -87,7 +92,8 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2020-09-28T18:01:41.190Z',
|
||||
docId: 'N-ng1XQB6yyY-xQxnGSM',
|
||||
location: [-82.8814151789993, 40.62806099653244],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-82.8814151789993 40.62806099653244)',
|
||||
shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip',
|
||||
},
|
||||
],
|
||||
|
@ -98,7 +104,8 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2020-09-28T18:01:41.191Z',
|
||||
docId: 'iOng1XQB6yyY-xQxnGSM',
|
||||
location: [-82.22068064846098, 39.006176185794175],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-82.22068064846098 39.006176185794175)',
|
||||
shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip',
|
||||
},
|
||||
],
|
||||
|
@ -109,7 +116,8 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2020-09-28T18:01:41.191Z',
|
||||
docId: 'n-ng1XQB6yyY-xQxnGSM',
|
||||
location: [-84.71324851736426, 41.6677269525826],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (-84.71324851736426 41.6677269525826)',
|
||||
shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip',
|
||||
},
|
||||
],
|
||||
|
@ -120,7 +128,8 @@ describe('transformResults', () => {
|
|||
{
|
||||
dateInShape: '2020-09-28T18:01:41.192Z',
|
||||
docId: 'GOng1XQB6yyY-xQxnGWM',
|
||||
location: [6.073727197945118, 39.07997465226799],
|
||||
location: [0, 0],
|
||||
locationWkt: 'POINT (6.073727197945118 39.07997465226799)',
|
||||
shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -6,64 +6,55 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { GeoContainmentAlertInstanceState } from '../types';
|
||||
|
||||
// Flatten agg results and get latest locations for each entity
|
||||
export function transformResults(
|
||||
results: estypes.SearchResponse<unknown>,
|
||||
results: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
dateField: string,
|
||||
geoField: string
|
||||
): Map<string, GeoContainmentAlertInstanceState[]> {
|
||||
const buckets = _.get(results, 'aggregations.shapes.buckets', {});
|
||||
const arrResults = _.flatMap(buckets, (bucket: unknown, bucketKey: string) => {
|
||||
const subBuckets = _.get(bucket, 'entitySplit.buckets', []);
|
||||
return _.map(subBuckets, (subBucket) => {
|
||||
const locationFieldResult = _.get(
|
||||
subBucket,
|
||||
`entityHits.hits.hits[0].fields["${geoField}"][0]`,
|
||||
''
|
||||
);
|
||||
const location = locationFieldResult
|
||||
? _.chain(locationFieldResult)
|
||||
.split(', ')
|
||||
.map((coordString) => +coordString)
|
||||
.reverse()
|
||||
.value()
|
||||
: [];
|
||||
const dateInShape = _.get(
|
||||
subBucket,
|
||||
`entityHits.hits.hits[0].fields["${dateField}"][0]`,
|
||||
null
|
||||
);
|
||||
const docId = _.get(subBucket, `entityHits.hits.hits[0]._id`);
|
||||
const resultsMap = new Map<string, GeoContainmentAlertInstanceState[]>();
|
||||
const boundarySplitBuckets = results?.aggregations?.shapes?.buckets ?? {};
|
||||
for (const boundaryId in boundarySplitBuckets) {
|
||||
if (!boundarySplitBuckets.hasOwnProperty(boundaryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
location,
|
||||
shapeLocationId: bucketKey,
|
||||
entityName: subBucket.key,
|
||||
dateInShape,
|
||||
docId,
|
||||
};
|
||||
});
|
||||
const entitySplitBuckets = boundarySplitBuckets[boundaryId]?.entitySplit?.buckets ?? [];
|
||||
for (let i = 0; i < entitySplitBuckets.length; i++) {
|
||||
const entityName = entitySplitBuckets[i].key;
|
||||
const entityResults = resultsMap.get(entityName) ?? [];
|
||||
entityResults.push({
|
||||
// Required for zero down time (ZDT)
|
||||
// populate legacy location so non-updated-kibana nodes can handle new alert state
|
||||
//
|
||||
// Why 0,0 vs parsing WKT and populating actual location?
|
||||
// This loop gets processed for each entity location in each containing boundary, ie: its a hot loop
|
||||
// There is a mimial amount of time between one kibana node updating and all Kibana nodes being updated
|
||||
// vs a huge CPU penetatily for all kibana nodes for the rest of the time
|
||||
// Algorithm optimized for the more common use case where all Kibana nodes are running updated version
|
||||
location: [0, 0],
|
||||
locationWkt:
|
||||
entitySplitBuckets[i].entityHits?.hits?.hits?.[0]?.fields?.[geoField]?.[0] ?? '',
|
||||
shapeLocationId: boundaryId,
|
||||
dateInShape:
|
||||
entitySplitBuckets[i].entityHits?.hits?.hits?.[0]?.fields?.[dateField]?.[0] ?? null,
|
||||
docId: entitySplitBuckets[i].entityHits?.hits?.hits?.[0]?._id,
|
||||
});
|
||||
resultsMap.set(entityName, entityResults);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove sort
|
||||
// legacy algorithm sorted entity hits oldest to newest for an undocumented reason
|
||||
// preserving sort to avoid unknown breaking changes
|
||||
resultsMap.forEach((value, key) => {
|
||||
if (value.length > 1) {
|
||||
// sort oldest to newest
|
||||
resultsMap.set(key, _.orderBy(value, ['dateInShape'], ['desc', 'asc']));
|
||||
}
|
||||
});
|
||||
const orderedResults = _.orderBy(arrResults, ['entityName', 'dateInShape'], ['asc', 'desc'])
|
||||
// Get unique
|
||||
.reduce(
|
||||
(
|
||||
accu: Map<string, GeoContainmentAlertInstanceState[]>,
|
||||
el: GeoContainmentAlertInstanceState & { entityName: string }
|
||||
) => {
|
||||
const { entityName, ...locationData } = el;
|
||||
if (entityName) {
|
||||
if (!accu.has(entityName)) {
|
||||
accu.set(entityName, []);
|
||||
}
|
||||
accu.get(entityName)!.push(locationData);
|
||||
}
|
||||
return accu;
|
||||
},
|
||||
new Map()
|
||||
);
|
||||
return orderedResults;
|
||||
|
||||
return resultsMap;
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
"2021-04-28T16:56:11.923Z"
|
||||
],
|
||||
"location":[
|
||||
"40.751759740523994, -73.99018926545978"
|
||||
"POINT (-73.99018926545978 40.751759740523994)"
|
||||
],
|
||||
"entity_id":[
|
||||
"0"
|
||||
|
@ -94,7 +94,7 @@
|
|||
"2021-04-28T16:56:11.923Z"
|
||||
],
|
||||
"location":[
|
||||
"40.75449890457094, -73.99561604484916"
|
||||
"POINT (-73.99561604484916 40.75449890457094)"
|
||||
],
|
||||
"entity_id":[
|
||||
"1"
|
||||
|
@ -157,7 +157,7 @@
|
|||
"2021-04-28T16:56:11.923Z"
|
||||
],
|
||||
"location":[
|
||||
"40.7667087810114, -73.98662586696446"
|
||||
"POINT (-73.98662586696446 40.7667087810114)"
|
||||
],
|
||||
"entity_id":[
|
||||
"2"
|
||||
|
@ -191,7 +191,7 @@
|
|||
"2021-04-28T16:56:01.896Z"
|
||||
],
|
||||
"location":[
|
||||
"40.755913141183555, -73.99459345266223"
|
||||
"POINT (-73.99459345266223 40.755913141183555)"
|
||||
],
|
||||
"entity_id":[
|
||||
"1"
|
||||
|
@ -225,7 +225,7 @@
|
|||
"2021-04-28T16:56:01.896Z"
|
||||
],
|
||||
"location":[
|
||||
"40.7506317878142, -73.98968475870788"
|
||||
"POINT (-73.98968475870788 40.7506317878142)"
|
||||
],
|
||||
"entity_id":[
|
||||
"0"
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
"2020-09-28T18:01:41.190Z"
|
||||
],
|
||||
"geo.coords.location" : [
|
||||
"40.62806099653244, -82.8814151789993"
|
||||
"POINT (-82.8814151789993 40.62806099653244)"
|
||||
],
|
||||
"entity_id" : [
|
||||
"936"
|
||||
|
@ -80,7 +80,7 @@
|
|||
"2020-09-28T18:01:41.191Z"
|
||||
],
|
||||
"geo.coords.location" : [
|
||||
"39.006176185794175, -82.22068064846098"
|
||||
"POINT (-82.22068064846098 39.006176185794175)"
|
||||
],
|
||||
"entity_id" : [
|
||||
"AAL2019"
|
||||
|
@ -114,7 +114,7 @@
|
|||
"2020-09-28T18:01:41.191Z"
|
||||
],
|
||||
"geo.coords.location" : [
|
||||
"41.6677269525826, -84.71324851736426"
|
||||
"POINT (-84.71324851736426 41.6677269525826)"
|
||||
],
|
||||
"entity_id" : [
|
||||
"AAL2323"
|
||||
|
@ -148,7 +148,7 @@
|
|||
"2020-09-28T18:01:41.192Z"
|
||||
],
|
||||
"geo.coords.location" : [
|
||||
"39.07997465226799, 6.073727197945118"
|
||||
"POINT (6.073727197945118 39.07997465226799)"
|
||||
],
|
||||
"entity_id" : [
|
||||
"ABD5250"
|
||||
|
|
|
@ -54,7 +54,11 @@ export interface GeoContainmentRuleState extends RuleTypeState {
|
|||
}
|
||||
|
||||
export interface GeoContainmentAlertInstanceState extends AlertInstanceState {
|
||||
// 8.10-, location is [lon, lat] array.
|
||||
// continue to populate 'location' for backwards compatibility with persisted alert instance state and ZDT
|
||||
location: number[];
|
||||
// 8.11+, location will be wkt represenation of geometry.
|
||||
locationWkt?: string;
|
||||
shapeLocationId: string;
|
||||
dateInShape: string | null;
|
||||
docId: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue