使用Jenkins扩展钉钉消息通知

news2025/1/12 13:40:59

Jenkins借助钉钉插件,实现当构建失败时,自动触发钉钉预警。虽然插件允许自定义消息主体,支持使用 Jenkins环境变量,但是局限性依旧很大。当接收到钉钉通知后,若想进一步查看报错具体原因,仍完全依赖邮件通知,很影响效率。

如何在钉钉通知消息中,获取到本次构建的具体内容,如失败占比、失败用例报错详情等,本文记录了解决思路。最终实现结果如图:

 

 

解决思路

Jenkins+RobotFramework项目

思路如下:

1、使用Startup Trigger插件和Groovy plugin插件,在服务重启时自动修改Jenkins CSP配置,解决Robot Framework项目测试报告在Jenkins服务器上无法预览的问题,如下:

System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")

 

2、Jenkins项目构建完成后,在构建后操作中,利用Post Build Task插件提供的命令行功能,传入相关参数执行封装的脚本,如下:


if "%构建失败时触发钉钉通知%"=="true" (
    echo ************推送钉钉消息通知************
    cd %WORKSPACE%/test
    C:/python38/python.exe dingtalk.py --id=%BUILD_NUMBER% --name=%JOB_NAME% --server=xxxxxx
    echo *******************************************
    exit 0
)

说明几点:

  • “构建失败时触发钉钉通知” 是我自定义的布尔类型的项目构建参数,用于更方便的控制是否触发钉钉消息通知;

  • dingtalk.py是封装的脚本,其内容在下面会详细说明;

  • Post Build Task插件的位置必须位于Robot Framework插件之后, 否则会引发以下异常:

robot.errors.DataError: Reading XML source '<in-memory file>' failed: Incompatible XML element 'html'

3、编写第二步中调用的钉钉消息通知脚本,其核心思路可分为两点:

  • 获取并分析本次构建结果文件(如: output.xml)、提取数据,如: 失败占比、失败用例报错详情等

  • 组装Markdown格式的消息主体,然后调用DingTalk Webhook接口,推送消息通知。

核心代码如下:


def fetch_robot_output_xml(xml_name=None, save=False, cache=True) -> str:
    """ 从服务器上获取 Robot Framework 项目构建完成后生成的xml文件
    @param xml_name: xml文件名称, 部分项目会合并测试报告, 默认`merge.xml`
    @param save: 是否将文件保存到本地, 默认 False
    @param cache: 默认为True, 会首先遍历本地已保存的构建文件, 若文件名称不存在才会向服务器发起请求;
    @Returns : xml文件内容
    """
    xml_name = xml_name if xml_name else "merge.xml"
    url = url_prefix + f"{name}/{id}" + f"/robot/report/{xml_name}"

    output_xml = None
    if cache:
        for xml in robot_outputs.glob('*.xml'):
            if name in xml.stem and id in xml.stem:
                output_xml = xml.read_text(encoding='utf-8')
                break
    if not output_xml:
        print(f"发起请求获取测试报告, 目标URL: {url}")
        # curl -X GET {URL} --user {USER}:{TOKEN}
        resp = requests.get(url, auth=HTTPBasicAuth(jenkins_user.encode('utf-8'), jenkins_token.encode('utf-8')))
        resp.encoding = 'utf-8'
        output_xml = resp.text

    if save and not cache:
        save_output_xml(output_xml, f"{server}_{name}_{id}_output.xml")
    print("Done.")
    return output_xml

def parse_robot_result(xml) -> dict:
    """解析用例运行结果
    pip install robotframework==3.2.1
    获取统计信息, 优先使用此方法, 因为xml文件中可能会不包含有效的测试报告信息, 如请求获取文件服务器返回404的情况
    """
    print('解析用例运行结果...')
    try:
        suite = ExecutionResult(xml).suite
    except DataError as e:
        raise e('解析xml文件失败, 请检查构建文件内容是否完整, 确认服务器上的构建文件是否已被删除')

    all_tests = suite.statistics.critical
    total, passed, failed = all_tests.total, all_tests.passed, all_tests.failed
    pass_rate = round((passed / total) * 100, 2)
    is_success = True if failed == 0 else False

    stat = {'server': server, 'job': name, 'build_id': id, 'total': total, 'passed': passed, 'failed': failed, 'pass_rate': str(pass_rate) + "%", 'is_success': is_success}
    print('Done')
    return stat


