目前上报自定义监控数据通过 CloudSat 中 UploadMonitorData 接口进行上报,需遵循自定义监控上报 URL 构造规范和接口数据规范。用户可以通过 UploadMonitorData 接口上报自定义的数据,然后进行监控数据分析和告警。

UploadMonitorData 接口 URL 构造规范

上传数据 UploadMonitorData 接口说明,接口采用 post 方法发送请求,请求 URL 需要构造,构造方法如下:

构造 URL 方法:构造前 URL + "?" + 构造验证请求串。

构造前 URL:

http://cloudsat.qingcloud.com/api/:zone/v1/custom/UploadMonitorData
说明

构造完成之后请求 URL 即最终 UploadMonitorData 接口请求 URL,URL 里面:zone,请根据具体分区信息填写,如上海 1 区 sh1,文中其他地方类似。

分区信息参考:山河云计算平台(jn1a)、齐鲁工大计算平台(jn2a)、

构造验证请求串示例:构造验证请求串方法详见下面说明(下方为示例,请自行参照后文构造验证请求串方法进行构造)。

access_key_id=CCDJRDKCCKZYTEXANZJD&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-12-23T13%3A32%3A34Z&version=1&zone=sh1&signature=sOdokWwvYJ80mM%2FxYbBTsgTgQl3iu%2F2WDXWjgKFPNNs%3D
说明

这部分请根据实际参数进行构造。

由于上面构造前 URL 上传监控数据需要对请求进行验证,这里采用类似 API 请求中签名(signature) 的生成方法,构造验证请求串,拼接在上面构造前 URL 后面。由于需要去 iaas 对用户信息进行确认,所以这里选择 DescribeUsers 这个 action 去构造签名信息。

构造完成后请求 URL 示例为

http://cloudsat.qingcloud.com/api/:zone/v1/custom/UploadMonitorData?access_key_id=CCDJRDKCCKZYTEXANZJD&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-12-23T13%3A32%3A34Z&version=1&zone=sh1&signature=sOdokWwvYJ80mM%2FxYbBTsgTgQl3iu%2F2WDXWjgKFPNNs%3D

下面为在上海 1 区,完成构建验证请求串之后拼接生成的一个最终 api 请求 URL 实例,请根据实际分区信息和构造的验证串拼接构造相应的最终 api 请求 URL。

http://cloudsat.qingcloud.com/api/sh1/v1/custom/UploadMonitorData?access_key_id=CCDJRDKCCKZYTEXANZJD&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-12-23T13%3A32%3A34Z&version=1&zone=sh1&signature=sOdokWwvYJ80mM%2FxYbBTsgTgQl3iu%2F2WDXWjgKFPNNs%3D

UploadMonitorData 接口数据规范

描述:上传监控数据。

请求类型POST

请求 URL

http://cloudsat.qingcloud.com/api/:zone/v1/custom/UploadMonitorData?access_key_id=QEJMCFROGCAPHUOAJMRN&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-10-13T10%3A28%3A33Z&version=1&zone=test&signature=SO9ZufFb69Om21bK%2BH7Gs6f%2FuuDljHh41STgIX%3D
注意

请仔细阅读上文说明,根据实际情况构造验证请求串,拼接成最终 api 请求 URL 并替换:zone 信息。

Headers

参数 参数值 是否必须 示例 备注

Content-Type

application/json

'Content-Type':'application/json'

不可缺少

请求 body 格式

{
    "user_id": "usr-123456",
    "namespace": "namespace-1",
    "data": [
      {
          "source": "test",
          "user_id": "usr-KJ8DrfQT",
          "tags": "role=master,interface=eth10",
          "group_id": "group10",
          "resource_id": "i-instance-10" ,
          "resource_name": "name10",
          "resource_type": "instance",
          "root_user_id": "usr-KJ8DrfQ",
          "meter": "disk_ri'",
          "region": "sh1",
          "value": 99,
          "value_type": "percent",
           "time_stamp":"2020-11-03T09:58:44Z"
         },{
          "source": "test",
          "user_id": "usr-KJ8DrfQT",
          "tags": "role=master,interface=eth10",
          "group_id": "group10" ,
          "resource_id": "i-instance-10",
          "resource_name": "name10" ,
          "resource_type": "instance",
          "root_user_id": "usr-KJ8DrfQ",
           "meter": "diskio",
          "region": "sh1",
          "value": 88,
          "value_type": "percent",
          "time_stamp":"2020-11-03T09:58:44Z"
         }
  ]
}

