KPI Design
==================
## 給共創Partner看的大綱

1. I.App 寫好 data sources 的 API之後,透過 ETCD 註冊自己(I.App) 的位置
2. KPI Setting Backend 會 watch ETCD 並且讀取 I.App 可用的 data sources
3. KPI Engine 會使用 KPI Setting 提供的 data source values 跟設定做計算 (計算完會存在 MongoDB)
4. KPI Engine 算好的值 同時也會塞入 API hub 的 Metric Parameter 底下的 value/values/historicalValues
5. Dashboard 的 Backend 會讀取 Metric Parameter 以及 Group / Object 的 API 給 Dashboard 的 Visualization frontend 做不同選項
6. Dashboard 的 Frontend 會透過 Dashboard backend 有的 options 跟計算好的KPI值呈現不同的 visualization 畫面
KPI Engine 算出來的值(Real time values, Historical value, value)都來自於 Federation 的 MetricParameter query.
下圖是 graphql playground 的 metric parameters query 的 values (real time values)

KPI 後端儲存的所有 Metric Parameters 可以透過 machine query 或是 group query 底下的 metricParameters 欄位讀取。
下圖是 graphql playground 的 machine query 底下的 metric parameters

由於 呼叫 metric parameter 透過 KPI Engine 算出來的值需要提供 group 或是 machine 的ID,共創夥伴們可能會需要 group 跟 machine 的 query.
group 是從 RTM 的 groups query 來的如下:
```graphql=
query groupList($rootGroupsOnly: Boolean, $lang: String) {
groups(rootGroupsOnly: $rootGroupsOnly) {
id
name(lang: $lang)
}
}
```
machine 是來自於某個 group 底下的 machine:
```graphql=
query getMachinesByGroup($groupId: ID!) {
group(id: $groupId) {
machines {
id
name
}
stations: machines(isStation: true) {
id
name
}
}
}
```
## GraphQL Schema (I.App)
各個 I.App 需要實作
```graphql=
# Types
scalar DateTime
# Define queries here
extend type Query {
# 取得此 I.App 提供的所有 DataSource,可以傳入 groupId 或 machineId 來過濾
# 可以用「mode」去過濾出有支援這個 mode 的 DataSource,沒傳則列出支援 time driven 的
dataSources(groupId: String, machineId: String, mode: String): [DataSource!]!
# 取得特定的幾個 DataSource,用 index 對應
dataSourcesByIds(args: [DataSourceByIdArg!]!): [DataSource]!
# 取得多個 DataSource 前處理過的資料,每個 DataSource 對應到一筆資料,用 index 對應
dataSourceValues(args: [DataSourceValueArg!]!): [DataSourceValue]!
# [{value: "123"}] => 正常情況
# [null] => id 不存在
# [{value: null}] => 沒有值
}
type DataSource {
id: String! # 至少在各 I.App 下要唯一
name(lang: String): String! # 純 UI 顯示用
valueType: DataSourceValueType! # 資料類型
summarizedMethods: [DataSourceSummarizedMethod!]!
# groupId 和 machineId 至少要有一個有值,用來讓 UI 呈現選擇時的階層,兩者不可能同時存在
groupId: String # 此 DataSource 是隸屬於哪個 Group
machineId: String # 此 DataSource 是隸屬於哪個 Machine
# 沒設則為 TimeDriven
mode: String # EventDriven ...
}
enum DataSourceValueType {
Number
Date
}
type DataSourceSummarizedMethod {
# 以 method 為主,先決定 method,再決定可以用哪種資料來算
method: String! # Min, Max, Avg ...
dataPeriods: [String!]! # Hourly, Daily ... 原始資料的時間類型,由各 I.App 提供
}
type DataSourceByIdArg {
id: String!
groupId: String
machineId: String
}
# 為了能取得有支援 event driven 的 DataSource 的值,多了 from 和 variables
type DataSourceValueArg {
id: String! # 至少在各 I.App 下要唯一
summarizedMethod: String! # Min, Max, Avg ...
dataPeriod: String! # Hourly, Daily ... 原始資料的時間類型,由各 I.App 提供
timestamp: DateTime! # 欲取得的資料點所對應的時間,實際取資料時用的時間範圍,由各 I.App 決定
groupId: String
machineId: String
# for event driven
from: DateTime
tag: String # e.g. work order ID
eventData: JSON # String JSON Object
}
type DataSourceValue {
value: String
}
```
## ETCD
### 現況
I.App 需要新增 metadata 告知 graphql 的 endpoint,key:`dataSourceUrl`
### 未來
為了支援 EventSource 的選取,新增 key:`eventSourceUrl`
## 取得 Metric 設定及資料
```graphql=
# Define queries here
extend type Query {
# query single metric parameter by id
metricParameter(id: ID!): MetricParameter!
# query metric parameters by iAppType
metricParameters(iAppType: IAppType): [MetricParameter!]!
}
type MetricParameter {
id: ID!
_id: ObjectID!
groupId: ID
machineId: ID
tenantId: ID
iAppType: IAppType
name: String!
description: String
# formula for data type
dataType: FormulaDataType!
numberDataSetting: KpiFormulaNumberDataSetting
# 1000 = 1 second for how often to calculate data in kpi engine
recordingRate: PositiveInt
eventSource: MetricParameterEventSource
# how many days to keep data
daysToKeepData: PositiveInt!
maxChangeRate: PositiveFloat!
# calculate historical values from real time values based on these settings
summarizedSetting: SummarizedSetting!
# formula for calculating via kpi engine
formula: String!
operands: [Operand!]!
highLowEvents: [MetricParameterHighLowEvent]!
createdAt: DateTime
updatedAt: DateTime
# below are calculated in the kpi engine and queried from grafana dashboard
# single real time value
value: MetricParameterValue
# real time values for multiple values based on date range
values(
from: DateTime
to: DateTime
orderBy: ValueOrderByInput
first: Int
after: String
last: Int
before: String
tag: String
): ValueConnection!
# historical values based on date range (calculated from real time values)
historicalValues(
from: DateTime
to: DateTime
orderBy: ValueOrderByInput
period: String
first: Int
after: String
last: Int
before: String
tag: String
): ValueConnection!
}
enum IAppType {
OEEM
MOM
EHS
DPM
}
enum FormulaDataType {
Number
}
type KpiFormulaNumberDataSetting {
decimalPlace: NonNegativeInt!
unit: String
spanLow: Float
spanHigh: Float
}
type MetricParameterEventSource {
iAppName: String!
id: String!
groupId: String
machineId: String
}
type SummarizedSetting {
hourly: SummarizedMethod
daily: SummarizedMethod
weekly: SummarizedMethod
monthly: SummarizedMethod
}
enum SummarizedMethod {
Sum
Average
Min
Max
Diff
}
type Operand {
name: String!
dataSource: OperandDataSource
}
# 選項從各 I.App 來
type OperandDataSource {
iAppName: String!
id: String!
valueType: OperandDataSourceValueType!
summarizedMethod: String!
dataPeriod: String!
groupId: String
machineId: String
mode: String
}
enum OperandDataSourceValueType {
Number
Date
}
type MetricParameterHighLowEvent {
isActive: Boolean!
value: Float!
level: HighLowEventLevel!
message: String!
notificationAction: KpiNotificationAction
}
enum HighLowEventLevel {
Critical
Major
Warning
}
type KpiNotificationAction {
isActive: Boolean!
groupId: String!
subject: String
message: String
}
type MetricParameterValue {
value: Float!
ts: DateTime!
}
enum Sort {
ASC
DESC
}
type ValueOrderByInput {
timestamp: Sort
}
type ValeEdge {
cursor: String!
node: MetricParameterValue
}
type ValueConnection {
edges: [ValueEdge]!
# nodes: [MetricParameterValue!]
pageInfo: PageInfo!
total: int!
}
```
如果資料有變化,可以透過 RedisPubSub 得知。
* channel: ifp-kpi.MetricParameter.{ID}
* message: 若為空字串,則代表資料被刪除,反之則為新增或更新
```json=
{
"id": "TWV0cmljUGFyYW1ldGVy.YnzO0Oyq8FmQuCN0",
"groupId": "R3JvdXA.X7S66PMvPwAGCTcZ",
"name": "MyMetric",
"dataType": "Number",
"numberDataSetting": { "decimalPlace": 1 },
"recordingRate": 1,
"daysToKeepData": 1,
"maxChangeRate": 1,
"summarizedSetting": {},
"formula": "100",
"operands": [],
"highLowEvents": [],
"createdAt": "2022-05-12T09:09:37.051Z",
"updatedAt": "2022-05-12T09:09:37.051Z"
}
```
## 取得各 I.App 的 dataSourceUrl
```graphql=
# Define queries here
extend type Query {
dataSourceUrls: [DataSourceUrl!]!
}
type DataSourceUrl {
name: String!
url: String!
}
```
如果資料有變化,可以透過 RedisPubSub 得知。
* channel: ifp-kpi.DataSourceUrl.{I.App name}
* message: url,若為空字串,則代表 url 已消失,反之則為新增或更新
## 取得各 I.App 的 eventSourceUrl
```graphql=
# Define queries here
extend type Query {
eventSourceUrls: [EventSourceUrl!]!
}
type EventSourceUrl {
name: String!
url: String!
}
```
如果資料有變化,可以透過 RedisPubSub 得知。
* channel: ifp-kpi.EventSourceUrl.{I.App name}
* message: url,若為空字串,則代表 url 已消失,反之則為新增或更新
## I.App 和 KPI 之間的設定同步
### DataSource
當 I.App 的 DataSource 「被刪除」時,要用 RedisPubSub 的機制通知 KPI。
* channel: ifp-kpi.DataSource.{I.App name}.{DataSource ID}
* message: ""(空字串,代表著刪除)
### EventSource
當 I.App 的 EventSource 「被刪除」時,要用 RedisPubSub 的機制通知 KPI。
* channel: ifp-kpi.EventSource.{I.App name}.{EventSource ID}
* message: ""(空字串,代表著刪除)
<!--
## DataSource ()
| Total(十進位) | Event Driven | Time Driven | Description |
|:---:|:------------:|:-----------:| ----------------------- |
|0 | 0 | 0 | 都不支援 (保留) |
|1 | 0 | 1 | Time Driven Only |
|2 | 1 | 0 | Event Driven Only |
|3 | 1 | 1 | 支援 Event 與 Time (預留) |
-->
## EventSource (規劃中)
由 Event 觸發 Metric 計算,UI 提供 Event Source 給 Metric 選擇。

