GitLab探秘:揭秘研发效能观测的卓越之道

350 阅读16分钟

研发效能观测的背景

在当今快节奏的软件开发环境中,团队需要确保其研发流程高效、质量可控,并能快速响应变化的需求。GitLab作为一个综合性的版本控制和协作平台,为团队提供了丰富的功能和工具来管理代码、协作开发和实施持续集成/持续交付(CI/CD)流程。然而,仅仅使用GitLab本身并不足以确保团队的研发效能达到最佳状态。

在这样的背景下,研发效能观测成为团队取得成功的关键要素之一。通过对研发流程中的关键指标进行监控和分析,团队能够获得有关研发效能的实时数据,并基于这些数据做出明智的决策和改进。这种观测实践使团队能够追踪研发活动的质量、效率和变化趋势,从而不断提升团队的研发效能。

本文旨在探讨GitLab研发效能观测的最佳实践,为团队提供一套实用的指南和建议。我们将讨论如何确定关键指标来度量研发效能,配置GitLab的监控工具来收集关键指标的数据,以及如何可视化和报告这些数据以支持团队的决策和改进。同时,我们还将分享一些数据分析和案例分析的方法,以帮助我们更好地理解如何应用这些最佳实践到自己的研发流程中。

研发效能观测的重要性

研发效能观测的关注对于组织和团队的成功至关重要。通过观测和监测研发效能,可以帮助我们做出更明智的决策,并持续改进软件开发过程。我们可以从以下五个方面来具体阐述一下研发效能观测的重要性:

  1. 评估团队绩效:研发效能观测允许团队和组织对其研发过程和绩效进行评估。通过监测关键指标和度量,可以了解团队的工作效率、质量和交付速度。这有助于发现潜在的瓶颈和改进机会,使团队能够作出有针对性的决策。
  2. 持续改进:通过观测研发效能,团队可以识别问题并制定改进计划。通过分析数据和指标,可以确定需要优化的领域,并采取措施来提高效率、减少浪费和提升质量。这种持续改进的循环可以推动团队的增长和成熟。
  3. 提高交付速度:研发效能观测有助于加速产品或功能的交付速度。通过使用自动化流程、持续集成和持续交付,团队可以更快地将软件发布到市场,并及时响应用户需求和市场竞争。及时的交付可以为组织带来竞争优势和商业价值。
  4. 优化资源利用:研发效能观测可以帮助团队更好地管理和利用资源。通过了解工作负载、任务分配和团队成员的能力,可以更好地规划资源分配,确保合理的工作负荷和高效的协作。
  5. 促进团队协作:通过共享数据和指标,研发效能观测可以促进团队之间的协作和沟通。团队成员可以更好地了解彼此的工作状态、进展和依赖关系,从而更好地协调合作。这种透明度有助于构建团队的信任和合作精神。

如何利用 GitLab 快速构建研发效能观测

GitLab 提供的功能

GitLab 提供了一系列功能性的 API,用于支持研发效能的可观测性。通过这些 API,我们可以实现自动化数据收集、进行分析和生成自定义报告。以下是 GitLab 提供的一些关键 API 功能:

  1. 代码仓库 API:GitLab 的代码仓库 API 允许访问和操作代码仓库中的各种资源。可以通过 API 获取提交历史、文件内容、分支信息,还可以创建、更新和删除代码库中的文件。
  2. CI/CD API:CI/CD API 提供了对 GitLab CI/CD 流程的控制和管理能力。可以使用 API 触发构建作业、获取构建和部署状态,以及查看构建日志和报告等。
  3. 错误跟踪 API:GitLab 的错误跟踪 API 允许创建、更新和关闭问题、缺陷和错误报告。通过 API,可以捕获应用程序的问题,并将其与代码库中的提交关联起来。
  4. 代码质量 API:代码质量 API 允许获取有关代码质量分析的数据。可以使用 API 获取代码静态分析报告、代码覆盖率信息、代码复杂度和其他与代码质量相关的指标。
  5. 仪表板和报告 API:GitLab 的仪表板和报告 API 提供了对仪表板和报告的操作和管理能力。可以使用 API 创建、更新和删除仪表板,以及配置报告的显示内容和格式。
  6. 项目管理 API:GitLab 提供了丰富的项目管理 API,用于创建、更新和删除项目、任务、问题和里程碑等。通过 API,可以管理项目的结构、任务分配和进度追踪。
  7. 用户和权限 API:用户和权限 API 允许管理 GitLab 上的用户、组织和权限。可以使用 API 创建和管理用户、组织和访问令牌,并为它们分配适当的权限和角色。