字段说明

字段 类型 举例 必填 说明

namespace

string

namespace-1

命名空间

region

string

sh1

region id

source

string

custom

监控数据的来源

group_id

string

group_1

监控数据的 group,或者分组标记

resource_id

string

i-12345678

监控数据关联的资源 id

resource_name

string

roger-test

监控数据关联的资源名称

resource_type

string

instance

资源类型

user_id

string

usr-123456

监控资源资源对应用户的 id

root_user_id

string

usr-123456

主账户 id

meter

string

cpu

监控指标,请于该命名空间下监控配置中的监控指标名称保持一致

value_type

string

raw

指标值的类型,例如:raw(原格式) percent(百分比)

value

int

80

监控的数据,整型的数据

time_stamp

string

2019-12-16T11:14:32Z

监控数据时间(UTC)

tags

string

role=master,cln-node=node-1,interface=eth0

数据的 tags,用于归类,都是 key,value 的形式,用于 like 查询,理论上存储的 key 都是按层级往后排,比如资源的下一级是 role,role 的下一级是节点,节点下有网卡

返回结果

{
    "data": {
        "upload_count": 2
    },
    "ret_code": 0
}

结果说明:ret_code 为 0 时表示上传数据成功,dataupload_count 表示上传数据条数。

自定义页面监控列表中涉及到一些时间区间内的监控数据计算,最小统计周期为 5 分钟,因此需连续发送一段时间(不小于 5 分钟)页面才有数据显示。

数据格式例如:

{
    "user_id": "usr-12345678",
    "namespace": "ns1",
    "data": [
     {
        "region": "sh1",
        "source": "custom",
        "user_id": "testuser",
        "group_id": "group_1",
        "resource_id": "resource_id1",
        "resource_name": "resource_name2",
        "resource_type": "instance",
        "root_user_id": "root_user",
        "meter": "cpu",
        "value": "100",
        "value_type": "raw",
        "tags":"role=master,interface=eth0",
        "time_stamp": "2019-12-16T11:14:32Z"
    },
    {
        "region": "sh1",
        "source": "custom",
        "user_id": "testuser",
        "group_id": "group_2",
        "resource_id": "resource_id1",
        "resource_name": "resource_name2",
        "resource_type": "instance",
        "root_user_id": "root_user",
        "meter": "memory",
        "value": "90",
        "value_type": "raw",
        "tags":"role=master,interface=eth0",
        "time_stamp": "2019-12-16T11:14:32Z"
    }
  ]
}

构造请求串方法

您需要先在控制台创建 API 密钥 ,获得 accesss_key_idsecret_access_key,这里假设:

access_key_id = 'QYACCESSKEYIDEXAMPLE'
secret_access_key = 'SECRETACCESSKEY'

例如:请求参数如下:

{
  "zone":"sh1",
  "signature_version":1,
  "signature_method":"HmacSHA256",
  "version":1,
  "access_key_id":"QYACCESSKEYIDEXAMPLE",
  "action":"DescribeUsers",
  "time_stamp":"2013-08-27T14:30:10Z"
}
说明

实际使用过程中请替换成您自己的 AccessKeyId,切换相应区间信息。签名验证可以参考 API 请求中签名(signature),使用链接文档中数据签名结果如果和链接中的一致代表签名方法正确。

步骤 1:参数升序排列

按参数名进行升序排列,排序后的参数为:

{
  "access_key_id":"QYACCESSKEYIDEXAMPLE",
  "action":"DescribeUsers",
  "signature_method":"HmacSHA256",
  "signature_version":1,
  "time_stamp":"2013-08-27T14:30:10Z",
  "version":1,
  "zone":"sh1"
}

步骤 2:参数 URL 编码

对参数名称和参数值进行 URL 编码,编码后的请求串为:

{
  "access_key_id":"QYACCESSKEYIDEXAMPLE",
  "action":"DescribeUsers",
  "signature_method":"HmacSHA256",
  "signature_version":1,
  "time_stamp":"2013-08-27T14%3A30%3A10Z",
  "version":1,
  "zone":"sh1"
}
注意
  • 编码时,空格要转换成 %20 , 而不是 +

  • 转码部分的字符要用大写,如 : 应转成 %3A,而不是 %3a

步骤 3:构造 URL 请求

参数名和参数值之间用 = 号连接,参数和参数之间用 号连接,构造后的 URL 请求为

access_key_id=QYACCESSKEYIDEXAMPLE&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2013-08-27T14%3A30%3A10Z&version=1&zone=sh1

步骤 4:构造被签名串

被签名串的构造规则为: 被签名串 = HTTP 请求方式 + “\n” + uri + “\n” + 请求串

注意

“\n” 是换行符,不要将 “\” 转义。也就是说,不要用 “\n” ,有些语言,比如 php 和 ruby ,请用 “\n” , 而不是 ‘\n’。

假设 HTTP 请求方法为 GET 请求的uri路径为 “/iaas/” , 则被签名串为

GET\n/iaas/\naccess_key_id=QYACCESSKEYIDEXAMPLE&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2013-08-27T14%3A30%3A10Z&version=1&zone=sh1

步骤 5:计算签名

计算被签名串的签名 signature。

将 API 密钥的私钥(secret_access_key) 作为 key,生成被签名串的 HMAC-SHA256 或者 HMAC-SHA1 签名,更多信息可参见 RFC2104 将签名进行 Base64 编码。 将 Base64 编码后的结果进行 URL 编码。

注意

当 Base64 编码后存在空格时,不要对空格进行 URL 编码,而要直接将空格转为 “”,以 Python(版本 2.7)代码为例(其他语言类似,需要使用 sha256 + base64 编码,最后需要再进行 URL 编码,URL 编码时需要将原有的空格 “ ” 转为 “”)。

import base64
import hmac
import urllib
from hashlib import sha256

# 前面生成的被签名串
string_to_sign = 'GET\n/iaas/\naccess_key_id=QYACCESSKEYIDEXAMPLE&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2013-08-27T14%3A30%3A10Z&version=1&zone=sh1'
h = hmac.new(secret_access_key, digestmod=sha256)
h.update(string_to_sign)
sign = base64.b64encode(h.digest()).strip()
signature = urllib.quote_plus(sign)

步骤 6:添加签名

将签名参数附在原有请求串的最后面,最终的 HTTP 请求串(为了查看方便,我们人为地将参数之间用回车键分隔开)。

access_key_id=QYACCESSKEYIDEXAMPLE
&action=DescribeUsers
&signature_method=HmacSHA256
&signature_version=1
&time_stamp=2013-08-27T14%3A30%3A10Z
&version=1
&zone=sh1
&signature=bOQMI8wJ4ikFnadNXc%2BpnVMcUyf83C7b9JO5%2FAvkGyk%3D

完整的请求串为(为了查看方便,我们人为地将参数之间用回车键分隔开)。

access_key_id=QYACCESSKEYIDEXAMPLE
&action=DescribeUsers
&signature_method=HmacSHA256
&signature_version=1
&time_stamp=2013-08-27T14%3A30%3A10Z
&version=1
&zone=sh1
&signature=bOQMI8wJ4ikFnadNXc%2BpnVMcUyf83C7b9JO5%2FAvkGyk%3D

示例代码

示例代码以 golang、python 语言为例,其他语言类似。

Golang 示例

构造验证请求串代码示例

请用实际数据替换 AccessKeyIDSecretAccessKeyZone 等信息。

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"
)