### I.App 實作
```graphql=
extend type Query {
# 可以用 groupId 或 machineId 去過濾 EventSource
# 如果都沒傳,則代表要列出的是「不屬於任何 Group 或 Machine」的 EventSource
eventSources(groupId: String, machineId: String): [EventSource!]!
# 取得特定的幾個 EventSource,用 index 對應
eventSourcesByIds(args: [EventSourceByIdArg!]!): [EventSource]!
}
type EventSource {
id: String! # 至少在各 I.App 下要唯一
name(lang: String): String! # 純 UI 顯示用
groupId: String # 此 EventSource 是隸屬於哪個 Group
machineId: String # 此 EventSource 是隸屬於哪個 Machine
}
type EventSourceByIdArg {
id: String!
groupId: String
machineId: String
}
```
### KPI 提供
#### 透過 GraphQL 觸發
```graphql=
extend type Mutation {
triggerMetricParameters(
iAppName: String
eventSourceId: String!,
groupId: String,
machineId: String,
from: DateTime,
timestamp: DateTime!,
tag: String, # e.g. work order ID
eventData: JSON # String JSON Object
): Void
}
```
#### 透過 Redis 觸發
透過 `PUBLISH` 至 `ifp-kpi-engine.trigger-metric-parameters` 觸發
```json=
{
"iAppName": "",
"eventSourceId": "",
"groupId": "",
"machineId": "",
"from": "",
"timestamp": "",
"tag": "",
"eventData": "", // JSON Object string
}
```
### Issues
- Metric 可能用到不同 DataSources,無法針對特定 DataSource 去給相關需要的參數(例:WorkOrderID),目前設計只能做到類似全域變數,每個 DataSource 都丟相同參數去詢問 DataSourceValue。