def pick_fail_cases(xml: str) -> list:
    print('获取所有失败用例及其报错信息...')
    root = ET.fromstring(xml)

    fail_cases = []
    for case in root.iter('test'):
        status = case.find('status')
        if status.get('status') == "FAIL":
            fail_cases.append({"case": case.get('name'), "message": truncate_text(status.text)})
    print('Done.')
    return fail_cases

def package_dingtalk_text(stat: dict) -> dict:
    print("组装钉钉通知信息主体...")
    # 项目页面地址
    job_url = url_prefix + stat['job'] + '/'
    # 本次构建ID以及详情页面地址
    build_id = stat['build_id']
    build_url = job_url + build_id + '/'
    # 统计信息
    summary_text = f"用例总数:{stat['total']},失败用例数:{stat['failed']}, 通过率: {stat['pass_rate']}\n"
    # 用例报错详情
    case_error_details = ""
    num = 1  # 序号
    if len(stat['fail_cases']) > 0:
        for case in stat['fail_cases']:
            case_error_details = case_error_details + f"{num}. 用例名称: {case['case']}, 失败原因: {case['message']}\n"
            num += 1
    print("Done.")
    return {
        "server_show_name": server_show_name,
        "job_show_name": job_show_name,
        "job_url": job_url,
        "build_id": build_id,
        "build_url": build_url,
        "summary_text": summary_text,
        "case_error_details": case_error_details
    }


def ding_talk(message=None):
    """
    目前只支持markdown语法的子集, 参考官方文档: https://developers.dingtalk.com/document/app/custom-robot-access
    """
    print("推送钉钉消息通知...")
    webhook = "https://oapi.dingtalk.com/robot/send?access_token=1048af6c9a25ea5e924ae328c6782b68c48a40bcea773df96eadce783f2941ca"
    headers = {"Content-Type": "application/json", "Charset": "UTF-8"}

    payload = {
        "msgtype": "markdown",
        "markdown": {
            "title":
            "心跳测试",
            "text":
            f"# [[{message['server_show_name']}] 心跳测试-{message['job_show_name']}]({message['job_url']})\n***\n>*失败用例会自动重复执行一次, 若仍然失败, 则标记为失败*\n***\n- 任务:#[{message['build_id']}]({message['build_url']})\n- 状态:失败\n- 统计信息:{message['summary_text']}\n - 失败用例报错详情: \n\n{message['case_error_details']}\n"
        },
        "at": {
            "isAtAll": True
        }
    }
    resp = requests.post(webhook, json=payload, headers=headers)
    print(resp.text)
    print("Done.")
    return resp

Jenkins+Jmeter项目

前两步是共同的,也是通过解析result.jtl文件提取需要的数据,直接贴代码:


def fetch(xml_name=None, save_file=False) -> str:
    xml_name = xml_name if xml_name else "result.jtl"
    url = url_prefix + f"{name}/{id}" + f"/artifact/{name}result/{xml_name}"
    print(url)
    resp = requests.get(url, auth=HTTPBasicAuth(jenkins_user.encode('utf-8'), jenkins_token.encode('utf-8')))
    status_code = resp.status_code
    if status_code == 200:
        resp.encoding = 'utf-8'
        xml = resp.text

        if save_file:
            save(xml, f"{server}_{name}_{id}_{xml_name}")

        return xml
    elif status_code == 404:
        print(f"{status_code}, 指定的测试报告不存在, 可能已被删除, 请去服务器上检查")
        raise Exception("{status_code}, 指定的测试报告不存在, 可能已被删除, 请去服务器上检查")
    elif status_code == 500:
        print(f"获取测试报告失败, 详情: {status_code} >> {resp.text}")
        raise Exception(f"获取测试报告失败, 详情: {status_code} >> {resp.text}")
    else:
        return None

