@@ -94,6 +94,7 @@ def __init__(
9494
9595 self .metadata_set = metadata_set if metadata_set is not None else {}
9696 self .timestamp : int | None = None
97+ self .dimension_sets : list [dict [str , str ]] = [] # Store multiple dimension sets
9798
9899 self ._metric_units = [unit .value for unit in MetricUnit ]
99100 self ._metric_unit_valid_options = list (MetricUnit .__members__ )
@@ -256,21 +257,30 @@ def serialize_metric_set(
256257
257258 metric_names_and_values .update ({metric_name : metric_value })
258259
260+ # Build Dimensions array: primary set + additional dimension sets
261+ dimension_arrays : list [list [str ]] = [list (dimensions .keys ())]
262+ all_dimensions : dict [str , str ] = dict (dimensions )
263+
264+ # Add each additional dimension set
265+ for dim_set in self .dimension_sets :
266+ all_dimensions .update (dim_set )
267+ dimension_arrays .append (list (dim_set .keys ()))
268+
259269 return {
260270 "_aws" : {
261271 "Timestamp" : self .timestamp or int (datetime .datetime .now ().timestamp () * 1000 ), # epoch
262272 "CloudWatchMetrics" : [
263273 {
264274 "Namespace" : self .namespace , # "test_namespace"
265- "Dimensions" : [ list ( dimensions . keys ())] , # [ "service" ]
275+ "Dimensions" : dimension_arrays , # [[ "service"], ["env", "region"] ]
266276 "Metrics" : metric_definition ,
267277 },
268278 ],
269279 },
270280 # NOTE: Mypy doesn't recognize splats '** syntax' in TypedDict
271- ** dimensions , # "service": "test_service"
272- ** metadata , # type: ignore[typeddict-item] # "username": "test"
273- ** metric_names_and_values , # "single_metric": 1.0
281+ ** all_dimensions , # type: ignore[typeddict-item] # All dimension key-value pairs
282+ ** metadata , # type: ignore[typeddict-item]
283+ ** metric_names_and_values ,
274284 }
275285
276286 def add_dimension (self , name : str , value : str ) -> None :
@@ -316,6 +326,70 @@ def add_dimension(self, name: str, value: str) -> None:
316326
317327 self .dimension_set [name ] = value
318328
329+ def add_dimensions (self , ** dimensions : str ) -> None :
330+ """Add a new set of dimensions creating an additional dimension array.
331+
332+ Creates a new dimension set in the CloudWatch EMF Dimensions array.
333+
334+ Example
335+ -------
336+ **Add multiple dimension sets**
337+
338+ metrics.add_dimensions(environment="prod", region="us-east-1")
339+
340+ Parameters
341+ ----------
342+ dimensions : str
343+ Dimension key-value pairs as keyword arguments
344+ """
345+ logger .debug (f"Adding dimension set: { dimensions } " )
346+
347+ if not dimensions :
348+ warnings .warn (
349+ "Empty dimensions dictionary provided" ,
350+ category = PowertoolsUserWarning ,
351+ stacklevel = 2 ,
352+ )
353+ return
354+
355+ sanitized = self ._sanitize_dimensions (dimensions )
356+ if not sanitized :
357+ return
358+
359+ self ._validate_dimension_limit (sanitized )
360+
361+ self .dimension_sets .append ({** self .default_dimensions , ** sanitized })
362+
363+ def _sanitize_dimensions (self , dimensions : dict [str , str ]) -> dict [str , str ]:
364+ """Convert dimension values to strings and filter out empty ones."""
365+ sanitized : dict [str , str ] = {}
366+
367+ for name , value in dimensions .items ():
368+ str_name = str (name )
369+ str_value = str (value )
370+
371+ if not str_name .strip () or not str_value .strip ():
372+ warnings .warn (
373+ f"Dimension { str_name } has empty name or value" ,
374+ category = PowertoolsUserWarning ,
375+ stacklevel = 2 ,
376+ )
377+ continue
378+
379+ sanitized [str_name ] = str_value
380+
381+ return sanitized
382+
383+ def _validate_dimension_limit (self , new_dimensions : dict [str , str ]) -> None :
384+ """Validate that adding new dimensions won't exceed CloudWatch limits."""
385+ all_keys = set (self .dimension_set .keys ())
386+ for ds in self .dimension_sets :
387+ all_keys .update (ds .keys ())
388+ all_keys .update (new_dimensions .keys ())
389+
390+ if len (all_keys ) > MAX_DIMENSIONS :
391+ raise SchemaValidationError (f"Maximum dimensions ({ MAX_DIMENSIONS } ) exceeded" )
392+
319393 def add_metadata (self , key : str , value : Any ) -> None :
320394 """Adds high cardinal metadata for metrics object
321395
@@ -377,6 +451,7 @@ def clear_metrics(self) -> None:
377451 logger .debug ("Clearing out existing metric set from memory" )
378452 self .metric_set .clear ()
379453 self .dimension_set .clear ()
454+ self .dimension_sets .clear ()
380455 self .metadata_set .clear ()
381456 self .set_default_dimensions (** self .default_dimensions )
382457
0 commit comments