通过这些功能性的 API,可以根据组织和团队的需求,定制和集成 GitLab 的功能,实现自动化的研发效能观测和分析。通过编程方式访问和操作 GitLab 的数据,可以构建自己的数据收集和分析工具,生成定制化的报告和仪表板,以满足特定的监控和度量需求。

设定研发效能观测的关键指标

我们可以通过GitLab的内置功能、API和集成的监测工具来收集和分析。通过持续观察指标,来识别问题、改进流程,并达到持续提升团队的研发效能的效果。

  • 代码提交频率:关注每个团队成员的代码提交频率,以了解开发活动的规模和速度。较高的提交频率可能表示团队的活跃程度和迭代速度。
  • 构建成功率:监测持续集成(CI)流水线中构建的成功率,这反映了代码的稳定性和质量。低的构建成功率可能意味着存在构建失败、错误或集成问题。
  • 构建和部署时间:观察构建和部署的时间,包括构建、测试和部署到生产环境的整体耗时。较短的构建和部署时间有助于实现快速迭代和持续交付。
  • 缺陷修复时间:追踪从缺陷报告到修复完成的时间,以便及时处理问题。较短的缺陷修复时间有助于提高软件的稳定性和用户满意度。
  • 代码质量指标:监测代码的质量,例如代码复杂度、重复代码、代码规范遵循情况等。这些指标可以帮助发现潜在的问题和代码质量改进机会。
  • 响应时间:跟踪团队对问题和合并请求的响应时间。较短的响应时间可以加快反馈循环和团队协作。
  • 上线频率:关注团队的上线频率,即新功能和修复的发布频率。较高的上线频率可能表示团队的敏捷度和持续交付能力。
  • 人力均衡度:团队成员之间工作负荷的平衡程度,确保每个团队成员的工作量相对均衡,避免出现过度负荷或闲置的情况。人力均衡度的目标是优化团队的生产力和效率。如果某些成员过度负荷,可能会导致工作质量下降、疲劳和项目延误。而在某些成员闲置的情况下,可能浪费了资源和潜在能力。通过评估人力均衡度,可以调整工作分配和资源管理,确保团队的工作负荷均衡,提高整体效能。
  • 千行代码 BUG 率:每千行代码中所包含的缺陷数量。它用于衡量代码的稳定性和质量。较低的BUG率通常表示代码的健壮性较高,而较高的BUG率可能意味着存在更多的错误和缺陷。通过跟踪和监测BUG率,可以评估开发团队的质量控制措施、代码审查和测试流程的有效性。

Gitlab 数据收集和分析

在对 Gitlab 数据采集的时候我们首先需要明确我们需要关注的主体,已经应该提前注意的事项。 首先,我们在做研发性能观测之前应该先对我们 gitlab 的用户名和邮箱做一个明确的规范,来规避一些用户自己随意填写的用户名和邮箱的问题影响统计的精确性;其次,我们应该做到层级采集 group -> project -> commit 首先应该先采集用户组、再根据每一个用户组采集其对应的项目组、再根据每一个项目采集对应项目的 commit 信息;最后,我们还要注意的是就是在我们 gitlab 中包含非常多的项目和用户的时候要尽量做到增量采集来避免发生链接报错的情况发生。下面是一个采集示例来为大家提供 gitlab 数据收集的思路:

import requests
from datetime import datetime, timedelta
​
from guance_integration__data_writer import DataWriter
​
​
# gitlab 地址
gitlab_url = "https://gitlab.xxxx.com"
gitlab_url = gitlab_url + "/api/v4"
​
private_token = "-tqguyk7xrk-FeE1Ubuz"
headers = {"PRIVATE-TOKEN": private_token}
​
# 增量 15 分钟, 获取 15 分钟前的时间
time_15_minutes_ago = (datetime.utcnow() - timedelta(minutes=15)).strftime('%Y-%m-%dT%H:%M:%SZ')
​
​
# 获取所有组 ID 和名称
def get_all_group_ids():
   group_ids = {}
   params = {"per_page"100"page"1}
