1515from collections .abc import Iterable as ABCIterable , Mapping as ABCMapping
1616import enum
1717from inspect import isclass
18- import logging
1918from typing import ClassVar , Iterable , List , Mapping , MutableMapping , Optional , Tuple , Type , Union
2019
2120from pydantic import BaseModel
21+ import structlog # type: ignore
2222
2323from .diff import Diff , DiffElement
2424from .utils import intersection
3232 ObjectNotFound ,
3333)
3434
35- logger = logging . getLogger ( __name__ )
35+ _logger = structlog . get_logger ( )
3636
3737
3838class DSyncFlags (enum .Flag ):
@@ -339,6 +339,7 @@ def __init__(self, name=None):
339339 Subclasses should be careful to call super().__init__() if they override this method.
340340 """
341341 self ._data = defaultdict (dict )
342+ self ._log = _logger .new (dsync = self )
342343
343344 # If the type is not defined, use the name of the class as the default value
344345 if self .type is None :
@@ -359,6 +360,15 @@ def __init_subclass__(cls):
359360 f'Incorrect field name - { value .__name__ } has type name "{ value .get_type ()} ", not "{ name } "'
360361 )
361362
363+ def __str__ (self ):
364+ """String representation of a DSync."""
365+ if self .type != self .name :
366+ return f'{ self .type } "{ self .name } "'
367+ return self .type
368+
369+ def __repr__ (self ):
370+ return f"<{ str (self )} >"
371+
362372 def load (self ):
363373 """Load all desired data from whatever backend data source into this instance."""
364374 # No-op in this generic class
@@ -375,12 +385,13 @@ def sync_from(self, source: "DSync", diff_class: Type[Diff] = Diff, flags: DSync
375385 diff_class (class): Diff or subclass thereof to use to calculate the diffs to use for synchronization
376386 flags (DSyncFlags): Flags influencing the behavior of this sync.
377387 """
388+ log = self ._log .bind (src = source , dst = self , flags = flags ).unbind ("dsync" )
378389 diff = self .diff_from (source , diff_class = diff_class , flags = flags )
379390
380- logger .info ("Beginning sync" )
391+ log .info ("Beginning sync" )
381392 for child in diff .get_children ():
382- self ._sync_from_diff_element (child , flags = flags )
383- logger .info ("Sync complete" )
393+ self ._sync_from_diff_element (child , flags = flags , logger = log )
394+ log .info ("Sync complete" )
384395
385396 def sync_to (self , target : "DSync" , diff_class : Type [Diff ] = Diff , flags : DSyncFlags = DSyncFlags .NONE ):
386397 """Synchronize data from the current DSync object into the given target DSync object.
@@ -393,7 +404,11 @@ def sync_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
393404 target .sync_from (self , diff_class = diff_class , flags = flags )
394405
395406 def _sync_from_diff_element (
396- self , element : DiffElement , flags : DSyncFlags = DSyncFlags .NONE , parent_model : DSyncModel = None ,
407+ self ,
408+ element : DiffElement ,
409+ flags : DSyncFlags = DSyncFlags .NONE ,
410+ logger : structlog .BoundLogger = None ,
411+ parent_model : DSyncModel = None ,
397412 ):
398413 """Synchronize a given DiffElement (and its children, if any) into this DSync.
399414
@@ -402,49 +417,53 @@ def _sync_from_diff_element(
402417 Args:
403418 element: DiffElement to synchronize diffs from
404419 flags (DSyncFlags): Flags influencing the behavior of this sync.
420+ logger: Parent logging context
405421 parent_model: Parent object to update (`add_child`/`remove_child`) if the sync creates/deletes an object.
406422 """
407423 # pylint: disable=too-many-branches
408424 # GFM: I made a few attempts at refactoring this to reduce the branching, but found that it was less readable.
409425 # So let's live with the slightly too high number of branches (14/12) for now.
426+ log = logger or self ._log
410427 object_class = getattr (self , element .type )
411428 obj = self .get (object_class , element .keys )
412- attrs = (
413- {attr_key : element .source_attrs [attr_key ] for attr_key in element .get_attrs_keys ()}
414- if element .source_attrs is not None
415- else {}
429+ # Get the attributes that actually differ between source and dest
430+ diffs = element .get_attrs_diffs ()
431+ log = log .bind (
432+ action = element .action ,
433+ model = object_class .get_type (),
434+ unique_id = object_class .create_unique_id (** element .keys ),
435+ diffs = diffs ,
416436 )
417437
418438 try :
419439 if element .action == "create" :
440+ log .debug ("Attempting object creation" )
420441 if obj :
421442 raise ObjectNotCreated (f"Failed to create { object_class .get_type ()} { element .keys } - it exists!" )
422- logger . info ( "Creating %s %s (%s)" , object_class .get_type (), element .keys , attrs )
423- obj = object_class . create ( dsync = self , ids = element . keys , attrs = attrs )
443+ obj = object_class .create ( dsync = self , ids = element .keys , attrs = { key : diffs [ key ][ "src" ] for key in diffs } )
444+ log . info ( "Created successfully" , status = "success" )
424445 elif element .action == "update" :
446+ log .debug ("Attempting object update" )
425447 if not obj :
426448 raise ObjectNotUpdated (f"Failed to update { object_class .get_type ()} { element .keys } - not found!" )
427- logger . info ( "Updating %s %s with %s" , object_class . get_type (), element . keys , attrs )
428- obj = obj . update ( attrs = attrs )
449+ obj = obj . update ( attrs = { key : diffs [ key ][ "src" ] for key in diffs } )
450+ log . info ( "Updated successfully" , status = "success" )
429451 elif element .action == "delete" :
452+ log .debug ("Attempting object deletion" )
430453 if not obj :
431454 raise ObjectNotDeleted (f"Failed to delete { object_class .get_type ()} { element .keys } - not found!" )
432- logger .info ("Deleting %s %s" , object_class .get_type (), element .keys )
433455 obj = obj .delete ()
456+ log .info ("Deleted successfully" , status = "success" )
434457 except ObjectCrudException as exception :
435- logger .error (
436- "Error during %s of %s %s (%s): %s" ,
437- element .action ,
438- object_class .get_type (),
439- element .keys ,
440- attrs ,
441- exception ,
442- )
458+ log .error (str (exception ), status = "error" )
443459 if not flags & DSyncFlags .CONTINUE_ON_FAILURE :
444460 raise
461+ else :
462+ if obj is None :
463+ log .warning ("Non-fatal failure encountered" , status = "failure" )
445464
446465 if obj is None :
447- logger .warning ("Not syncing children of %s %s" , element . type , element . keys )
466+ log .warning ("Not syncing children" )
448467 return
449468
450469 if element .action == "create" :
@@ -457,7 +476,7 @@ def _sync_from_diff_element(
457476 parent_model .remove_child (obj )
458477
459478 for child in element .get_children ():
460- self ._sync_from_diff_element (child , flags = flags , parent_model = obj )
479+ self ._sync_from_diff_element (child , flags = flags , parent_model = obj , logger = logger )
461480
462481 # ------------------------------------------------------------------------------
463482 # Diff calculation and construction
@@ -471,20 +490,25 @@ def diff_from(self, source: "DSync", diff_class: Type[Diff] = Diff, flags: DSync
471490 diff_class (class): Diff or subclass thereof to use for diff calculation and storage.
472491 flags (DSyncFlags): Flags influencing the behavior of this diff operation.
473492 """
474- logger .info ("Beginning diff" )
493+ log = self ._log .bind (src = source , dst = self , flags = flags ).unbind ("dsync" )
494+ log .info ("Beginning diff" )
475495 diff = diff_class ()
476496
477497 for obj_type in intersection (self .top_level , source .top_level ):
478498
479499 diff_elements = self ._diff_objects (
480- source = source .get_all (obj_type ), dest = self .get_all (obj_type ), source_root = source , flags = flags ,
500+ source = source .get_all (obj_type ),
501+ dest = self .get_all (obj_type ),
502+ source_root = source ,
503+ flags = flags ,
504+ logger = log ,
481505 )
482506
483507 for diff_element in diff_elements :
484508 diff .add (diff_element )
485509
486510 # Notify the diff that it has been fully populated, in case it wishes to print, save to a file, etc.
487- logger .info ("Diff complete" )
511+ log .info ("Diff complete" )
488512 diff .complete ()
489513 return diff
490514
@@ -498,12 +522,13 @@ def diff_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
498522 """
499523 return target .diff_from (self , diff_class = diff_class , flags = flags )
500524
501- def _diff_objects (
525+ def _diff_objects ( # pylint: disable=too-many-arguments
502526 self ,
503527 source : Iterable [DSyncModel ],
504528 dest : Iterable [DSyncModel ],
505529 source_root : "DSync" ,
506530 flags : DSyncFlags = DSyncFlags .NONE ,
531+ logger : structlog .BoundLogger = None ,
507532 ) -> List [DiffElement ]:
508533 """Generate a list of DiffElement between the given lists of objects.
509534
@@ -514,6 +539,7 @@ def _diff_objects(
514539 dest: DSyncModel instances retrieved from this DSync instance
515540 source_root (DSync): The other DSync object being diffed against (owner of the `source` models, if any)
516541 flags (DSyncFlags): Flags influencing the behavior of this diff operation.
542+ logger: Parent logging context
517543
518544 Raises:
519545 TypeError: if the source and dest args are not the same type, or if that type is unsupported
@@ -537,16 +563,16 @@ def _diff_objects(
537563 self ._validate_objects_for_diff (combined_dict )
538564
539565 for uid in combined_dict :
566+ log = logger or self ._log
540567 src_obj , dst_obj = combined_dict [uid ]
541-
542- if flags & DSyncFlags .SKIP_UNMATCHED_SRC and not dst_obj :
543- logger .debug ("Skipping unmatched source object {src_obj}" )
544- continue
545- if flags & DSyncFlags .SKIP_UNMATCHED_DST and not src_obj :
546- logger .debug ("Skipping unmatched dest object {dst_obj}" )
547- continue
548-
568+ if not src_obj and not dst_obj :
569+ # Should never happen
570+ raise RuntimeError (f"UID { uid } is in combined_dict but has neither src_obj nor dst_obj??" )
549571 if src_obj :
572+ log = log .bind (model = src_obj .get_type (), unique_id = src_obj .get_unique_id ())
573+ if flags & DSyncFlags .SKIP_UNMATCHED_SRC and not dst_obj :
574+ log .debug ("Skipping unmatched source object" )
575+ continue
550576 diff_element = DiffElement (
551577 obj_type = src_obj .get_type (),
552578 name = src_obj .get_shortname (),
@@ -555,24 +581,25 @@ def _diff_objects(
555581 dest_name = self .name ,
556582 )
557583 elif dst_obj :
584+ log = log .bind (model = dst_obj .get_type (), unique_id = dst_obj .get_unique_id ())
585+ if flags & DSyncFlags .SKIP_UNMATCHED_DST and not src_obj :
586+ log .debug ("Skipping unmatched dest object" )
587+ continue
558588 diff_element = DiffElement (
559589 obj_type = dst_obj .get_type (),
560590 name = dst_obj .get_shortname (),
561591 keys = dst_obj .get_identifiers (),
562592 source_name = source_root .name ,
563593 dest_name = self .name ,
564594 )
565- else :
566- # Should be unreachable
567- raise RuntimeError (f"UID { uid } is in combined_dict but has neither src_obj nor dst_obj??" )
568595
569596 if src_obj :
570597 diff_element .add_attrs (source = src_obj .get_attrs (), dest = None )
571598 if dst_obj :
572599 diff_element .add_attrs (source = None , dest = dst_obj .get_attrs ())
573600
574601 # Recursively diff the children of src_obj and dst_obj and attach the resulting diffs to the diff_element
575- self ._diff_child_objects (diff_element , src_obj , dst_obj , source_root , flags )
602+ self ._diff_child_objects (diff_element , src_obj , dst_obj , source_root , flags = flags , logger = logger )
576603
577604 diffs .append (diff_element )
578605
@@ -607,6 +634,7 @@ def _diff_child_objects( # pylint: disable=too-many-arguments
607634 dst_obj : Optional [DSyncModel ],
608635 source_root : "DSync" ,
609636 flags : DSyncFlags ,
637+ logger : structlog .BoundLogger ,
610638 ):
611639 """For all children of the given DSyncModel pair, diff them recursively, adding diffs to the given diff_element.
612640
@@ -638,7 +666,7 @@ def _diff_child_objects( # pylint: disable=too-many-arguments
638666 dst_objs = self .get_by_uids (getattr (dst_obj , child_fieldname ), child_type ) if dst_obj else []
639667
640668 for child_diff_element in self ._diff_objects (
641- source = src_objs , dest = dst_objs , source_root = source_root , flags = flags
669+ source = src_objs , dest = dst_objs , source_root = source_root , flags = flags , logger = logger ,
642670 ):
643671 diff_element .add_child (child_diff_element )
644672
0 commit comments