def parse(xml):
    """分析xml测试报告, 提取数据"""
    result = {}
    try:
        root = ET.fromstring(xml)
    except ParseError:
        raise ParseError("解析XML文件失败, 请检查服务器上文件是否存在")

    # 获取统计信息
    samples = root.findall('httpSample')
    failures = [sample for sample in samples if sample.get('s') == "false"]
    failures_cnt = len(failures)
    success = [sample for sample in samples if sample.get('s') == "true"]
    success_cnt = len(success)
    is_success = True if failures_cnt == 0 else False
    success_rate = "100%" if is_success else str(round((success_cnt / len(samples)) * 100, 2)) + "%"
    result['summary'] = {"samples": len(samples), "is_success": is_success, "failures_cnt": failures_cnt, "success_rate": success_rate}

    # 数据清洗, 获取失败用例的报错信息
    fail_details = []
    if failures_cnt > 0:
        pat = re.compile(".*R-TraceId:(.*?)\n.*", re.DOTALL)
        for sample in failures:
            msg = [] 
            for res in sample.findall('assertionResult'):
                for child in res:
                    if child.tag in ("failure", "error") and child.text == "true":
                        try:
                            msg.append(res.find('failureMessage').text)  # ?这个节点可能会不存在
                        except AttributeError:
                            msg.append("")

            response_header = sample.find('responseHeader').text
            try:
                traceid = pat.match(response_header).group(1).strip()
            except AttributeError:
                traceid = None
            response_data = sample.find('responseData').text.replace('\r\n', '')
            fail_details.append({
                'sample': sample.get('lb'),
                "traceid": traceid,
                'data': truncate_text(response_data),
                'msg': truncate_text(",".join(msg))
            })

    result['fail_samples'] = fail_details
    result.update({
        "server": server,
        'job': name,
        'build_id': id,
    })
    return result

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/76160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LWIP——无操作系统移植

目录 移植说明 LwIP前期准备 以太网DMA描述符 LwIP移植流程 添加网卡驱动程序 添加LwIP源文件 移植头文件 网卡驱动编写 移植总结 移植说明 LwIP的移植可以分为两大类&#xff1a;第一类是只移植内核核心&#xff0c;此时用户应用程序编写只能基于RaW/CallBack API进行…

mybatisplus 集成druid连接池源码分析

mybatisplus 集成druid连接池源码分析&#xff1a;从spring的源码过渡到druid的相关jar包&#xff0c;里面是druid相关的类&#xff0c;下面我们开始分析&#xff1a; 1、取数据库连接的地方入口&#xff1a;public abstract class DataSourceUtils 为spring-jdbc包里面的过渡…

深度学习 Day21——利用RNN实现心脏病预测

深度学习 Day21——利用RNN实现心脏病预测 文章目录深度学习 Day21——利用RNN实现心脏病预测一、前言二、我的环境三、什么是RNN四、前期工作1、设置GPU2、导入数据3、检查数据五、数据预处理1、划分数据集2、数据标准化六、构建RNN模型七、编译模型八、训练模型九、模型评估十…

网站TDK三大标签SEO优化

网站TDK三大标签SEO优化 SEO(Search Engine Optimization)汉译为搜索引擎优化&#xff0c;是一种利用搜索引擎的规则提高网站在有关搜索引擎内自然排名的方式 SEO的目的是对网站进行深度的优化&#xff0c;从而帮助网站获取免费的流量&#xff0c;进而在搜索引擎上提升网站的排…

day39 CSRFSSRF协议玩法内网探针漏洞利用

前言&#xff1a; #知识点&#xff1a; 1、CSRF-原理&危害&探针&利用等 2、SSRF-原理&危害&探针&利用等 3、CSRF&SSRF-黑盒下漏洞探针点 详细点&#xff1a; CSRF 全称&#xff1a;Cross-site request forgery&#xff0c;即&#xff0c;跨站…

java计算机毕业设计ssm兴发农家乐服务管理系统n159q(附源码、数据库)

java计算机毕业设计ssm兴发农家乐服务管理系统n159q&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&…

RK3568平台开发系列讲解(系统优化篇)如何进行内存优化

🚀返回专栏总目录 文章目录 一、设备分级二、Bitmap 优化三、内存泄漏沉淀、分享、成长,让自己和他人都能有所收获!😄 📢内存优化,应该从哪里着手呢?我通常会从设备分级、Bitmap 优化和内存泄漏这三个方面入手。 一、设备分级 内存优化首先需要根据设备环境来综合考虑…

硬件基础

目录 一、Cisco Packet Tracer 8.1.1安装 二、汉化 一、Cisco Packet Tracer 8.1.1安装 官方汉化包的Ciscohttps://www.netacad.com/portal/resources/browse/341e11c1-d03f-4433-9413-29b9d207e7eb 直接在官网下载但是官网有时候比较慢 思科数据包跟踪器 - 网络仿真工具 (n…

北斗/GNSS高精度数据处理暨GAMIT/GLOBK v10.75软件

随着GNSS导航定位技术在不同领域的广泛应用和技术更新的飞速发展&#xff0c;在大型工程项目的设计、施工、运行和管理各个阶段对工程测量提出了更高的要求&#xff0c;许多测绘、勘测、规划、市政、交通、铁道、水利水电、建筑、矿山、道桥、国土资源、气象、地震等行业部门在…