type verifyInfo struct {
	httpMothod      string
	path            string
	secretAccessKey string
	describeUsers   *describeUsers
}

type describeUsers struct {
	AccessKeyID      string `json:"access_key_id"`
	Action           string `json:"action"`
	SignatureMethod  string `json:"signature_method"`
	SignatureVersion int    `json:"signature_version"`
	Version          int    `json:"version"`
	Zone             string `json:"zone"`
	TimeStamp        string `json:"time_stamp"`
}

func hmacSha256(data string, secret string) []byte {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(data))
	return h.Sum(nil)
}

func formatParams(v interface{}) (map[string]interface{}, error) {
	paramMap := make(map[string]interface{})
	jsonBuf, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(jsonBuf, &paramMap)
	if err != nil {
		return nil, err
	}
	return paramMap, nil
}

func sortParams(params map[string]interface{}) string {
	var keys []string
	for k := range params {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var parts []string
	for _, key := range keys {
		value := url.QueryEscape(strVal(params[key]))
		value = strings.Replace(strings.Replace(value, ":", "%3A", -1), " ", "%20", -1)
		parts = append(parts, key+"="+value)
	}
	return strings.Join(parts, "&")
}

func constructSignature(httpMothod, reqUrl, requestStr, key string) string {
	signaturePre := httpMothod + "\n" + reqUrl + "\n" + requestStr
	hmacShaStr := hmacSha256(signaturePre, key)
	base64Str := base64.StdEncoding.EncodeToString(hmacShaStr)
	signature := strings.TrimSpace(base64Str)
	signature = strings.Replace(signature, " ", "+", -1)
	signature = url.QueryEscape(signature)
	return signature
}

func strVal(value interface{}) string {
	var key string
	if value == nil {
		return key
	}
	switch value.(type) {
	case int:
		it := value.(int)
		key = strconv.Itoa(it)
	case string:
		key = value.(string)
	default:
		newValue, _ := json.Marshal(value)
		key = string(newValue)
	}
	return key
}

func (v *verifyInfo) buildVerifyStr() (string, error) {
	params, err := formatParams(v.describeUsers)
	if err != nil {
		return "", err
	}
	sortParams := sortParams(params)
	signature := constructSignature(v.httpMothod, v.path, sortParams, v.secretAccessKey)
	rsqParams := sortParams + "&signature=" + signature
	return rsqParams, nil
}


func main() {
	vi := &verifyInfo{
		httpMothod:      "GET",
		path:            "/iaas/",
		secretAccessKey: "SECRETACCESSKEY",
		describeUsers: &describeUsers{
			AccessKeyID:      "QYACCESSKEYIDEXAMPLE",
			Action:           "DescribeUsers",
			SignatureMethod:  "HmacSHA256",
			SignatureVersion: 1,
			Version:          1,
			Zone:             "sh1",
			TimeStamp:        time.Now().Format("2006-01-02T15:04:05Z"),
		},
	}

	result, _ := vi.buildVerifyStr()
	fmt.Println(result)
}

代码 result 结果即构造的验证请求串,由于验证信息有时效性,如中断数据发送后重新发送时间间隔大于 5 分钟,请重新构造验证请求串。

验证所构造的验证请求串是否正确

使用自己构造的验证请求串替换下面代码中 result,执行下面代码。打印结果为 1,验证请求串构造错误;打印结果为 0,验证请求串构造正确。

type successRespJson struct {
	Message interface{} `json:"message,required"`
	RetCode int         `json:"ret_code,required"`
}

func Verify(url string) int {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return 1
	}
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true, Renegotiation: tls.RenegotiateOnceAsClient},
	}
	cli := http.Client{
		Transport:     tr,
		CheckRedirect: nil,
		Jar:           nil,
		Timeout:       0,
	}
	response, _:= cli.Do(req)
	defer response.Body.Close()
	body, _:= ioutil.ReadAll(response.Body)
	var vr successRespJson
	if response.StatusCode == http.StatusOK {
		if err := json.Unmarshal(body, &vr); err != nil {
			return 1
		}
	}
	return vr.RetCode
}

