From 6f2db114c03415295de933a3a2cb9aa32a26cf45 Mon Sep 17 00:00:00 2001 From: chyroc Date: Wed, 29 May 2024 10:05:30 +0800 Subject: [PATCH 1/2] add: EventV2CorehrOffboardingUpdatedV2 --- api_application_version_get.go | 18 +++++++++--------- api_event_callback_v2_card_action_trigger.go | 15 +++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/api_application_version_get.go b/api_application_version_get.go index 46360f75..a88e3aa2 100644 --- a/api_application_version_get.go +++ b/api_application_version_get.go @@ -81,13 +81,13 @@ type GetApplicationVersionRespAppVersion struct { BackHomeURL string `json:"back_home_url,omitempty"` // 后台主页地址 I18n []*GetApplicationVersionRespAppVersionI18n `json:"i18n,omitempty"` // 应用的国际化信息列表 CommonCategories []string `json:"common_categories,omitempty"` // 应用分类的国际化描述 - EventInfos []*GetApplicationVersionRespAppVersionEventInfo `json:"event_infos,omitempty"` // Events []string `json:"events,omitempty"` // 应用已订阅开放平台事件列表 Status int64 `json:"status,omitempty"` // 版本状态, 可选值有: 0: 未知状态, 1: 审核通过, 2: 审核拒绝, 3: 审核中, 4: 未提交审核 CreateTime string `json:"create_time,omitempty"` // 版本创建时间(单位: s) PublishTime string `json:"publish_time,omitempty"` // 版本发布时间(单位: s) Ability *GetApplicationVersionRespAppVersionAbility `json:"ability,omitempty"` // 当前版本下应用开启的能力 Remark *GetApplicationVersionRespAppVersionRemark `json:"remark,omitempty"` // 跟随应用版本的信息 + EventInfos []*GetApplicationVersionRespAppVersionEventInfo `json:"event_infos,omitempty"` // 应用已订阅事件详情列表 } // GetApplicationVersionRespAppVersionAbility ... @@ -199,21 +199,21 @@ type GetApplicationVersionRespAppVersionAbilityWorkplaceWidget struct { MinLarkVersion string `json:"min_lark_version,omitempty"` // 最低兼容飞书版本号 } +// GetApplicationVersionRespAppVersionEventInfo ... +type GetApplicationVersionRespAppVersionEventInfo struct { + EventType string `json:"event_type,omitempty"` // 事件类型, 事件唯一标识 + EventName string `json:"event_name,omitempty"` // 事件名称 + EventDescription string `json:"event_description,omitempty"` // 事件描述 +} + // GetApplicationVersionRespAppVersionI18n ... type GetApplicationVersionRespAppVersionI18n struct { - I18nKey string `json:"i18n_key,omitempty"` // 国际化语言的 key, 可选值有: zh_cn: 中文, en_us: 英文, ja_jp: 日文 + I18nKey string `json:"i18n_key,omitempty"` // 国际化语言的 key, 可选值有: zh_cn: 中文, en_us: 英文, ja_jp: 日文, zh_hk: 繁体中文(中国香港), zh_tw: 繁体中文(中国台湾), id_id: 印度尼西亚语, ms_my: 马来语, de_de: 德语, es_es: 西班牙语, fr_fr: 法语, it_it: 意大利语, pt_br: 葡萄牙语(巴西), vi_vn: 越南语, ru_ru: 俄语, th_th: 泰语, ko_kr: 韩语 Name string `json:"name,omitempty"` // 应用国际化名称 Description string `json:"description,omitempty"` // 应用国际化描述(副标题) HelpUse string `json:"help_use,omitempty"` // 国际化帮助文档链接 } -// GetApplicationVersionRespAppVersionEventInfo ... -type GetApplicationVersionRespAppVersionEventInfo struct { - EventDescription string `json:"event_description"` - EventName string `json:"event_name"` - EventType string `json:"event_type"` -} - // GetApplicationVersionRespAppVersionRemark ... type GetApplicationVersionRespAppVersionRemark struct { Remark string `json:"remark,omitempty"` // 备注说明 diff --git a/api_event_callback_v2_card_action_trigger.go b/api_event_callback_v2_card_action_trigger.go index 14963314..c12e1ed5 100644 --- a/api_event_callback_v2_card_action_trigger.go +++ b/api_event_callback_v2_card_action_trigger.go @@ -19,7 +19,6 @@ package lark import ( "context" - "encoding/json" ) // EventV2CardActionTrigger 卡片回传交互作用于飞书卡片的 请求回调 交互组件。当终端用户点击飞书卡片上的回传交互组件后, 你在开发者后台应用内注册的回调请求地址将会收到 卡片回传交互 回调。该回调包含了用户与卡片之间的交互信息。 @@ -46,11 +45,15 @@ type EventV2CardActionTrigger struct { // EventV2CardActionTriggerAction ... type EventV2CardActionTriggerAction struct { - Value json.RawMessage `json:"value,omitempty"` // 交互元素的 value 字段值。 - Tag string `json:"tag,omitempty"` // 交互元素的 tag 字段值。 - Option json.RawMessage `json:"option,omitempty"` // 选中 option 的 value。(button 元素不适用) - Name string `json:"name,omitempty"` // 表单容器中,“提交”按钮组件本身的回传属性 - FormValue json.RawMessage `json:"form_value"` // 表单容器内用户提交的数据 + Value *EventV2CardActionTriggerActionValue `json:"value,omitempty"` // 交互组件绑定的开发者自定义回传数据, 对应组件中的 value 属性。 + Tag string `json:"tag,omitempty"` // 交互组件的标签。 + Timezone string `json:"timezone,omitempty"` // 用户当前所在地区的时区。当用户操作日期选择器、时间选择器、或日期时间选择器时返回。 + Name string `json:"name,omitempty"` // 组件的自定义唯一标识, 用于识别内嵌在表单容器中的某个组件。 + FormValue *EventV2CardActionTriggerActionFormValue `json:"form_value,omitempty"` // 表单容器内用户提交的数据。示例值: ```JSON, {, "field name 1": [ // 表单容器内某多选组件的 name 和 value, "selectDemo1", "selectDemo2", ], "field name 2": "value 2", // 表单容器内某交互组件的 name 和 value, "field name 3": "value 3", // 表单容器内某交互组件的 name 和 value, }, ``` + InputValue string `json:"input_value,omitempty"` // 当输入框组件未内嵌在表单容器中时, 用户在输入框中提交的数据。 + Option string `json:"option,omitempty"` // 当折叠按钮组、下拉选择-单选、人员选择-单选、日期选择器、时间选择器、日期时间选择器组件未内嵌在表单容器中时, 用户选择该类组件某个选项时, 组件返回的选项回调值。 + Options string `json:"options,omitempty"` // 当下拉选择-多选组件和人员选择-多选组件未内嵌在表单容器中时, 用户选择该类组件某个选项时, 组件返回的选项回调值。 + Checked bool `json:"checked,omitempty"` // 当勾选器组件未内嵌在表单容器中时, 勾选器组件的回调数据。 } // EventV2CardActionTriggerContext ... From 2cab63831de5b8d713407bb4f73c3902f11a8c59 Mon Sep 17 00:00:00 2001 From: chyroc Date: Wed, 29 May 2024 18:16:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api_application_version_get.go | 18 ++--- api_auth_app_access_token_get.go | 14 +--- api_auth_app_ticket.go | 8 +- api_auth_tenant_access_token_get.go | 29 +++---- api_event_callback_v2_card_action_trigger.go | 15 ++-- impl_request.go | 81 +++++++++++++------- lark.go | 6 ++ logger.go | 46 ++++++++++- 8 files changed, 138 insertions(+), 79 deletions(-) diff --git a/api_application_version_get.go b/api_application_version_get.go index a88e3aa2..46360f75 100644 --- a/api_application_version_get.go +++ b/api_application_version_get.go @@ -81,13 +81,13 @@ type GetApplicationVersionRespAppVersion struct { BackHomeURL string `json:"back_home_url,omitempty"` // 后台主页地址 I18n []*GetApplicationVersionRespAppVersionI18n `json:"i18n,omitempty"` // 应用的国际化信息列表 CommonCategories []string `json:"common_categories,omitempty"` // 应用分类的国际化描述 + EventInfos []*GetApplicationVersionRespAppVersionEventInfo `json:"event_infos,omitempty"` // Events []string `json:"events,omitempty"` // 应用已订阅开放平台事件列表 Status int64 `json:"status,omitempty"` // 版本状态, 可选值有: 0: 未知状态, 1: 审核通过, 2: 审核拒绝, 3: 审核中, 4: 未提交审核 CreateTime string `json:"create_time,omitempty"` // 版本创建时间(单位: s) PublishTime string `json:"publish_time,omitempty"` // 版本发布时间(单位: s) Ability *GetApplicationVersionRespAppVersionAbility `json:"ability,omitempty"` // 当前版本下应用开启的能力 Remark *GetApplicationVersionRespAppVersionRemark `json:"remark,omitempty"` // 跟随应用版本的信息 - EventInfos []*GetApplicationVersionRespAppVersionEventInfo `json:"event_infos,omitempty"` // 应用已订阅事件详情列表 } // GetApplicationVersionRespAppVersionAbility ... @@ -199,21 +199,21 @@ type GetApplicationVersionRespAppVersionAbilityWorkplaceWidget struct { MinLarkVersion string `json:"min_lark_version,omitempty"` // 最低兼容飞书版本号 } -// GetApplicationVersionRespAppVersionEventInfo ... -type GetApplicationVersionRespAppVersionEventInfo struct { - EventType string `json:"event_type,omitempty"` // 事件类型, 事件唯一标识 - EventName string `json:"event_name,omitempty"` // 事件名称 - EventDescription string `json:"event_description,omitempty"` // 事件描述 -} - // GetApplicationVersionRespAppVersionI18n ... type GetApplicationVersionRespAppVersionI18n struct { - I18nKey string `json:"i18n_key,omitempty"` // 国际化语言的 key, 可选值有: zh_cn: 中文, en_us: 英文, ja_jp: 日文, zh_hk: 繁体中文(中国香港), zh_tw: 繁体中文(中国台湾), id_id: 印度尼西亚语, ms_my: 马来语, de_de: 德语, es_es: 西班牙语, fr_fr: 法语, it_it: 意大利语, pt_br: 葡萄牙语(巴西), vi_vn: 越南语, ru_ru: 俄语, th_th: 泰语, ko_kr: 韩语 + I18nKey string `json:"i18n_key,omitempty"` // 国际化语言的 key, 可选值有: zh_cn: 中文, en_us: 英文, ja_jp: 日文 Name string `json:"name,omitempty"` // 应用国际化名称 Description string `json:"description,omitempty"` // 应用国际化描述(副标题) HelpUse string `json:"help_use,omitempty"` // 国际化帮助文档链接 } +// GetApplicationVersionRespAppVersionEventInfo ... +type GetApplicationVersionRespAppVersionEventInfo struct { + EventDescription string `json:"event_description"` + EventName string `json:"event_name"` + EventType string `json:"event_type"` +} + // GetApplicationVersionRespAppVersionRemark ... type GetApplicationVersionRespAppVersionRemark struct { Remark string `json:"remark,omitempty"` // 备注说明 diff --git a/api_auth_app_access_token_get.go b/api_auth_app_access_token_get.go index be2ed4bb..ec723284 100644 --- a/api_auth_app_access_token_get.go +++ b/api_auth_app_access_token_get.go @@ -24,15 +24,13 @@ import ( // docs: https://open.feishu.cn/document/ukTMukTMukTM/uADN14CM0UjLwQTN func (r *AuthService) GetAppAccessToken(ctx context.Context) (*TokenExpire, *Response, error) { if r.cli.mock.mockGetAppAccessToken != nil { - r.cli.Log(ctx, LogLevelDebug, "[lark] Auth#GetAppAccessToken mock enable") + r.cli.Log(ctx, LogLevelTrace, "[lark] Auth#GetAppAccessToken mock enable") return r.cli.mock.mockGetAppAccessToken(ctx) } - r.cli.Log(ctx, LogLevelInfo, "[lark] Auth#GetAppAccessToken call api") - val, ttl, err := r.cli.store.Get(ctx, genAppTokenKey(r.cli.isISV, r.cli.appID)) if err != nil && err != ErrStoreNotFound { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken get token from store failed: %s", err) + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken get_token_cache failed, err=%s", err) } else if val != "" && ttl > 0 { return &TokenExpire{Token: val, Expire: int64(ttl.Seconds())}, &Response{}, nil } @@ -64,18 +62,12 @@ func (r *AuthService) GetAppAccessToken(ctx context.Context) (*TokenExpire, *Res response, err := r.cli.RawRequest(ctx, req, resp) if err != nil { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken POST %s failed: %s", uri, err) return nil, response, err - } else if resp.Code != 0 { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken POST %s failed, code: %d, msg: %s", uri, resp.Code, resp.Msg) - return nil, response, NewError("Token", "GetAppAccessToken", resp.Code, resp.Msg) } - r.cli.Log(ctx, LogLevelDebug, "[lark] Auth#GetAppAccessToken log_id: %s, response: %s", response.LogID, jsonString(resp)) - err = r.cli.store.Set(ctx, genAppTokenKey(r.cli.isISV, r.cli.appID), resp.AppAccessToken, time.Second*time.Duration(resp.Expire)) if err != nil { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken set token to store failed: %s", err) + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppAccessToken set_token_cache failed, err=%s", err) } return &TokenExpire{ diff --git a/api_auth_app_ticket.go b/api_auth_app_ticket.go index 182da0ef..67833bfa 100644 --- a/api_auth_app_ticket.go +++ b/api_auth_app_ticket.go @@ -28,12 +28,18 @@ func (r *Lark) WithTenant(tenantKey string) *Lark { // GetAppTicket ... func (r *AuthService) GetAppTicket(ctx context.Context) (string, error) { s, _, err := r.cli.store.Get(ctx, genISVAppTicketKey(r.cli.appID)) + if err != nil && err != ErrStoreNotFound { + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppTicket get_ticket_cache failed, app_id=%s, err=%s", r.cli.appID, err) + } if err == ErrStoreNotFound && r.cli.getAppTicketFunc != nil { ticket, err := r.cli.getAppTicketFunc(ctx, r.cli, r.cli.appID) if err != nil { + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppTicket get_ticket failed, app_id=%s, err=%s", r.cli.appID, err) return "", err } - _ = r.SetAppTicket(ctx, ticket) + if err = r.SetAppTicket(ctx, ticket); err != nil { + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetAppTicket set_ticket_cache failed, app_id=%s, err=%s", r.cli.appID, err) + } return ticket, nil } return s, err diff --git a/api_auth_tenant_access_token_get.go b/api_auth_tenant_access_token_get.go index a0e7b76f..54b8ef9e 100644 --- a/api_auth_tenant_access_token_get.go +++ b/api_auth_tenant_access_token_get.go @@ -26,15 +26,13 @@ import ( // https://open.feishu.cn/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/tenant_access_token func (r *AuthService) GetTenantAccessToken(ctx context.Context) (*TokenExpire, *Response, error) { if r.cli.mock.mockGetTenantAccessToken != nil { - r.cli.Log(ctx, LogLevelDebug, "[lark] Auth#GetTenantAccessToken mock enable") + r.cli.Log(ctx, LogLevelTrace, "[lark] Auth#GetTenantAccessToken mock enable") return r.cli.mock.mockGetTenantAccessToken(ctx) } - r.cli.Log(ctx, LogLevelInfo, "[lark] Auth#GetTenantAccessToken call api") - val, ttl, err := r.cli.store.Get(ctx, genTenantTokenKey(r.cli.isISV, r.cli.appID, r.cli.tenantKey)) if err != nil && err != ErrStoreNotFound { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken get token from store failed: %s", err) + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken get_from_cache failed, err=%s", err) } else if val != "" && ttl > 0 { return &TokenExpire{Token: val, Expire: int64(ttl.Seconds())}, &Response{}, nil } @@ -67,20 +65,12 @@ func (r *AuthService) GetTenantAccessToken(ctx context.Context) (*TokenExpire, * response, err := r.cli.RawRequest(ctx, req, resp) if err != nil { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken GET %s failed: %s", uri, err) return nil, response, err - } else if resp.Code != 0 { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken GET %s failed, code: %d, msg: %s", uri, resp.Code, resp.Msg) - return nil, response, NewError("Token", "GetTenantAccessToken", resp.Code, resp.Msg) } - r.cli.Log(ctx, LogLevelDebug, "[lark] Auth#GetTenantAccessToken log_id: %s, response: %s", response.LogID, jsonString(resp)) - - err = r.cli.store.Set(ctx, genTenantTokenKey(r.cli.isISV, r.cli.appID, r.cli.tenantKey), resp.TenantAccessToken, time.Second*time.Duration(resp.Expire)) - if err != nil { - r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken set token to store failed: %s", err) + if err = r.cli.store.Set(ctx, genTenantTokenKey(r.cli.isISV, r.cli.appID, r.cli.tenantKey), resp.TenantAccessToken, time.Second*time.Duration(resp.Expire)); err != nil { + r.cli.Log(ctx, LogLevelError, "[lark] Auth#GetTenantAccessToken set_token_cache failed, err=%s", err) } - return &TokenExpire{ Token: resp.TenantAccessToken, Expire: resp.Expire, @@ -112,9 +102,10 @@ type getTenantAccessTokenReq struct { } type getTenantAccessTokenResp struct { - Code int64 `json:"code,omitempty"` // 错误码,非 0 表示失败 - Msg string `json:"msg,omitempty"` // 错误描述 - TenantAccessToken string `json:"tenant_access_token"` - AppAccessToken string `json:"app_access_token"` - Expire int64 `json:"expire"` + Code int64 `json:"code,omitempty"` // 错误码,非 0 表示失败 + Msg string `json:"msg,omitempty"` // 错误描述 + TenantAccessToken string `json:"tenant_access_token"` + AppAccessToken string `json:"app_access_token"` + Expire int64 `json:"expire"` + Error *ErrorDetail `json:"error,omitempty"` } diff --git a/api_event_callback_v2_card_action_trigger.go b/api_event_callback_v2_card_action_trigger.go index c12e1ed5..14963314 100644 --- a/api_event_callback_v2_card_action_trigger.go +++ b/api_event_callback_v2_card_action_trigger.go @@ -19,6 +19,7 @@ package lark import ( "context" + "encoding/json" ) // EventV2CardActionTrigger 卡片回传交互作用于飞书卡片的 请求回调 交互组件。当终端用户点击飞书卡片上的回传交互组件后, 你在开发者后台应用内注册的回调请求地址将会收到 卡片回传交互 回调。该回调包含了用户与卡片之间的交互信息。 @@ -45,15 +46,11 @@ type EventV2CardActionTrigger struct { // EventV2CardActionTriggerAction ... type EventV2CardActionTriggerAction struct { - Value *EventV2CardActionTriggerActionValue `json:"value,omitempty"` // 交互组件绑定的开发者自定义回传数据, 对应组件中的 value 属性。 - Tag string `json:"tag,omitempty"` // 交互组件的标签。 - Timezone string `json:"timezone,omitempty"` // 用户当前所在地区的时区。当用户操作日期选择器、时间选择器、或日期时间选择器时返回。 - Name string `json:"name,omitempty"` // 组件的自定义唯一标识, 用于识别内嵌在表单容器中的某个组件。 - FormValue *EventV2CardActionTriggerActionFormValue `json:"form_value,omitempty"` // 表单容器内用户提交的数据。示例值: ```JSON, {, "field name 1": [ // 表单容器内某多选组件的 name 和 value, "selectDemo1", "selectDemo2", ], "field name 2": "value 2", // 表单容器内某交互组件的 name 和 value, "field name 3": "value 3", // 表单容器内某交互组件的 name 和 value, }, ``` - InputValue string `json:"input_value,omitempty"` // 当输入框组件未内嵌在表单容器中时, 用户在输入框中提交的数据。 - Option string `json:"option,omitempty"` // 当折叠按钮组、下拉选择-单选、人员选择-单选、日期选择器、时间选择器、日期时间选择器组件未内嵌在表单容器中时, 用户选择该类组件某个选项时, 组件返回的选项回调值。 - Options string `json:"options,omitempty"` // 当下拉选择-多选组件和人员选择-多选组件未内嵌在表单容器中时, 用户选择该类组件某个选项时, 组件返回的选项回调值。 - Checked bool `json:"checked,omitempty"` // 当勾选器组件未内嵌在表单容器中时, 勾选器组件的回调数据。 + Value json.RawMessage `json:"value,omitempty"` // 交互元素的 value 字段值。 + Tag string `json:"tag,omitempty"` // 交互元素的 tag 字段值。 + Option json.RawMessage `json:"option,omitempty"` // 选中 option 的 value。(button 元素不适用) + Name string `json:"name,omitempty"` // 表单容器中,“提交”按钮组件本身的回传属性 + FormValue json.RawMessage `json:"form_value"` // 表单容器内用户提交的数据 } // EventV2CardActionTriggerContext ... diff --git a/impl_request.go b/impl_request.go index 6119bef8..8dc8414f 100644 --- a/impl_request.go +++ b/impl_request.go @@ -70,33 +70,62 @@ func (r *Mock) UnMockRawRequest() { } func (r *Lark) rawRequest(ctx context.Context, req *RawRequestReq, resp interface{}) (response *Response, err error) { - r.Log(ctx, LogLevelInfo, "[lark] %s#%s call api", req.Scope, req.API) - // 1. parse request rawHttpReq, err := r.parseRawHttpRequest(ctx, req) if err != nil { + // 这里日志不需要区分 level, 输出 [error] 日志 + r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s parse_req failed, err=%s", req.Scope, req.API, req.Method, req.URL, err) return response, err } - // 2. do request - response, err = r.doRequest(ctx, rawHttpReq, resp) + // 2. request log + logLevel := r.getLogLevel(req.Scope, req.API) + switch logLevel { + case LogLevelDebug: + r.Log(ctx, LogLevelDebug, "[lark] %s#%s %s %s start, body=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, string(rawHttpReq.RawBody)) + case LogLevelInfo: + r.Log(ctx, LogLevelInfo, "[lark] %s#%s %s %s start", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL) + default: + // error 不需要 req 日志, 合并到 resp 一起 + } - logID, statusCode := getResponseLogID(response) + // 3. do request + var respContent string + response, respContent, err = r.doRequest(ctx, rawHttpReq, resp) if err != nil { - r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s failed, log_id: %s, status_code: %d, error: %s", req.Scope, req.API, req.Method, req.URL, logID, statusCode, err) + switch logLevel { + case LogLevelDebug: + // [debug]: 详细 error 日志 + r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s failed, log_id=%s, status=%d, body=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, response.LogID, response.StatusCode, respContent) + default: + // [其他]: 简单 error 日志 + r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s failed, log_id=%s, status=%d", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, response.LogID, response.StatusCode) + } return response, err } - code, msg, detailErr := getCodeMsg(resp) + + // 4. response log + if response.StatusCode >= http.StatusBadRequest || code != 0 { + r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s failed, log_id=%s, status=%d, code=%d, msg=%s, detail=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, response.LogID, response.StatusCode, code, msg, detailErr) + } else { + switch logLevel { + case LogLevelDebug: + r.Log(ctx, LogLevelDebug, "[lark] %s#%s %s %s success, log_id=%s, body=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, response.LogID, respContent) + case LogLevelInfo: + r.Log(ctx, LogLevelInfo, "[lark] %s#%s %s %s success, log_id=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, response.LogID) + default: + // error 不需要 resp 日志 + } + } + + // 5. response if code != 0 { e := NewError(req.Scope, req.API, code, msg) e.(*Error).ErrorDetail = detailErr - r.Log(ctx, LogLevelError, "[lark] %s#%s %s %s failed, log_id: %s, status_code: %d, code: %d, msg: %s", req.Scope, req.API, req.Method, req.URL, logID, statusCode, code, msg) return response, e } - r.Log(ctx, LogLevelDebug, "[lark] %s#%s success, log_id: %s, status_code: %d, response: %s", req.Scope, req.API, logID, statusCode, "TODO") - return response, nil } @@ -149,17 +178,12 @@ func (r *Lark) parseRawHttpRequest(ctx context.Context, req *RawRequestReq) (*ra return rawHttpReq, nil } -func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realResponse interface{}) (*Response, error) { +func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realResponse interface{}) (*Response, string, error) { response := new(Response) response.Method = rawHttpReq.Method response.URL = rawHttpReq.URL response.Header = map[string][]string{} - if r.logLevel <= LogLevelTrace { - r.Log(ctx, LogLevelTrace, "[lark] request %s#%s, %s %s, header=%s, body=%s", rawHttpReq.Scope, rawHttpReq.API, - rawHttpReq.Method, rawHttpReq.URL, jsonHeader(rawHttpReq.Headers), string(rawHttpReq.RawBody)) - } - if rawHttpReq.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, rawHttpReq.Timeout) @@ -168,7 +192,7 @@ func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realRe req, err := http.NewRequestWithContext(ctx, rawHttpReq.Method, rawHttpReq.URL, rawHttpReq.Body) if err != nil { - return response, err + return response, "", err } for k, v := range rawHttpReq.Headers { req.Header.Set(k, v) @@ -176,7 +200,7 @@ func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realRe resp, err := r.httpClient.Do(ctx, req) if err != nil { - return response, err + return response, "", err } _, media, _ := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) @@ -197,15 +221,14 @@ func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realRe bs, err := ioutil.ReadAll(resp.Body) if err != nil { - return response, err + return response, "", err } - if r.logLevel <= LogLevelTrace { - if respFilename == "" { - r.Log(ctx, LogLevelTrace, "[lark] response %s#%s, %s %s, body=%s", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, string(bs)) - } else { - r.Log(ctx, LogLevelTrace, "[lark] response %s#%s, %s %s, body=", rawHttpReq.Scope, rawHttpReq.API, rawHttpReq.Method, rawHttpReq.URL, len(bs)) - } + var respContent string + if respFilename == "" { + respContent = string(bs) + } else { + respContent = fmt.Sprintf("", len(bs)) } if realResponse != nil { @@ -220,20 +243,20 @@ func (r *Lark) doRequest(ctx context.Context, rawHttpReq *rawHttpRequest, realRe setter.SetFilename(respFilename) } if isSpecResp { - return response, nil + return response, respContent, nil } } if len(bs) == 0 && resp.StatusCode >= http.StatusBadRequest { - return response, fmt.Errorf("request fail: %s", resp.Status) + return response, respContent, fmt.Errorf("request fail: %s", resp.Status) } if err = json.Unmarshal(bs, realResponse); err != nil { - return response, fmt.Errorf("invalid json: %s, err: %s", bs, err) + return response, respContent, fmt.Errorf("invalid json: %s, err: %s", bs, err) } } - return response, nil + return response, respContent, nil } func (r *rawHttpRequest) parseHeader(ctx context.Context, ins *Lark, req *RawRequestReq) error { diff --git a/lark.go b/lark.go index b854b7e9..11de4ef3 100644 --- a/lark.go +++ b/lark.go @@ -157,6 +157,12 @@ func WithNonBlockingCallback(noBlocking bool) ClientOptionFunc { } } +func WithDisableErrorLog(disableErrorLog bool) ClientOptionFunc { + return func(lark *Lark) { + lark.disableErrorLog = disableErrorLog + } +} + // MethodOptionFunc new method option type MethodOptionFunc func(*MethodOption) diff --git a/logger.go b/logger.go index 607c96bc..d3419499 100644 --- a/logger.go +++ b/logger.go @@ -32,11 +32,33 @@ type Logger interface { } // LogLevel ... +// +// 正常情况下: +// trace: 大于两条日志: 1,2. Token 获取日志, 3,4: 同 debug 的两条日志 +// debug: 两条日志: 1. xx start, body=, 2. xx success, log_id=, body= +// info: 两条日志: 1. xx start 2. xx success, log_id= +// warn,error: 没有日志 +// +// 出错的情况: +// 各日志级别都会输出 error 级别的日志, 如果是<=debug, 会有 body, 否则没有 +// 如果不希望输出 error 日志, 可以通过 WithDisableErrorLog 将 error 降级到 warn 日志 +// +// 1. 开启了 mock 的情况下, mock 链路打印一条 debug 日志 +// +// 2. req: +// 2.1 trace/debug: [debug] 日志 + 请求体 +// 2.2 info: [info] 日志 + 无请求体 +// 2.3 warn/error: 没有日志 +// +// 3. resp: +// 3.1 trace/debug: 无论有无错误: [debug] 日志 + 返回体 +// 3.2 info: 无论有无错误: [info] 日志 + 无返回体 +// 3.3 warn/error: 有错误: [error] 日志 + 无返回体; 无错误: 无日志 type LogLevel int // LogLevelTrace ... const ( - LogLevelTrace LogLevel = iota + 1 // 只有两个 log req 和 resp 的 文本内容 + LogLevelTrace LogLevel = iota + 1 LogLevelDebug LogLevelInfo LogLevelWarn @@ -61,7 +83,29 @@ func (r LogLevel) String() string { } } +func (r *Lark) getLogLevel(scope, api string) LogLevel { + switch api { + case "GetAppAccessToken", "GetTenantAccessToken", "GetAppTicket": + // 针对内部自动调用的 3 个 token 相关接口 + // 如果 log 级别被调用方设置为 trace, 则表示调用方需要非常详细的日志, 仍旧输出 token 接口详细日志 + // 否则, 认为 token 的 req 和 resp 不需要日志, 只需要 error 日志 + if r.logLevel == LogLevelTrace { + return LogLevelDebug + } + return LogLevelError + } + if r.logLevel <= LogLevelDebug { + return LogLevelDebug + } else if r.logLevel <= LogLevelInfo { + return LogLevelInfo + } + return LogLevelError +} + func (r *Lark) Log(ctx context.Context, level LogLevel, msg string, args ...interface{}) { + if level == LogLevelError && r.disableErrorLog { + level = LogLevelWarn + } if r.logger != nil && r.logLevel <= level { r.logger.Log(ctx, level, msg, args...) }