1010
1111import sys
1212import time
13+ from datetime import datetime , timezone
1314from platform import node
1415
1516import psycopg
17+ from psycopg import sql
1618
1719from glances .exports .export import GlancesExport
1820from glances .logger import logger
@@ -77,20 +79,13 @@ def init(self):
7779 return db
7880
7981 def normalize (self , value ):
80- """Normalize the value to be exportable to TimescaleDB."""
81- if value is None :
82- return 'NULL'
83- if isinstance (value , bool ):
84- return str (value ).upper ()
82+ """Normalize the value for use in a parameterized psycopg query (returns raw Python value)."""
8583 if isinstance (value , (list , tuple )):
8684 # Special case for list of one boolean
8785 if len (value ) == 1 and isinstance (value [0 ], bool ):
88- return str (value [0 ]).upper ()
89- return ', ' .join ([f"'{ v } '" for v in value ])
90- if isinstance (value , str ):
91- return f"'{ value } '"
92-
93- return f"{ value } "
86+ return value [0 ]
87+ return ', ' .join (str (v ) for v in value )
88+ return value # None → NULL, bool/str/int/float handled natively by psycopg
9489
9590 def update (self , stats ):
9691 """Update the TimescaleDB export module."""
@@ -137,8 +132,8 @@ def update(self, stats):
137132 segmented_by .extend (['hostname_id' ]) # Segment by hostname
138133 for key , value in plugin_stats .items ():
139134 creation_list .append (f"{ key } { convert_types [type (value ).__name__ ]} NULL" )
140- values_list .append ('NOW()' ) # Add the current time (insertion time)
141- values_list .append (f"' { self .hostname } '" ) # Add the hostname
135+ values_list .append (datetime . now ( timezone . utc ) ) # Add the current time (insertion time)
136+ values_list .append (self .hostname ) # Add the hostname
142137 values_list .extend ([self .normalize (value ) for value in plugin_stats .values ()])
143138 values_list = [values_list ]
144139 elif isinstance (plugin_stats , list ) and len (plugin_stats ) > 0 and 'key' in plugin_stats [0 ]:
@@ -153,9 +148,9 @@ def update(self, stats):
153148 # Create the values list (it is a list of list to have a single datamodel for all the plugins)
154149 for plugin_item in plugin_stats :
155150 item_list = []
156- item_list .append ('NOW()' ) # Add the current time (insertion time)
157- item_list .append (f"' { self .hostname } '" ) # Add the hostname
158- item_list .append (f"' { plugin_item .get ('key' )} '" )
151+ item_list .append (datetime . now ( timezone . utc ) ) # Add the current time (insertion time)
152+ item_list .append (self .hostname ) # Add the hostname
153+ item_list .append (plugin_item .get ('key' ))
159154 item_list .extend ([self .normalize (value ) for value in plugin_item .values ()])
160155 values_list .append (item_list [:- 1 ])
161156 else :
@@ -175,34 +170,52 @@ def export(self, plugin, creation_list, segmented_by, values_list):
175170
176171 with self .client .cursor () as cur :
177172 # Is the table exists?
178- cur .execute (f"select exists(select * from information_schema.tables where table_name='{ plugin } ')" )
173+ cur .execute (
174+ "SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name=%s)" ,
175+ [plugin ],
176+ )
179177 if not cur .fetchone ()[0 ]:
180178 # Create the table if it does not exist
181179 # https://github.com/timescale/timescaledb/blob/main/README.md#create-a-hypertable
182- # Execute the create table query
183- create_query = f"""
184- CREATE TABLE { plugin } (
185- { ', ' .join (creation_list )}
186- )
187- WITH (
188- timescaledb.hypertable,
189- timescaledb.partition_column='time',
190- timescaledb.segmentby = '{ ", " .join (segmented_by )} '
191- );"""
180+ # Build CREATE TABLE using sql.Identifier for column names (prevents injection)
181+ # Each item in creation_list is "colname TYPE [NULL|NOT NULL]"
182+ fields = sql .SQL (', ' ).join (
183+ sql .SQL ("{} {}" ).format (
184+ sql .Identifier (item .split (' ' )[0 ]),
185+ sql .SQL (' ' .join (item .split (' ' )[1 :]))
186+ )
187+ for item in creation_list
188+ )
189+ create_query = sql .SQL (
190+ "CREATE TABLE {table} ({fields}) WITH ("
191+ "timescaledb.hypertable, "
192+ "timescaledb.partition_column='time', "
193+ "timescaledb.segmentby = {segmentby});"
194+ ).format (
195+ table = sql .Identifier (plugin ),
196+ fields = fields ,
197+ segmentby = sql .Literal (', ' .join (segmented_by )),
198+ )
192199 logger .debug (f"Create table: { create_query } " )
193200 try :
194201 cur .execute (create_query )
195202 except Exception as e :
196203 logger .error (f"Cannot create table { plugin } : { e } " )
197204 return
198205
199- # Insert the data
206+ # Insert the data using parameterized queries (prevents injection)
200207 # https://github.com/timescale/timescaledb/blob/main/README.md#insert-and-query-data
201- insert_list = [f"({ ',' .join (i )} )" for i in values_list ]
202- insert_query = f"INSERT INTO { plugin } VALUES { ',' .join (insert_list )} ;"
208+ col_names = [item .split (' ' )[0 ] for item in creation_list ]
209+ cols = sql .SQL (', ' ).join (sql .Identifier (c ) for c in col_names )
210+ placeholders = sql .SQL (', ' ).join (sql .Placeholder () for _ in col_names )
211+ insert_query = sql .SQL ("INSERT INTO {table} ({cols}) VALUES ({vals})" ).format (
212+ table = sql .Identifier (plugin ),
213+ cols = cols ,
214+ vals = placeholders ,
215+ )
203216 logger .debug (f"Insert data into table: { insert_query } " )
204217 try :
205- cur .execute (insert_query )
218+ cur .executemany (insert_query , values_list )
206219 except Exception as e :
207220 logger .error (f"Cannot insert data into table { plugin } : { e } " )
208221 return
0 commit comments