Skip to content

Commit e7df8dd

Browse files
authored
Merge branch 'hhyo:master' into wf_error_handle
2 parents 952d8c9 + 69ddfd0 commit e7df8dd

File tree

4 files changed

+188
-8
lines changed

4 files changed

+188
-8
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const pgsqlDiagnosticInfo = {
2+
fieldsProcesslist: [
3+
'pgsql',
4+
["All", "Not Idle"],
5+
[
6+
{ title: '', field: 'checkbox', checkbox: true },
7+
{ title: 'PId', field: 'pid', sortable: true },
8+
{ title: '阻塞PID', field: 'block_pids', sortable: false },
9+
{ title: '数据库', field: 'datname', sortable: true },
10+
{ title: '用户', field: 'usename', sortable: true },
11+
{ title: '应用名称', field: 'application_name', sortable: true },
12+
{ title: '状态', field: 'state', sortable: true },
13+
{ title: '客户端地址', field: 'client_addr', sortable: true },
14+
{ title: '耗时(秒)', field: 'elapsed_time_seconds', sortable: true },
15+
{ title: '耗时', field: 'elapsed_time', sortable: true },
16+
{ title: '查询语句', field: 'query', sortable: true },
17+
{ title: '等待事件类型', field: 'wait_event_type', sortable: true },
18+
{ title: '等待事件', field: 'wait_event', sortable: true },
19+
{ title: '查询开始时间', field: 'query_start', sortable: true },
20+
{ title: '后端开始时间', field: 'backend_start', sortable: true },
21+
{ title: '父PID', field: 'leader_pid', sortable: true },
22+
{ title: '客户端主机名', field: 'client_hostname', sortable: true },
23+
{ title: '客户端端口', field: 'client_port', sortable: true },
24+
{ title: '事务开始时间', field: 'transaction_start_time', sortable: true },
25+
{ title: '状态变更时间', field: 'state_change', sortable: true },
26+
{ title: '后端XID', field: 'backend_xid', sortable: true },
27+
{ title: '后端XMIN', field: 'backend_xmin', sortable: true },
28+
{ title: '后端类型', field: 'backend_type', sortable: true },
29+
]
30+
]
31+
}