​
   while True:
       response = requests.get(f"{gitlab_url}/groups", headers=headers, params=params)
       if response.status_code != 200:
           break
​
       groups = response.json()
       if not groups:
           break
​
       for group in groups:
           group_ids[group["id"]] = group["name"]
​
       params["page"] += 1
       response.close()
   # print(f"DEBUG group_ids: {group_ids}")
   return group_ids
​
​
# 获取项目 ID 组
def get_project_ids(group_id):
   project_ids = []
   params = {"per_page"100"page"1}
​
   while True:
       response = requests.get(f"{gitlab_url}/groups/{group_id}/projects", headers=headers, params=params)
       if response.status_code != 200:
           break
​
       projects = response.json()
       if not projects:
           break
​
       for project in projects:
           project_ids.append(project["id"])
​
       params["page"] += 1
       response.close()
   # print(f"DEBUG project_ids: {project_ids}")
   return project_ids
​
​
# 获取项目的 Name
def get_project_name(project_id):
   response = requests.get(f"{gitlab_url}/projects/{project_id}", headers=headers)
​
   if response.status_code == 200:
       project = response.json()
       response.close()
       return project["name"]
   else:
       print(f"Error: {response.status_code}")
       return None
​
​
# 获取项目的 Name
def get_group_name(group_id):
   response = requests.get(f"{gitlab_url}/groups/{group_id}", headers=headers)
​
   if response.status_code == 200:
       group = response.json()
       response.close()
       return group["name"]
   else:
       print(f"Error: {response.status_code}")
       return None
​
​
# 获取项目的 commit 列表
def get_project_commits(project_id):
   commits = []
   params = {"per_page"100"page"1}
​
   while True:
       response = requests.get(f"{gitlab_url}/projects/{project_id}/repository/commits?"
                               f"since={time_15_minutes_ago}&private_token={private_token}", params=params)
       if response.status_code != 200:
           break
​
       commit_list = response.json()
       if not commit_list:
           break
​
       commits.extend(commit_list)
       params["page"] += 1
       response.close()
   print(f"DEBUG project_commits: 本次共有 {len(commits)} 条 commit 记录")
   return commits
​
​
# 获取 commit 的详细信息
def get_commit_details(commit_sha, project_id):
   response = requests.get(f"{gitlab_url}/projects/{project_id}/repository/commits/{commit_sha}", headers=headers)
   if response.status_code == 200:
       return response.json()
   else:
       return None
​
​
# 获取 issues 信息
def get_project_issues(project_id):
   issues = []
   params = {"per_page"100"page"1'private_token': private_token, 'created_after': time_15_minutes_ago}
​
   while True:
       response = requests.get(f"{gitlab_url}/projects/{project_id}/issues?", params=params)
       if response.status_code != 200:
           break
​
       issue_list = response.json()
       if not issue_list:
           break
​
       issues.extend(issue_list)
       params["page"] += 1
       response.close()
   print(f"DEBUG project_issues: 本次共有 {len(issues)} 条 issue 记录")
   return issues
​
​
# 获取 merges 信息
def get_project_merges(project_id):
   merges = []
   params = {"per_page"100"page"1'private_token': private_token, 'created_after': time_15_minutes_ago}
​
   while True:
       response = requests.get(f"{gitlab_url}/projects/{project_id}/merge_requests", params=params)
       if response.status_code != 200:
           break
​
       merge_list = response.json()
       if not merge_list:
           break
​
       merges.extend(merge_list)
       params["page"] += 1
       response.close()
   print(f"DEBUG project_merges: 本次共有 {len(merges)} 条 merge 记录")
   return merges
