gnomalies
v1.0.2
Published
Reversible, controlled anomaly detection and management ![The Gnomaly](https://raw.githubusercontent.com/c0d3runn3r/gnomalies/master/img/gnome.png) ## Table of Contents
Downloads
7
Readme
Gnomalies
Reversible, controlled anomaly detection and management
Table of Contents
Example
const Gnomalies = require("../index.js");
// This Anomaly "fixes" lowercase letters. Skipping normal class syntax for brevity here...
class EvilLowercase extends Gnomalies.Anomaly {}
EvilLowercase.detect = async (system, opts) => system.str.match(/[a-z]/)?true:false;
EvilLowercase.prototype.action = async (system, opts) => system.str = system.str.toUpperCase();
// This one turns sad faces into happy faces
class SadFace extends Gnomalies.Anomaly {}
SadFace.detect = async (system, opts) => system.str.match(/😔/)?true:false;
SadFace.prototype.action = async (system, opts) => system.str = system.str.replace(/😔/g, "🙂");
// Here is a system with things to fix
let system ={ str: "Hello World 😔" };
(async ()=>{
// Fix the system
let processor = new Gnomalies.Processor([EvilLowercase, SadFace]);
await processor.detect(system); // processor.anomalies now contains relevant anomalies
await processor.process(system);
console.log(system.str); // "HELLO WORLD 🙂"
})();
Anomalies
We define a system as "an entity represented by a collection of values". An anomaly is the condition that a subset of those values are in an inferior state that could potentially be transformed into a superior state. The Anomaly
class containes a set of functions that allow us to process these anomalies in a systematic way, including fingerprinting and reversion management.
| Wrapper | Override this function | Purpose |
|-------------------------------|---------------------------|------------------------------------------------------------|
| Anomaly.detect(system, opts)
| _detect(system, opts)
| Detect an anomaly (static function!) |
| .action(system, opts)
| _action(system, opts)
| Correct the anomaly by changing system
|
| .revert(system, opts)
| _revert(system, opts)
| Undo action()
|
| .evaluate(system, opts)
| _evaluate(system, opts)
| Evaluate the successfulness of action()
or revert()
|
As you can see, the workhorse anomaly management functions you should override start with _underscore
. Do not call the underscore function directly however; please call the wrapper (no underscore). This allows us to handle events, fingerprinting, etc while keeping your code simple.
When an anomaly is actioned we take before-and-after fingerprints so that we can ensure any reversion is done properly and actually results in a resoration of the original state. We also check fingerprints before starting a reversion (to make sure we are reverting the same thing we actioned). By default, this is all done by taking the SHA256 hash of JSON.serialize(system)
. Do make sure you set fingerprint_keys
to something sensible if you don't want everything about your system fingerprinted.
revert()
is intelligent enough to know that a fingerprint matching your preaction fingerprint means no reversion is necessary; overloaded _revert()
will never gets called. In that case, the fingerprint is further checked against the postaction fingerprint. A match means that we should be able to perform a clean reversion; _revert()
is called. A mismatch means we are in some sort of dirty state; an error is thrown.
Once _revert()
is done, the wrapping revert()
checks fingerprints to make sure we have a pristine preaction
state. A mismatch will throw an error.
Processor
A helpful Processor
class is included that can automate much of the anomaly detection, processing, error handling, and reversion work. To use a Processor, pass an array of Anomaly classes (not instances!) to it's constructor. Then call .detect()
, passing a system. The Processor will call each anomaly's .detect()
method, constructing anomaly objects for those that return true
and storing the resulting array in .anomalies
. You can then call Processor.process()
to .action()
each anomaly on the system.
When using processor.process()
, errors thrown in anomaly.action()
result in an automatic call to anomaly.revert()
. If revert()
also throws, the anomaly.dirty
will be set to true
.
Once you have processed all anomalies, you should check to see which ones are paused, and if any of those are dirty.
A few important departures from the original concept (//delete me after everyone is on board)
Anomaly.state
tracks only the state of the anomaly with respect to theaction()
.- Since 'paused' (or ManualReview) doesn't tell us anything about the state, it's actually just a property - not a state. This is
.paused
. - Activity notifications about work being done are events (e.g.
Anomaly#activity
->{"activity" : "detect", "progress" : "45" }
) - We don't retain any logger references or emit 'error_log' events. Instead we emit
Anomaly#log
events for allAnomaly.log()
calls. If you want to log these in a logger, just consume them from theProcessor
.
Subclassing notes
- Most overridden functions are
async
- When you implement
_revert()
, you are expected to store whatever data you need during_action()
in order to perform the reversion. Make sure you override.serialize()
and.from_serialized()
so that this data gets stored with your class! - You should override
fingerprint()
to call the base method with just the keys that should be used in the fingerprint. Otherwise all keys insystem
will be fingerprinted by default. - You may emit Anomaly#activity with your progress, in percent.
Anomaly
will emit 0 and 100 for you as bookends automatically. - Use the built in
Anomaly.log.{debug|info|warn|error}()
methods for logging. It is accessable via.history
. Each call will also emitAnomaly#log
events, making it easy to connect with your external logging engine.
Todo
Acknowledgements
Thanks to Dr. Jonathan Van Schenck for developing the Anomaly Report concept. This project is based on his original class.
MIT License
Copyright (c) 2022 Nova Dynamics
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
API
Modules
Classes
Gnomalies
- Gnomalies
- .Anomaly
- new Anomaly(params)
- instance
- .fingerprints ⇒ object | string | string
- .fingerprint_keys ⇒ array.<string>
- .dirty ⇒ boolean
- .dirty
- .name ⇒ string
- .paused ⇒ boolean
- .id ⇒ string
- .history ⇒ array
- .state ⇒ string
- .action(system, opts) ⇒ Promise
- .revert(system, opts) ⇒ Promise
- .evaluate(system, opts) ⇒ Promise
- .toJSON(keys) ⇒ object
- .snapshot(system) ⇒ object
- .fingerprint(system) ⇒ string
- .iterations([state]) ⇒ number
- .pause(reason)
- .resume(reason)
- static
- .allowed_states ⇒ array
- ._detect()
- .detect(system, opts) ⇒ Promiose.<boolean>
- .Processor
- new Processor([classes])
- .anomalies ⇒ Array.<Anomaly>
- .classes ⇒ Array.<Anomaly>
- .reset() ⇒ Processor
- .serialize() ⇒ string
- .deserialize(data) ⇒ Processor
- .anomalies_with_state(state) ⇒ Array.<Anomaly>
- .detect(system, opts)
- .process(system, opts) ⇒ Promise
- .process_one() ⇒ Promise.<(Anomaly|null)>
- .Anomaly
Gnomalies.Anomaly
Anomaly
To use this class, extend it and override .detect(), .action(), and any other methods you need. If you are saving the anomaly for later use, you should also make sure .serialize() and .deserialize() will meet your needs. If your processor will be using fingerprints, you should also make sure .fingerprint() will meet your needs.
Kind: static class of Gnomalies
- .Anomaly
- new Anomaly(params)
- instance
- .fingerprints ⇒ object | string | string
- .fingerprint_keys ⇒ array.<string>
- .dirty ⇒ boolean
- .dirty
- .name ⇒ string
- .paused ⇒ boolean
- .id ⇒ string
- .history ⇒ array
- .state ⇒ string
- .action(system, opts) ⇒ Promise
- .revert(system, opts) ⇒ Promise
- .evaluate(system, opts) ⇒ Promise
- .toJSON(keys) ⇒ object
- .snapshot(system) ⇒ object
- .fingerprint(system) ⇒ string
- .iterations([state]) ⇒ number
- .pause(reason)
- .resume(reason)
- static
- .allowed_states ⇒ array
- ._detect()
- .detect(system, opts) ⇒ Promiose.<boolean>
new Anomaly(params)
constructor
Throws:
- Error error on invalid parameter
| Param | Type | Default | Description | | --- | --- | --- | --- | | params | object | | parameters for this object | | [params.history] | array | | the log | | [params.id] | string | | the id | | [params.description] | string | | a short description of this anomaly type | | [params.state] | string | | the state | | [params.paused] | boolean | | whether the anomaly is paused | | [params.dirty] | boolean | | whether the anomaly is dirty | | [params.fingerprint_keys] | array | | the keys that will be used to generate fingerprints, or null for all keys. Expects full paths into the systems to be analyzed, e.g. ["a.name", "b.name.first"] | | [params.fingerprints] | object | | the fingerprints |
anomaly.fingerprints ⇒ object | string | string
fingerprints (getter)
Kind: instance property of Anomaly
Returns: object - fingerprints the fingerprintsstring - fingerprints.preaction the fingerprint before the actionstring - fingerprints.postaction the fingerprint after the action
anomaly.fingerprint_keys ⇒ array.<string>
fingerprint_keys (getter)
This is set in the constructor and can't be changed after instantiation (otherwise fingerprints would stop being reliable)
Kind: instance property of Anomaly
Returns: array.<string> - the keys that are used to generate fingerprints
anomaly.dirty ⇒ boolean
dirty (getter)
This flag is set by the processor to indicate we failed somewhere during a state transition
Kind: instance property of Anomaly
Returns: boolean - true if this anomaly is dirty
anomaly.dirty
dirty (setter)
This flag is set by the processor to indicate we failed somewhere during a state transition
Kind: instance property of Anomaly
| Param | Type | Description | | --- | --- | --- | | dirty | boolean | true if this anomaly is dirty |
anomaly.name ⇒ string
name (getter)
Kind: instance property of Anomaly
Returns: string - the name of this anomaly
anomaly.paused ⇒ boolean
paused (getter)
Kind: instance property of Anomaly
Returns: boolean - true if this is paused
anomaly.id ⇒ string
id (getter)
Kind: instance property of Anomaly
Returns: string - the id for this anomaly
anomaly.history ⇒ array
Log (getter)
Kind: instance property of Anomaly
Returns: array - the log for this anomaly
anomaly.state ⇒ string
state (getter)
Kind: instance property of Anomaly
Returns: string - the state of the anomaly
anomaly.action(system, opts) ⇒ Promise
Action
Performs the action for this anomaly. If the anomaly is not in a preaction state, an error is thrown. When using fingerprints, we take the fingerprint before and after calling _action(). Do not override me. Override _action() instead!
Kind: instance method of Anomaly
Returns: Promise - promise that resolves when the action is complete
Throws:
- Error error if the anomaly is not in a preaction state or system is undefined
Emits: activity
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed | | opts | object | arbitrary options |
anomaly.revert(system, opts) ⇒ Promise
Revert
Undo the action for this anomaly. If we are in a preaction state and using fingerprints, we verify the fingerprint and then return. Otherwise, we check that we match the postaction fingerprint; call _revert(), and then check the preaction fingerprint. If any of this fails, we throw an error. Do not override me. Override _revert() instead!
Kind: instance method of Anomaly
Returns: Promise - promise that resolves when the reversion is complete
Throws:
- Error error on error
Emits: activity
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed | | opts | object | arbitrary options |
anomaly.evaluate(system, opts) ⇒ Promise
Evaluate
Evaluate the success of our action. Throw an error if you believe the action or reversion failed. Can also be used for post-action cleanup, statistics, etc. Do not override me. Override _evaluate() instead!
Kind: instance method of Anomaly
Returns: Promise - promise that resolves when the evaluation is complete
Throws:
- Error error on error
Emits: activity
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed | | opts | object | arbitrary options |
anomaly.toJSON(keys) ⇒ object
toJSON
Serialize this anomaly for storage. By default, we serialize the following keys: #id, #name, #state, #log, #paused, #dirty, #fingerprint_keys
If you override this funciton, you should call super.serialize() and add your own keys to the result.
Kind: instance method of Anomaly
Returns: object - the JSON-ized object
Throws:
- Error error on error
| Param | Type | Description | | --- | --- | --- | | keys | array.<string> | the keys to serialize |
anomaly.snapshot(system) ⇒ object
Snapshot
Take a snapshot of the system suitable for fingerprinting.
Kind: instance method of Anomaly
Returns: object - the snapshot
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed |
anomaly.fingerprint(system) ⇒ string
Fingerprint
Creates a SHA256 hash of the system's keys and values, using the set of keys that were specified in our constructor. Skips keys that point to functions.
Kind: instance method of Anomaly
Returns: string - the fingerprint as a hex string
Throws:
- Error error if we can't find a specified key
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed |
anomaly.iterations([state]) ⇒ number
State iterations counter
Show how many times we have transitioned to a given state
Kind: instance method of Anomaly
Returns: number - the number of times we have transitioned to this state
| Param | Type | Default | Description | | --- | --- | --- | --- | | [state] | string | ""postaction"" | the state to count |
anomaly.pause(reason)
pause
Pause this anomaly
Kind: instance method of Anomaly
Emit: Anomaly#pause
| Param | Type | Description | | --- | --- | --- | | reason | string | the reason for the pause |
anomaly.resume(reason)
resume
Resume this anomaly
Kind: instance method of Anomaly
Emit: Anomaly#resume
| Param | Type | Description | | --- | --- | --- | | reason | string | the reason for the resume |
Anomaly.allowed_states ⇒ array
allowed_states (getter)
Kind: static property of Anomaly
Returns: array - the allowed states
Anomaly._detect()
Placeholder functions - these don't do anything until they are overidden Function signature matches that of their non-underscored wrappers
Kind: static method of Anomaly
Anomaly.detect(system, opts) ⇒ Promiose.<boolean>
Detect an anomaly. Don't override me. Override _detect() instead!
Kind: static method of Anomaly
Returns: Promiose.<boolean> - true if an anomaly is detected
Throws:
- Error error on error
| Param | Type | Description | | --- | --- | --- | | system | object | the system being analyzed | | opts | object | arbitrary options |
Gnomalies.Processor
AnomalyProcessor
Detect and process anomalies in a queue. Call the constructor with an array of Anomaly classes, then await .detect() and .process()
Kind: static class of Gnomalies
- .Processor
- new Processor([classes])
- .anomalies ⇒ Array.<Anomaly>
- .classes ⇒ Array.<Anomaly>
- .reset() ⇒ Processor
- .serialize() ⇒ string
- .deserialize(data) ⇒ Processor
- .anomalies_with_state(state) ⇒ Array.<Anomaly>
- .detect(system, opts)
- .process(system, opts) ⇒ Promise
- .process_one() ⇒ Promise.<(Anomaly|null)>
new Processor([classes])
Create a new Processor
Returns: Processor - the new Processor
Throws:
- Error if classes is not an array
| Param | Type | Default | Description | | --- | --- | --- | --- | | [classes] | Array.<Anomaly> | [] | An array of Anomaly classes we may detect |
processor.anomalies ⇒ Array.<Anomaly>
Get all anomalies
Kind: instance property of Processor
Returns: Array.<Anomaly> - the queue of anomalies
processor.classes ⇒ Array.<Anomaly>
Get all classes
Kind: instance property of Processor
Returns: Array.<Anomaly> - all Anomaly classes
processor.reset() ⇒ Processor
Reset the set of anomalies
Kind: instance method of Processor
Returns: Processor - this for chaining
Emits: reset
processor.serialize() ⇒ string
Serialize all anomalies
Kind: instance method of Processor
Returns: string - the serialized anomalies
processor.deserialize(data) ⇒ Processor
Deserialize anomalies into our .anomalies property
Kind: instance method of Processor
Returns: Processor - this for chaining
Throws:
- Error error on error
| Param | Type | Description | | --- | --- | --- | | data | string | The serialized anomalies |
processor.anomalies_with_state(state) ⇒ Array.<Anomaly>
Get anomalies with a particular state
Kind: instance method of Processor
Returns: Array.<Anomaly> - anomalies with the specified state
Throws:
- Error if state is invalid
| Param | Type | Description | | --- | --- | --- | | state | string | The state to filter on |
processor.detect(system, opts)
Detect anomalies in a system
Adds anomalies to the queue
Kind: instance method of Processor
| Param | Type | Description | | --- | --- | --- | | system | object | The system to detect anomalies in | | opts | object | Options to pass to the detect() methods |
processor.process(system, opts) ⇒ Promise
Process all anomalies
Processes all anomalies in the queue from the preaction state to the resolved state (if possible). May call .action(), .evaluate(), .revert()
Events are bubbled up from each anomaly. Anomalies that fail between states will be paused and will be set to .dirty
state
Kind: instance method of Processor
Returns: Promise - resolves when all anomalies are processed
Emits: log, state, pause, resume, activity
| Param | Type | Description | | --- | --- | --- | | system | object | The system to detect anomalies in | | opts | object | Options to pass to the methods (action() etc) |
processor.process_one() ⇒ Promise.<(Anomaly|null)>
Process one anomaly
Finds the first (by order) anomaly with a state of "preaction" and processes it
to the resolved state (if possible). May call .action(), .evaluate(), .revert()
Events are bubbled up. Anomalies that fail between states will be paused and will be set to .dirty
state
Kind: instance method of Processor
Returns: Promise.<(Anomaly|null)> - the anomaly processed, or null if there are no anomalies to process. You can check for success by checking the anomaly itself.
Emits: log, state, pause, resume, activity
Key
Key
A rich representation of a key extracted from an object
Kind: global class
- Key
- new Key(name, type, [path], [value])
- instance
- static
- .compare(a, b) ⇒ number
new Key(name, type, [path], [value])
Constructor
| Param | Type | Default | Description | | --- | --- | --- | --- | | name | string | | the name of the key | | type | string | | the type of thing pointed to by the key | | [path] | string | null | the parent path | | [value] | any | | the value of the key |
key.fullname ⇒ string
Get the full name (path) of the key, e.g. "a.b.0.name"
Kind: instance property of Key
Returns: string - the full name of the key
key.name ⇒ string
Get the name of the key, e.g. "name"
Kind: instance property of Key
Returns: string - the name of the key
key.type ⇒ string
Get the type of the key, e.g. "string"
Kind: instance property of Key
Returns: string - the type of the key
key.path ⇒ string
Get the path of the key, but not its own name: e.g. "a.b.0"
Kind: instance property of Key
Returns: string - the path of the key
key.value ⇒ any
Get the value pointed at by the key (if any) in the original object
Kind: instance property of Key
Returns: any - the value of the key
key.compare(other) ⇒ number
Compare another key to this key
Kind: instance method of Key
Returns: number - -1 if this < other, 0 if this == other, 1 if this > other
| Param | Type | Description | | --- | --- | --- | | other | Key | the other key |
Key.compare(a, b) ⇒ number
Compare two keys (for sorting). This is just a string comparison of .fullname
.
Future work could be to make this sort number-like array elements numerically, e.g. "a.b.2" should be before "a.b.10"
Kind: static method of Key
Returns: number - -1 if a < b, 0 if a == b, 1 if a > b
| Param | Type | Description | | --- | --- | --- | | a | Key | the first key | | b | Key | the second key |
KeyExtractor
KeyExtractor
Extract all keys from an object
Kind: global class
KeyExtractor.extract(obj) ⇒ array.<Key>
Extract all keys from an object
This function extracts a set of deep keys (like "a.b.0.name") from a nested collection of objects, arrays, Maps and Sets.
Map and Set are converted to Object and Array, respectively, before processing. This results in map keys being stringified.
In other words, the keys "0" and 0 are not distinguishable in the result of this function.
Kind: static method of KeyExtractor
Returns: array.<Key> - an array of keys
| Param | Type | Description | | --- | --- | --- | | obj | Object | an array, map, set, or object whose paths need extraction |