Test Failed
Push — develop ( 95620b...6631b3 )
by Nicolas
21:21 queued 18:00
created

glances.exports.glances_duckdb   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 196
Duplicated Lines 18.37 %

Importance

Changes 0
Metric Value
eloc 113
dl 36
loc 196
rs 10
c 0
b 0
f 0
wmc 29

6 Methods

Rating   Name   Duplication   Size   Complexity  
A Export.normalize() 0 5 4
A Export.exit() 0 11 1
A Export.__init__() 25 25 2
B Export.export() 0 36 5
A Export.init() 0 14 4
D Export.update() 11 63 13

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2025 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""DuckDB interface class."""
10
11
import sys
12
import time
13
from datetime import datetime
14
from platform import node
15
16
import duckdb
17
18
from glances.exports.export import GlancesExport
19
from glances.logger import logger
20
21
# Define the type conversions for DuckDB
22
# https://duckdb.org/docs/stable/clients/python/conversion
23
convert_types = {
24
    'bool': 'BOOLEAN',
25
    'int': 'BIGINT',
26
    'float': 'DOUBLE',
27
    'str': 'VARCHAR',
28
    'tuple': 'VARCHAR',  # Store tuples as VARCHAR (comma-separated)
29
    'list': 'VARCHAR',  # Store lists as VARCHAR (comma-separated)
30
    'NoneType': 'VARCHAR',
31
}
32
33
34
class Export(GlancesExport):
35
    """This class manages the DuckDB export module."""
36
37 View Code Duplication
    def __init__(self, config=None, args=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
38
        """Init the DuckDB export IF."""
39
        super().__init__(config=config, args=args)
40
41
        # Mandatory configuration keys (additional to host and port)
42
        self.db = None
43
44
        # Optional configuration keys
45
        self.user = None
46
        self.password = None
47
        self.hostname = None
48
49
        # Load the configuration file
50
        self.export_enable = self.load_conf(
51
            'duckdb', mandatories=['database'], options=['user', 'password', 'hostname']
52
        )
53
        if not self.export_enable:
54
            exit('Missing DuckDB config')
55
56
        # The hostname is always add as an identifier in the DuckDB table
57
        # so we can filter the stats by hostname
58
        self.hostname = self.hostname or node().split(".")[0]
59
60
        # Init the DuckDB client
61
        self.client = self.init()
62
63
    def init(self):
64
        """Init the connection to the DuckDB server."""
65
        if not self.export_enable:
66
            return None
67
68
        try:
69
            db = duckdb.connect(database=self.database)
70
        except Exception as e:
71
            logger.critical(f"Cannot connect to DuckDB {self.database} ({e})")
72
            sys.exit(2)
73
        else:
74
            logger.info(f"Stats will be exported to DuckDB: {self.database}")
75
76
        return db
77
78
    def normalize(self, value):
79
        # Nothing to do...
80
        if isinstance(value, list) and len(value) == 1 and value[0] in ['True', 'False']:
81
            return bool(value[0])
82
        return value
83
84
    def update(self, stats):
85
        """Update the DuckDB export module."""
86
        if not self.export_enable:
87
            return False
88
89
        # Get all the stats & limits
90
        # Current limitation with sensors and fs plugins because fields list is not the same
91
        self._last_exported_list = [p for p in self.plugins_to_export(stats) if p not in ['sensors', 'fs']]
92
        all_stats = stats.getAllExportsAsDict(plugin_list=self.last_exported_list())
93
        all_limits = stats.getAllLimitsAsDict(plugin_list=self.last_exported_list())
94
95
        # Loop over plugins to export
96
        for plugin in self.last_exported_list():
97
            # Remove some fields
98 View Code Duplication
            if isinstance(all_stats[plugin], dict):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
99
                all_stats[plugin].update(all_limits[plugin])
100
                # Remove the <plugin>_disable field
101
                all_stats[plugin].pop(f"{plugin}_disable", None)
102
            elif isinstance(all_stats[plugin], list):
103
                for i in all_stats[plugin]:
104
                    i.update(all_limits[plugin])
105
                    # Remove the <plugin>_disable field
106
                    i.pop(f"{plugin}_disable", None)
107
            else:
108
                continue
109
110
            plugin_stats = all_stats[plugin]
111
            creation_list = []  # List used to create the DuckDB table
112
            values_list = []  # List of values to insert (list of lists, one list per row)
113
            if isinstance(plugin_stats, dict):
114
                # Create the list to create the table
115
                creation_list.append('time TIMETZ')
116
                creation_list.append('hostname_id VARCHAR')
117
                for key, value in plugin_stats.items():
118
                    creation_list.append(f"{key} {convert_types[type(self.normalize(value)).__name__]}")
119
                # Create the list of values to insert
120
                item_list = []
121
                item_list.append(self.normalize(datetime.now().replace(microsecond=0)))
122
                item_list.append(self.normalize(f"{self.hostname}"))
123
                item_list.extend([self.normalize(value) for value in plugin_stats.values()])
124
                values_list = [item_list]
125
            elif isinstance(plugin_stats, list) and len(plugin_stats) > 0 and 'key' in plugin_stats[0]:
126
                # Create the list to create the table
127
                creation_list.append('time TIMETZ')
128
                creation_list.append('hostname_id VARCHAR')
129
                creation_list.append('key_id VARCHAR')
130
                for key, value in plugin_stats[0].items():
131
                    creation_list.append(f"{key} {convert_types[type(self.normalize(value)).__name__]}")
132
                # Create the list of values to insert
133
                for plugin_item in plugin_stats:
134
                    item_list = []
135
                    item_list.append(self.normalize(datetime.now().replace(microsecond=0)))
136
                    item_list.append(self.normalize(f"{self.hostname}"))
137
                    item_list.append(self.normalize(f"{plugin_item.get('key')}"))
138
                    item_list.extend([self.normalize(value) for value in plugin_item.values()])
139
                    values_list.append(item_list)
140
            else:
141
                continue
142
143
            # Export stats to DuckDB
144
            self.export(plugin, creation_list, values_list)
145
146
        return True
147
148
    def export(self, plugin, creation_list, values_list):
149
        """Export the stats to the DuckDB server."""
150
        logger.debug(f"Export {plugin} stats to DuckDB")
151
152
        # Create the table if it does not exist
153
        table_list = [t[0] for t in self.client.sql("SHOW TABLES").fetchall()]
154
        if plugin not in table_list:
155
            # Execute the create table query
156
            create_query = f"""
157
CREATE TABLE {plugin} (
158
{', '.join(creation_list)}
159
);"""
160
            logger.debug(f"Create table: {create_query}")
161
            try:
162
                self.client.execute(create_query)
163
            except Exception as e:
164
                logger.error(f"Cannot create table {plugin}: {e}")
165
                return
166
167
        # Commit the changes
168
        self.client.commit()
169
170
        # Insert values into the table
171
        for values in values_list:
172
            insert_query = f"""
173
INSERT INTO {plugin} VALUES (
174
{', '.join(['?' for _ in values])}
175
);"""
176
            logger.debug(f"Insert values into table {plugin}: {values}")
177
            try:
178
                self.client.execute(insert_query, values)
179
            except Exception as e:
180
                logger.error(f"Cannot insert data into table {plugin}: {e}")
181
182
        # Commit the changes
183
        self.client.commit()
184
185
    def exit(self):
186
        """Close the DuckDB export module."""
187
        # Force last write
188
        self.client.commit()
189
190
        # Close the DuckDB client
191
        time.sleep(3)  # Wait a bit to ensure all data is written
192
        self.client.close()
193
194
        # Call the father method
195
        super().exit()
196