【实时数仓】介绍、需求分析、统计架构分析和ods层日志行为数据采集

文章目录一 电商实时数仓介绍1 普通实时计算与实时2 实时电商数仓分层二 实时数仓需求分析1 离线计算与实时计算的比较2 应数场景&#xff08;1&#xff09;日常统计报表或分析图中需要包含当日部分&#xff08;2&#xff09;实时数据大屏监控&#xff08;3&#xff09;数据预警…

不同系列的 ESP 芯片的 GPIO 默认初始状态

ESP 系列芯片的 GPIO 上电状态的含义&#xff1a; wpu: weak pull-up&#xff08;为弱上拉模式&#xff09;wpd: weak pull-down&#xff08;为弱下拉模式&#xff09;ie: input enable&#xff08;输入使能模式&#xff09;oe: output enable&#xff08;输出使能模式&#x…

【Docker】第三章 镜像管理

3.1 镜像是什么 简单说&#xff0c;Docker镜像是一个不包含Linux内核而又精简的Linux操作系统。 3.2 镜像从哪里来 Docker Hub 是由Docker公司负责维护的公共注册中心&#xff0c;包含大量的容器镜像&#xff0c;Docker工具默认从这个公共镜像库下载镜像。 https://hub.docker.…

碳中和科普

什么叫碳达峰和碳中和&#xff1f; 我国在2020年第75届联合国大会上宣布&#xff0c;二氧化碳排放量努力争取于2030年前达到峰值&#xff0c;2060年前实现碳中和。 碳达峰和碳中和中的碳指的都是以二氧化碳为代表的温室气体 碳达峰 碳达峰指的是碳排放达到峰值后进入平稳下降…

洛谷入门赛 202212F 宇宙密码 ——深搜

题目描述 经历十九年的探索&#xff0c;人们终于找到了宇宙中的那份瑰宝。 这份瑰宝被装在一个密码箱里&#xff0c;按照情报&#xff0c;密码应为一串长度为 nn 的数字 aa。 人们满怀希望地输入了密码&#xff0c;但是密码箱没有任何反应。 这时人们意识到&#xff0c;在十…

迈向高算力、跨域融合新拐点,智能座舱各路玩家如何卡位?

当前&#xff0c;中国车联网发展进入平稳增长周期&#xff0c;5G、V2X市场迎来拐点。 借助数字化转型驱动&#xff0c;互联化、数字化、个性化的智能座舱&#xff0c;以及与之强关联的座舱域控制器方案正实现快速发展和落地。 高工智能汽车研究院监测数据显示&#xff0c;202…

[附源码]Python计算机毕业设计SSM基于人脸识别和测温的宿舍管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

使用ESPRIT,LS-ESPRIT,Music以及Root-Music四种算法进行角度估计matlab仿真

目录 一、理论基础 二、核心程序 三、测试结果 一、理论基础 1.1ESPRIT ESPRIT算法全称为&#xff1a;Estimation of Signal Parameters using Rotational Invariance Techniques.与Root_MUSIC算法相同&#xff0c;也是一种参数估计技术。ESPRIT算法在旋转矢量中&#xff0…

Jetpack组件(三)Lifecycle

本篇是Jetpack组件系列文章的第三篇&#xff0c;将介绍第二个组件Lifecycle。Lifecycle为开发者管理 Activity 和 Fragment 生命周期提供了极大的便利&#xff0c;帮助开发者书写更轻量、易于维护的代码 一、Lifecycle简介 Lifecycle用于存储有关组件&#xff08;如 activity …

UE实现指北针效果

文章目录 1.实现目标2.实现过程2.1 设计指北针Widget2.2 实时指北2.3 添加到页面显示3.参考资料1.实现目标 在UE中实现指北针效果,GIF图如下。 2.实现过程 实现思路较为简单,即获取到当前场景的Rotation,来设置UMG的旋转角度即可。 2.1 设计指北针Widget 包括底图圆环,…

嵌入式开发学习之--通讯的基本概念

提示&#xff1a;本章主要了解一下通讯的基本概念&#xff0c;无代码 文章目录前言一、通讯的基本概念1.1串行通讯与并行通讯1.2全双工、半双工及单工通讯1.3同步通讯与异步通讯1.4通讯速率总结前言 对于嵌入式开发来说&#xff0c;基本就是在传递信息和解析信息&#xff0c;根…