func main() {
	result := "access_key_id=CCDJRDKCCKZYTEXANZJD&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-12-23T14%3A03%3A44Z&version=1&zone=sh1&signature=d6eMFDgO3E6wBqbbn2AydX%2BxQws7iD%2BtSgfGF6Lq7Uo%3D"
	url := "https://api.shanhe.com/iaas/" + "?" + result
	fmt.Println(Verify(url))
}

上传自定义数据代码示例

上传自定义数据需要预先在 CloudSat 自定义监控中创建命名空间和监控配置,并根据实际情况替换下面代码中一些数据,如 UserIdNamespaceRegionMeter 等。Meter 请与对应命名空间上监控配置中的监控指标保持一致。时间使用 UTC 时间,所有上传数据字段严格遵守接口数据规范中字段说明规范。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"strconv"
	"time"
)

type monitorData struct {
	UserId    string  `json:"user_id"`
	Namespace string  `json:"namespace"`
	Data      []meters `json:"data"`
}

type meters struct {
	Source       string `json:"source"`
	UserId       string `json:"user_id"`
	Tags         string `json:"tags"`
	GroupId      string `json:"group_id"`
	ResourceId   string `json:"resource_id"`
	ResourceName string `json:"resource_name"`
	ResourceType string `json:"resource_type"`
	RootUserId   string `json:"root_user_id"`
	Meter        string `json:"meter"`
	Region       string `json:"region"`
	Value        int    `json:"value"`
	ValueType    string `json:"value_type"`
	TimeStamp    string `json:"time_stamp"`
}

type date struct {
	UploadCount int `json:"upload_count"`
}

type ret struct {
	Data date `json:"data"`
	RetCode int `json:"ret_code"`
}

var client = http.Client{
	Timeout: 10 * time.Second,
}

func HttpPostJson(url string, data interface{}, result interface{}) error {
	buf := bytes.NewBuffer(nil)
	encoder := json.NewEncoder(buf)
	if err := encoder.Encode(data); err != nil {
		return err
	}

	request, err := http.NewRequest(http.MethodPost, url, buf)
	if err != nil {
		return err
	}

	request.Header.Add("Content-Type", "application/json")
	request.Header.Add("Content-Type", "charset=UTF-8")
	response, err := client.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	decoder := json.NewDecoder(response.Body)
	if err = decoder.Decode(&result); err != nil {
		return err
	}
	fmt.Println(result)
	return nil
}

func main() {

    for {
	    timeStamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
		rand.Seed(time.Now().UnixNano())
		flag := strconv.Itoa(rand.Intn(16))
		p := monitorData{
			UserId: "usr-12345678",
			Namespace: "cloudsat-test",
			Data: []meters{
				{
					Source: "dylan-test",
					UserId: "usr-12345678",
					Tags: "role=master,interface=eth10",
					GroupId: "group1",
					ResourceId: "i-instance-"+flag ,
					ResourceName: "name"+flag,
					ResourceType: "instance",
					RootUserId: "usr-12345678",
					Meter: "cpu",
					Region: "sh1",
					Value: rand.Intn(100),
					ValueType: "percent",
					TimeStamp:timeStamp,
				},{
					Source: "dylan-test",
					UserId: "usr-12345678",
					Tags: "role=master,interface=eth10",
					GroupId: "group1" ,
					ResourceId: "i-instance-"+flag ,
					ResourceName: "name"+flag,
					ResourceType: "instance",
					RootUserId: "usr-12345678",
					Meter: "memory",
					Region: "sh1",
					Value: rand.Intn(100),
					ValueType: "percent",
					TimeStamp:timeStamp,
				},
			},
		}
		var r ret

		url := `http://cloudsat.qingcloud.com/api/sh1/v1/custom/UploadMonitorData?access_key_id=CCDJRDKCCKZYTEXANZJD&action=DescribeUsers&signature_method=HmacSHA256&signature_version=1&time_stamp=2020-12-23T09%3A52%3A10Z&version=1&zone=sh1&signature=vorfODyQzKD0v4En7y5mdwkvUe4OZGQcDdQU5xBSjio%3D`
		err := HttpPostJson(url,p,r)
		if err != nil {
			fmt.Println(err)
		}
		time.Sleep(time.Duration(1)*time.Second)
	}

}