sql/engines/pgsql.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@file: pgsql.py
66
@time: 2019/03/29
77
"""
8+
import json
89
import re
910
import psycopg2
1011
import logging
@@ -197,16 +198,38 @@ def query(
197198
f"SET search_path TO %(schema_name)s;", {"schema_name": schema_name}
198199
)
199200
cursor.execute(sql, parameters)
200-
effect_row = cursor.rowcount
201+
# effect_row = cursor.rowcount
201202
if int(limit_num) > 0:
202203
rows = cursor.fetchmany(size=int(limit_num))
203204
else:
204205
rows = cursor.fetchall()
205206
fields = cursor.description
207+
column_type_codes = [i[1] for i in fields] if fields else []
208+
# 定义 JSON 和 JSONB 的 type_code,# 114 是 json,3802 是 jsonb
209+
JSON_TYPE_CODE = 114
210+
JSONB_TYPE_CODE = 3802
211+
# 对 rows 进行循环处理,判断是否是 jsonb 或 json 类型
212+
converted_rows = []
213+
for row in rows:
214+
new_row = []
215+
for idx, col_value in enumerate(row):
216+
# 理论上, 下标不会越界的
217+
column_type_code = (
218+
column_type_codes[idx] if idx < len(column_type_codes) else None
219+
)
220+
# 只在列类型为 json 或 jsonb 时转换
221+
if column_type_code in [JSON_TYPE_CODE, JSONB_TYPE_CODE]:
222+
if isinstance(col_value, (dict, list)):
223+
new_row.append(json.dumps(col_value)) # 转为 JSON 字符串
224+
else:
225+
new_row.append(col_value)
226+
else:
227+
new_row.append(col_value)
228+
converted_rows.append(tuple(new_row))
206229

207230
result_set.column_list = [i[0] for i in fields] if fields else []
208-
result_set.rows = rows
209-
result_set.affected_rows = effect_row
231+
result_set.rows = converted_rows
232+
result_set.affected_rows = len(converted_rows)
210233
except Exception as e:
211234
logger.warning(
212235
f"PgSQL命令执行报错,语句:{sql}, 错误信息:{traceback.format_exc()}"
@@ -361,3 +384,45 @@ def close(self):
361384
if self.conn:
362385
self.conn.close()
363386
self.conn = None
387+
388+
def processlist(self, command_type, **kwargs):
389+
"""获取连接信息"""
390+
sql = """
391+
select psa.pid
392+
,concat('{',array_to_string(pg_blocking_pids(psa.pid),','),'}') block_pids
393+
,psa.leader_pid
394+
,psa.datname,psa.usename
395+
,psa.application_name
396+
,psa.state
397+
,psa.client_addr::text client_addr
398+
,round(GREATEST(EXTRACT(EPOCH FROM (now() - psa.query_start)),0)::numeric,4) elapsed_time_seconds
399+
,GREATEST(now() - psa.query_start, INTERVAL '0 second') AS elapsed_time
400+
,(case when psa.leader_pid is null then psa.query end) query
401+
,psa.wait_event_type,psa.wait_event
402+
,psa.query_start
403+
,psa.backend_start
404+
,psa.client_hostname,psa.client_port
405+
,psa.xact_start transaction_start_time
406+
,psa.state_change,psa.backend_xid,psa.backend_xmin,psa.backend_type
407+
from pg_stat_activity psa
408+
where 1=1
409+
AND psa.pid <> pg_backend_pid()
410+
$state_not_idle$
411+
order by (case
412+
when psa.state='active' then 10
413+
when psa.state like 'idle in transaction%' then 5
414+
when psa.state='idle' then 99 else 100 end)
415+
,elapsed_time_seconds desc
416+
,(case when psa.leader_pid is not null then 1 else 0 end);
417+
"""
418+
# escape
419+
command_type = self.escape_string(command_type)
420+
if not command_type:
421+
command_type = "Not Idle"
422+
423+
if command_type == "Not Idle":
424+
sql = sql.replace("$state_not_idle$", "and psa.state<>'idle'")
425+
426+
# 所有的模板进行替换
427+
sql = sql.replace("$state_not_idle$", "")
428+
return self.query("postgres", sql)

sql/engines/tests.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from datetime import timedelta, datetime
3-
from unittest.mock import patch, Mock, ANY
3+
from unittest.mock import MagicMock, patch, Mock, ANY
44

55
import sqlparse
66
from django.contrib.auth import get_user_model
@@ -576,16 +576,46 @@ def test_query(self, _conn, _cursor, _execute):
576576
@patch("psycopg2.connect.cursor")
577577
@patch("psycopg2.connect")
578578
def test_query_not_limit(self, _conn, _cursor, _execute):
579-
_conn.return_value.cursor.return_value.fetchall.return_value = [(1,)]
579+
# 模拟数据库连接和游标
580+
mock_cursor = MagicMock()
581+
_conn.return_value.cursor.return_value = mock_cursor
582+
583+
# 模拟 SQL 查询的返回结果,包含 JSONB 类型、字符串和数字数据
584+
mock_cursor.fetchall.return_value = [
585+
({"key": "value"}, "test_string", 123) # 返回一行数据,三列
586+
]
587+
mock_cursor.description = [
588+
("json_column", 3802), # JSONB 类型
589+
("string_column", 25), # 25 表示 TEXT 类型的 OID
590+
("number_column", 23), # 23 表示 INTEGER 类型的 OID
591+
]
592+
593+
# _conn.return_value.cursor.return_value.fetchall.return_value = [(1,)]
580594
new_engine = PgSQLEngine(instance=self.ins)
581595
query_result = new_engine.query(
582596
db_name="some_dbname",
583-
sql="select 1",
597+
sql="SELECT json_column, string_column, number_column FROM some_table",
584598
limit_num=0,
585599
schema_name="some_schema",
586600
)
601+
602+
# 断言查询结果的类型和数据
587603
self.assertIsInstance(query_result, ResultSet)
588-
self.assertListEqual(query_result.rows, [(1,)])
604+
# 验证返回的 JSONB 列已转换为 JSON 字符串
605+
expected_row = ('{"key": "value"}', "test_string", 123)
606+
self.assertListEqual(query_result.rows, [expected_row])
607+
608+
expected_column = ["json_column", "string_column", "number_column"]
609+
# 验证列名是否正确
610+
self.assertEqual(query_result.column_list, expected_column)
611+
612+
# 验证受影响的行数
613+
self.assertEqual(query_result.affected_rows, 1)
614+
615+
# 验证类型代码是否正确(3802 表示 JSONB,25 表示 TEXT,23 表示 INTEGER)
616+
expected_column_type_codes = [3802, 25, 23]
617+
actual_column_type_codes = [desc[1] for desc in mock_cursor.description]
618+
self.assertListEqual(actual_column_type_codes, expected_column_type_codes)
589619

590620
@patch(
591621
"sql.engines.pgsql.PgSQLEngine.query",
@@ -826,6 +856,40 @@ def test_execute_workflow_exception(self, _conn, _cursor, _execute):
826856
execute_result.rows[0].__dict__.keys(), row.__dict__.keys()
827857
)
828858

859+
@patch("psycopg2.connect")
860+
def test_processlist_not_idle(self, mock_connect):
861+
# 模拟数据库连接和游标
862+
mock_cursor = MagicMock()
863+
mock_connect.return_value.cursor.return_value = mock_cursor
864+
865+
# 假设 query 方法返回的结果
866+
mock_cursor.fetchall.return_value = [
867+
(123, "test_db", "user", "app_name", "active")
868+
]
869+
870+
# 创建 PgSQLEngine 实例
871+
new_engine = PgSQLEngine(instance=self.ins)
872+
873+
# 调用 processlist 方法
874+
result = new_engine.processlist(command_type="Not Idle")
875+
self.assertEqual(result.rows, mock_cursor.fetchall.return_value)
876+
877+
@patch("psycopg2.connect")
878+
def test_processlist_idle(self, mock_connect):
879+
# 模拟数据库连接和游标
880+
mock_cursor = MagicMock()
881+
mock_connect.return_value.cursor.return_value = mock_cursor
882+
883+
# 假设 query 方法返回的结果
884+
mock_cursor.fetchall.return_value = [
885+
(123, "test_db", "user", "app_name", "idle")
886+
]
887+
# 创建 PgSQLEngine 实例
888+
new_engine = PgSQLEngine(instance=self.ins)
889+
# 调用 processlist 方法
890+
result = new_engine.processlist(command_type="Idle")
891+
self.assertEqual(result.rows, mock_cursor.fetchall.return_value)
892+
829893

830894
class TestModel(TestCase):
831895
def setUp(self):

sql/templates/dbdiagnostic.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<optgroup id="optgroup-mongo" label="MongoDB"></optgroup>
2929
<optgroup id="optgroup-oracle" label="Oracle"></optgroup>
3030
<optgroup id="optgroup-redis" label="Redis"></optgroup>
31+
<optgroup id="optgroup-pgsql" label="PgSQL"></optgroup>
3132
</select>
3233
</div>
3334
<div id="command-div" class="form-group">
@@ -94,6 +95,7 @@ <h4 class="modal-title text-danger">确定要终止所选会话吗?</h4>
9495
{% load static %}
9596
<script src="{% static 'bootstrap-table/js/bootstrap-table-export.min.js' %}"></script>
9697
<script src="{% static 'bootstrap-table/js/tableExport.min.js' %}"></script>
98+
<script src="{% static 'dbdiagnostic/js/db_info.js' %}"></script>
9799
<script>
98100

99101
var processListColumns = [];
@@ -453,6 +455,24 @@ <h4 class="modal-title text-danger">确定要终止所选会话吗?</h4>
453455
]
454456

455457

458+
if (typeof pgsqlDiagnosticInfo !== "undefined" && Array.isArray(pgsqlDiagnosticInfo.fieldsProcesslist)) {
459+
processListTableInfos.push(pgsqlDiagnosticInfo?.fieldsProcesslist);
460+
}
461+
if (typeof mysqlDiagnosticInfo !== "undefined" && Array.isArray(mysqlDiagnosticInfo.fieldsProcesslist)) {
462+
processListTableInfos.push(mysqlDiagnosticInfo?.fieldsProcesslist);
463+
}
464+
if (typeof mongoDiagnosticInfo !== "undefined" && Array.isArray(mongoDiagnosticInfo.fieldsProcesslist)) {
465+
processListTableInfos.push(mongoDiagnosticInfo?.fieldsProcesslist);
466+
}
467+
if (typeof redisDiagnosticInfo !== "undefined" && Array.isArray(redisDiagnosticInfo.fieldsProcesslist)) {
468+
processListTableInfos.push(redisDiagnosticInfo?.fieldsProcesslist);
469+
}
470+
if (typeof oracleDiagnosticInfo !== "undefined" && Array.isArray(oracleDiagnosticInfo.fieldsProcesslist)) {
471+
processListTableInfos.push(oracleDiagnosticInfo?.fieldsProcesslist);
472+
}
473+
474+
475+
456476
// 问题诊断--进程列表
457477
function get_process_list() {
458478
$("#command-div").show();
@@ -1056,7 +1076,7 @@ <h4 class="modal-title text-danger">确定要终止所选会话吗?</h4>
10561076
//获取用户实例列表
10571077
$(function () {
10581078
// 会话管理-支持的数据库类型
1059-
supportedDbType=['mysql','mongo', 'oracle','redis']
1079+
supportedDbType=['mysql','mongo', 'oracle','redis','pgsql']
10601080
$.ajax({
10611081
type: "get",
10621082
url: "/group/user_all_instances/",

0 commit comments

Comments
 (0)