Coverage for frappe_manager / logger / context.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-07-02 18:13 +0530

1""" 

2Context information for logging. 

3 

4This module provides LoggerContext for adding contextual information 

5(bench name, operation, component) to log messages. 

6""" 

7 

8from dataclasses import dataclass, field 

9from typing import Any 

10 

11 

12@dataclass 

13class LoggerContext: 

14 """ 

15 Immutable context information for logging. 

16 

17 Provides structured context (bench, operation, component, correlation_id) that can be 

18 formatted as prefixes in log messages or converted to dictionaries for 

19 structured logging. 

20 

21 Example: 

22 >>> ctx = LoggerContext(bench="mybench", operation="create", correlation_id="550e8400-...") 

23 >>> ctx.format() 

24 '[corr=550e8400] [bench=mybench] [op=create]' 

25 

26 >>> child = ctx.child(component="docker") 

27 >>> child.format() 

28 '[corr=550e8400] [bench=mybench] [op=create] [component=docker]' 

29 """ 

30 

31 bench: str | None = None 

32 operation: str | None = None 

33 component: str | None = None 

34 correlation_id: str | None = None 

35 extra: dict[str, Any] = field(default_factory=dict) 

36 

37 def child(self, **overrides) -> "LoggerContext": 

38 """ 

39 Create a child context with inherited values and overrides. 

40 

41 The child context inherits all values from the parent unless 

42 explicitly overridden. This is useful for propagating context 

43 through nested operations. 

44 

45 Args: 

46 **overrides: Context fields to override (bench, operation, component, correlation_id, extra) 

47 

48 Returns: 

49 New LoggerContext with inherited and overridden values 

50 

51 Example: 

52 >>> parent = LoggerContext(bench="mybench", operation="create", correlation_id="550e8400-...") 

53 >>> child = parent.child(component="docker") 

54 >>> child.bench # Inherited 

55 'mybench' 

56 >>> child.component # Overridden 

57 'docker' 

58 >>> child.correlation_id # Inherited 

59 '550e8400-...' 

60 """ 

61 # Extract extra overrides if provided 

62 extra_overrides = overrides.pop("extra", {}) 

63 

64 return LoggerContext( 

65 bench=overrides.get("bench", self.bench), 

66 operation=overrides.get("operation", self.operation), 

67 component=overrides.get("component", self.component), 

68 correlation_id=overrides.get("correlation_id", self.correlation_id), 

69 extra={**self.extra, **extra_overrides}, 

70 ) 

71 

72 def format(self) -> str: 

73 """ 

74 Format context as a prefix string for log messages. 

75 

76 Formats all non-None context values as [key=value] pairs. 

77 Correlation ID is shortened to first 8 characters for readability. 

78 Returns empty string if no context values are set. 

79 

80 Returns: 

81 Formatted context string (e.g., "[corr=550e8400] [bench=mybench] [op=create]") 

82 

83 Example: 

84 >>> LoggerContext().format() 

85 '' 

86 >>> LoggerContext(correlation_id="550e8400-e29b-41d4-a716-446655440000").format() 

87 '[corr=550e8400]' 

88 >>> LoggerContext(bench="mybench", operation="create", correlation_id="550e8400-...").format() 

89 '[corr=550e8400] [bench=mybench] [op=create]' 

90 """ 

91 parts = [] 

92 

93 # Correlation ID comes first (shortened to 8 chars for readability) 

94 if self.correlation_id: 

95 short_corr = self.correlation_id[:8] if len(self.correlation_id) >= 8 else self.correlation_id 

96 parts.append(f"corr={short_corr}") 

97 

98 if self.bench: 

99 parts.append(f"bench={self.bench}") 

100 if self.operation: 

101 parts.append(f"op={self.operation}") 

102 if self.component: 

103 parts.append(f"component={self.component}") 

104 

105 for key, value in self.extra.items(): 

106 if value is not None: # Skip None values 

107 parts.append(f"{key}={value}") 

108 

109 if parts: 

110 return "[" + "] [".join(parts) + "]" 

111 return "" 

112 

113 def to_dict(self) -> dict[str, Any]: 

114 """ 

115 Convert context to dictionary for structured logging. 

116 

117 Includes all context fields (even if None) plus any extra fields. 

118 Useful for JSON logging or structured log systems. 

119 

120 Returns: 

121 Dictionary with all context values 

122 

123 Example: 

124 >>> ctx = LoggerContext(bench="mybench", operation="create", correlation_id="550e8400-...") 

125 >>> ctx.to_dict() 

126 {'bench': 'mybench', 'operation': 'create', 'component': None, 'correlation_id': '550e8400-...'} 

127 """ 

128 return { 

129 "bench": self.bench, 

130 "operation": self.operation, 

131 "component": self.component, 

132 "correlation_id": self.correlation_id, 

133 **self.extra, 

134 } 

135 

136 def __bool__(self) -> bool: 

137 """ 

138 Check if context has any values set. 

139 

140 Returns True if any field (bench, operation, component, correlation_id, or extra) is non-None. 

141 

142 Returns: 

143 True if context has values, False if empty 

144 

145 Example: 

146 >>> bool(LoggerContext()) 

147 False 

148 >>> bool(LoggerContext(bench="mybench")) 

149 True 

150 >>> bool(LoggerContext(correlation_id="550e8400-...")) 

151 True 

152 """ 

153 return any( 

154 [ 

155 self.bench is not None, 

156 self.operation is not None, 

157 self.component is not None, 

158 self.correlation_id is not None, 

159 bool(self.extra), 

160 ], 

161 )