---
# System prepended metadata

title: KPI Design

---

KPI Design
==================

## 給共創Partner看的大綱

![](https://i.imgur.com/nbF4RuM.png)


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)
![](https://i.imgur.com/nhJstmM.png)


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

下圖是 graphql playground 的 machine query 底下的 metric parameters
![](https://i.imgur.com/wLcKQCq.png)

由於 呼叫 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 選擇。
![](https://i.imgur.com/Ll5PgPN.png)

  
### 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。