​
​
@DFF.API('gitlab 数据采集-增量', timeout=900)
def main():
   group_ids = get_all_group_ids()
   try:
       data_writer = DataWriter(guance_id="Guance")
   except Exception as e:
       print(">>>>>>>>>>>>> 提示信息:连接器配置异常!请修改连接器配置 <<<<<<<<<<<<<<<<")
       print(e)
       return
   if len(group_ids) > 0:
       for group_id in group_ids:
           group_name = get_group_name(group_id)
           project_ids = get_project_ids(group_id)
           if len(project_ids) > 0:
               for project_id in project_ids:
                   project_name = get_project_name(project_id)
                   project_commits = get_project_commits(project_id)
                   if len(project_commits) > 0:
                       category = "logging"
                       data = []
                       for project_commit in project_commits:
                           commit_sha = project_commit["id"]
                           commit_details = get_commit_details(commit_sha, project_id)
                           if commit_details:
                               category = "logging"
                               body = {
                                   "measurement""gitlab_commits",
                                   "tags"       : {
                                       "group_name"     : group_name,
                                       "project_name"   : project_name,
                                       "committer_name" : commit_details["committer_name"],
                                       "committer_email": commit_details["committer_email"]
                                  },
                                   "fields"     : {
                                       "commit sha"   : commit_sha,
                                       "web_url"       : commit_details["web_url"].strip(),
                                       "message"       : commit_details["message"].strip(),
                                       "Added lines"   : commit_details["stats"]["additions"],
                                       "Deleted lines" : commit_details["stats"]["deletions"],
                                       "Total lines"   : commit_details["stats"]["total"],
                                       "committed_date": commit_details["committed_date"]
                                  },
                                   "timestamp" : int(datetime.strptime(commit_details["created_at"],
                                                                        "%Y-%m-%dT%H:%M:%S.%f%z").timestamp())
                              }
                               data.append(body)
                       data_writer.write_by_category(category, data)
​
                   # 获取所有 issues
                   project_issues = get_project_issues(project_id)
                   if len(project_issues) > 0:
                       category = "logging"
                       data = []
                       for project_issue in project_issues:
                           if project_issue:
                               body = {
                                   "measurement""gitlab_issues",
                                   "tags"       : {
                                       "group_name" : group_name,
                                       "project_name": project_name,
                                       "author_name" : project_issue["author"]["name"]
                                  },
                                   "fields"     : {
                                       "title"               : project_issue["title"].strip(),
                                       "description"         : project_issue["description"].strip(),
                                       "state"               : project_issue["state"],
                                       "updated_at"         : project_issue["updated_at"],
                                       "closed_at"           : project_issue["closed_at"],
                                       "web_url"             : project_issue["web_url"],
                                       "type"               : project_issue["type"],
                                       "due_date"           : project_issue["due_date"],
                                       "issue_type"         : project_issue["issue_type"],
                                       "user_notes_count"   : project_issue["user_notes_count"],
                                       "merge_requests_count": project_issue["merge_requests_count"],
                                       "upvotes"             : project_issue["upvotes"],
                                       "downvotes"           : project_issue["downvotes"]
                                  },
                                   "timestamp" : int(datetime.strptime(project_issue["created_at"],
                                                                        '%Y-%m-%dT%H:%M:%S.%f%z').timestamp())
                              }
                               data.append(body)
                       data_writer.write_by_category(category, data)
​
                   project_merges = get_project_merges(project_id)
                   if len(project_merges) > 0:
                       data = []
                       category = "logging"
                       for project_merge in project_merges:
                           body = {
                               "measurement""gitlab_merges",
                               "tags"       : {
                                   "group_name" : group_name,
                                   "project_name": project_name,
                                   "author_name" : project_merge["author"]["name"]
                              },
                               "fields": {
                                   "title"           : project_merge["title"].strip(),
                                   "description"     : project_merge["description"].strip(),
                                   "state"           : project_merge["state"],
                                   "updated_at"     : project_merge["updated_at"],
                                   "closed_at"       : project_merge["closed_at"],
                                   "merged_at"       : project_merge["merged_at"],
                                   "web_url"         : project_merge["web_url"],
                                   "merge_commit_sha": project_merge["merge_commit_sha"],
                                   "merge_status"   : project_merge["merge_status"],
                                   "upvotes"         : project_merge["upvotes"],
                                   "downvotes"       : project_merge["downvotes"]
                              },
                               "timestamp"int(datetime.strptime(project_merge["created_at"],
                                                                    '%Y-%m-%dT%H:%M:%S.%f%z').timestamp())
                          }
                           data.append(body)
                       data_writer.write_by_category(category, data)