python 完整上传自定义数据示例

请根据实际,使用自己的 access_key_idsecret_access_keyuser_idnamespaceregionmeter 替换代码中相应变量,这里以上海1区(sh1)为例,实际应用中需要同步更新 self.CloudSatUrl 中对应的区间信息,详情参见上文。

import hmac
import json
import base64
import datetime
import time
from hashlib import sha256
from collections import OrderedDict

try:
    import urllib.parse as urllib
except:
    import urllib
import requests

def sort_value(old_dict):
    items = sorted(old_dict.items())
    new_dict = OrderedDict()
    for item in items:
        new_dict[item[0]] = old_dict[item[0]]
    return new_dict

class UploadMonitorData(object):
    def __init__(self, access_key_id, secret_access_key):
        self.access_key_id = access_key_id
        self.secret_access_key = secret_access_key

        nowtime = datetime.datetime.now()
        self.signature_time = datetime.datetime.strftime(nowtime, "%Y-%m-%dT%H:%M:%SZ")

        self.time_stamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
        self.time = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
        self.url = "https://api.shanhe.com/iaas/"
        self.CloudSatUrl = "http://cloudsat.qingcloud.com/api/sh1/v1/custom/UploadMonitorData"
        self.url_path = '/iaas/'
        self.methods = 'GET'

    def post_request(self, post_url):
        headers = {"Content-Type": "application/json; charset=UTF-8"}
        body = {
            "user_id": "usr-8Ljvov6I",
            "namespace": "pt-test",
            "data": [
                {
                    "source": "custom",
                    "user_id": "usr-8Ljvov6I",
                    "resource_id": "i-ets7af6q",
                    "resource_type": "instance",
                    "root_user_id": "usr-8Ljvov6I",
                    "meter": "cpu",
                    "region": "sh1",
                    "value": 99,
                    "value_type": "percent",
                    "time_stamp": self.time_stamp
                }, {
                    "source": "custom",
                    "user_id": "usr-8Ljvov6I",
                    "resource_id": "i-ets7af6q",
                    "resource_type": "instance",
                    "root_user_id": "usr-8Ljvov6I",
                    "meter": "memory",
                    "region": "sh1",
                    "value": 96,
                    "value_type": "percent",
                    "time_stamp": self.time_stamp
                }
            ]
        }
        result = requests.post(url=post_url, headers=headers, data=json.dumps(body))
        print(result.status_code)
        print(result.reason)
        print(result.text)

    def post_monitor_data(self):
        od = OrderedDict()
        od['access_key_id'] = self.access_key_id
        od['action'] = "DescribeUsers"
        od['signature_method'] = "HmacSHA256"
        od['signature_version'] = 1
        od['time_stamp'] = self.signature_time
        od['version'] = 1
        od['zone'] = "sh1"

        od_sort = sort_value(od)
        data = urllib.urlencode(od_sort)
        string_to_sign = self.methods + "\n" + self.url_path + "\n" + data
        print(string_to_sign)
        h = hmac.new(self.secret_access_key.encode(), digestmod=sha256)
        h.update(string_to_sign.encode())
        sign = base64.b64encode(h.digest()).strip()
        signature = urllib.quote_plus(sign)
        post_url = self.CloudSatUrl + "?" + data + "&signature=" + signature
        self.post_request(post_url)

if __name__ == '__main__':
    access_key_id = 'ACCESSKEYID'
    secret_access_key = 'SECRETACCESSKEY'

    while True:
        upload_struct_object = UploadMonitorData(access_key_id, secret_access_key)
        upload_struct_object.post_monitor_data()
        time.sleep(3)