influxdata / kapacitor

Open source framework for processing, monitoring, and alerting on time series data
MIT License
2.31k stars 492 forks source link

Mechanism for edge detection in series #643

Closed nathanielc closed 6 years ago

nathanielc commented 8 years ago

What if you have a series that is reporting the state of a service at intervals? Something like

timestamp                  status
1465577250               up
1465577370               down
1465577490               down
1465577610               down
1465577730               down
1465577850               up
1465577970               up
1465578090               down
1465578210               down

You would like to trim down that series into the edges of the event, meaning when the status changed.

timestamp                  status
1465577250               up
1465577370               down
1465577850               up
1465578090               down

I see two ways of doing this:

  1. Use the AlertNode since it already can track state changes and has the .stateChangesOnly property.
  2. Add a feature to the eval node to be able to use both the current point and the previous point.

Example via method 1

stream
    |from()...
    |alert()
       .info(lambda: "status" == 'up')
       .warn(lambda: "status" == 'down')
       .fieldLevel('level')
       .stateChangesOnly()
    |elapsed('level', 1s)
    // Going to get elapsed time between up->down events and down->up events
    // we only care about down->up events aka how long it was down.
    |where(lambda:"level" == 'down')
    ...

The above script will work in current versions, but it seems odd and is limited to only tracking 4 states, OK, INFO, WARN, CRIT, and the state names have to change to those names.

Example via option 2

stream
    |from()...
    |eval(lambda: "p.status" != "status")
       .previous('p.')
       .as('changed')
       .keep() // Keep will keep the current fields so the stats of `down`, means we just changed to the down state.
    // Drop all points where the status didn't change
    |where(lambda: "changed")
    |elapsed('status', 1s)
    // Going to get elapsed time between up->down events and down->up events
    // we only care about down->up events aka how long it was down.
    |where(lambda:"status" == 'down')
    ...

This example reads much better as its clear what we are trying to do instead of abusing the alert node. This scales out to as many states as you like and preserves the original state names.

See relevant mailing list discussion https://groups.google.com/forum/?pli=1#!topic/influxdb/GspSPy0_REE

nathanielc commented 8 years ago

This also has a use in online incremental algorithms, where you want to calculate the next value in a series from the previous value.

seesiva commented 8 years ago

Can this help to build a counter ? Example: One transition from ON to OFF where keep on adding the count as+1 also determining the time elapsed between ON and OFF states.

pankajmehta commented 7 years ago

Hi Nathanielc you have mentioned on how o read previous status to get the right elapsed time, when I'm trying, i'm getting error that no "previous method is found"

Can you please help me, Im stuck from the last 3 days.

here is my code

var e10_1 = stream |from() .database('telegraf10') .retentionPolicy('autogen') .measurement('downtime') |eval(lambda: "p.value" != "value") .previous('p.') .as('changed') .keep() |where(lambda: "changed") |elapsed('value' , 1s) |where(lambda: "value" == 1) // 1:error

Rgds Pankaj

jensoleg commented 7 years ago

We need exactly this feature.

The case is a sensor delivering data from plants in horticultures and other places. The moisture goes up and down following irrigation and natural rain. To calculate irrigation events we look at the moisture growth/decline and from the derivative we can spot irrigation.

This script below gives us exactly the state sequence as @nathanielc described above. Trimming down the series would be a really welcomed feature. We also need to calculate the elapsed times between the events but guess this can be done already with a elapsed node .

stream |from() .database('spiio_senors') .retentionPolicy('autogen') .measurement('sensor_data') .groupBy(*) |window() .period(30m) .every(30m) .align() |derivative('moisture') .as('dryrate') |eval(lambda: ceil("dryrate")) .as('irrigation_state') |influxDBOut() .database('irrigation') .retentionPolicy('autogen') .measurement('irrigations') .precision('sā€™)

rmiggily commented 7 years ago

šŸ‘ for the feature. For something like status with known states there are some workarounds as @nathanielc mentioned. However we are trying to detect changes in a string field with arbitrary values. I modified the example:

timestamp                myfield
1465577250               blabla1
1465577370               blabla2
1465577490               blabla2
1465577610               blabla2
1465577730               blabla2
1465577850               blabla3
1465577970               blabla3
1465578090               blabla4
1465578210               blabla4

We would like to be alerted whenever myfield value changes.

timestamp                myfield
1465577250               blabla1
1465577370               blabla2
1465577850               blabla3
1465578090               blabla4

I guess the script should look like this:

var message = 'Value changed to {{ index .Fields "value" }}.'
var data = stream
    |from()
        .database(db)
        .retentionPolicy(rp)
        .measurement(measurement)
        .groupBy(groupBy)
        .where(whereFilter)
    |eval(lambda: "myfield")
        .as('value')

var trigger = data
    |alert()
        .valueChange("value")
        .message(message)        
        .post(url)

Any help would be great.

eswak commented 7 years ago

We can do it easily with the derivative. Here's a example of an on-line threshold crossing that only outputs changes :

stream
  |from()...
  |eval(lambda: int("value" > threshold))
    .as('outofboundary')
    .keep('outofboundary')
  |derivative('outofboundary')
    .as('diff')
  |where(lambda: "diff" != 0) // filter out successive same-state points
  |eval(lambda: if("diff" > 0, 'going-above-threshold', 'back-to-normal'))
    .as('change')
    .keep('change')
  |alert()...
rmiggily commented 7 years ago

Thank you for your response. I guess derivative would solve the problem for numeric fields, but the problem stays the same for fields with string type. Correct me if I am wrong. But I think derivative cannot be applied to non-integer values. What can we do if the field is of type string?

nathanielc commented 6 years ago

Fixed by #1844