​

上述示例代码展示了一个基本的 GitLab 数据收集的实现过程。它使用GitLab Python API库进行数据收集,我们也可以根据实际需求和要分析的指标进行进一步的开发和定制。通过使用GitLab API提供的各种功能和数据,结合适当的数据分析技术和工具,可以根据团队和项目的需求进行更深入的数据收集、分析和可视化。

研发效能最佳实践

  • 项目整体情况:通过查看不同分组不同项目来查看,当前分组和项目的基本情况,从代码提交人数、提交代码次数和新增代码行数及人均代码行数来关注项目的整体进展,从当前的 issue 和 merge 的情况来查看当前项目的问题情况
  • 代码提交情况:我们可以通过关注人力均衡度来查看当前项目中团队成员之间工作负荷的平衡程度,确保每个团队成员的工作量相对均衡,避免出现过度负荷或闲置的情况,也可以从代码提交次数及行数排名来查看当前的人员工作积极性,还可以通过人均当量趋势来查看整体项目的进展情况,当我们发现问题的时候可以从下方的代码提交记录中及时排查到人
  • Issue 情况:我们可以从 Issue 的状态分类来查看项目整体的 bug 及需求管理情况,也可以通过提交 Issue 的排行情况来查看最关系项目的用户,当我们发现问题的时候我们可以及时从 Issue 详情中快速查看
  • merge 情况:我们可以从 merge 的状态分类来查看当前项目的整体状态,也可以通过提交 merge 请求的排行来查看当前项目 merge 的情况,当我们发现问题的时候也可以及时的从 merge 详情中快速查看

首先,我们可以根据人力均衡度来关注团队成员之间工作负荷的平衡程度,确保每个团队成员的工作量相对均衡,避免出现过度负荷或闲置的情况。人力均衡度的目标是优化团队的生产力和效率。如果某些成员过度负荷,可能会导致工作质量下降、疲劳和项目延误。而在某些成员闲置的情况下,可能浪费了资源和潜在能力。通过评估人力均衡度,可以调整工作分配和资源管理,确保团队的工作负荷均衡,提高整体效能。

其次,我们可以根据人均当量趋势来查看项目的人均工作强度和工作进展来确保我们的项目进度正常推进

最后,我们可以根据提交代码量和提交代码频率来关注团队成员之间的工作积极性,避免在某些成员闲置的情况下,可能浪费了资源和潜在能力。我们可以通过评估 commit 的情况来确保团队的工作负荷均衡,达到提高整体的能效的目的。

如何进一步提升企业研发效能

提升企业研发效能是一个持续的过程,我们可以综合考虑组织文化、流程改进、团队协作和技术工具等方面。比如以下几点意见供我们参考

  • 我们可以设定更加明确的目标和指标来确保为研发团队设定明确的目标,并建立可衡量的指标来跟踪和评估团队的绩效。这样可以为团队提供明确的方向,激励他们努力追求高效率和高质量的工作。
  • 尽量采用敏捷开发方法,敏捷开发方法可以帮助团队更好地应对变化,提高开发速度和交付质量。采用敏捷方法,如Scrum或Kanban,可以促进迭代开发、自组织团队和持续改进的文化。
  • 使用自动化和持续集成/持续交付(CI/CD),利用自动化工具和流程来提高开发、测试和部署的效率。实施持续集成和持续交付实践,确保代码的快速集成、自动化测试和频繁部署。
  • 尽量培养团队技能和知识分享,投资于团队的技能培养和学习机会,确保他们具备所需的技术和领域知识。鼓励团队成员之间的知识分享和合作,促进团队的整体提升。
  • 通过优化沟通和协作来建立良好的沟通渠道和协作机制,使团队成员能够有效地交流、合作和共享信息。采用适合团队的协作工具,如团队聊天应用、项目管理工具和文档分享平台,提高团队的协同效能。
  • 进一步追踪和评估研发过程,通过定期审查和评估研发过程,识别潜在的瓶颈和改进机会。使用可观测性工具和指标,收集和分析关键数据,以便及时调整和优化研发流程。