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
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
1"""
2Context information for logging.
4This module provides LoggerContext for adding contextual information
5(bench name, operation, component) to log messages.
6"""
8from dataclasses import dataclass, field
9from typing import Any
12@dataclass
13class LoggerContext:
14 """
15 Immutable context information for logging.
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.
21 Example:
22 >>> ctx = LoggerContext(bench="mybench", operation="create", correlation_id="550e8400-...")
23 >>> ctx.format()
24 '[corr=550e8400] [bench=mybench] [op=create]'
26 >>> child = ctx.child(component="docker")
27 >>> child.format()
28 '[corr=550e8400] [bench=mybench] [op=create] [component=docker]'
29 """
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)
37 def child(self, **overrides) -> "LoggerContext":
38 """
39 Create a child context with inherited values and overrides.
41 The child context inherits all values from the parent unless
42 explicitly overridden. This is useful for propagating context
43 through nested operations.
45 Args:
46 **overrides: Context fields to override (bench, operation, component, correlation_id, extra)
48 Returns:
49 New LoggerContext with inherited and overridden values
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", {})
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 )
72 def format(self) -> str:
73 """
74 Format context as a prefix string for log messages.
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.
80 Returns:
81 Formatted context string (e.g., "[corr=550e8400] [bench=mybench] [op=create]")
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 = []
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}")
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}")
105 for key, value in self.extra.items():
106 if value is not None: # Skip None values
107 parts.append(f"{key}={value}")
109 if parts:
110 return "[" + "] [".join(parts) + "]"
111 return ""
113 def to_dict(self) -> dict[str, Any]:
114 """
115 Convert context to dictionary for structured logging.
117 Includes all context fields (even if None) plus any extra fields.
118 Useful for JSON logging or structured log systems.
120 Returns:
121 Dictionary with all context values
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 }
136 def __bool__(self) -> bool:
137 """
138 Check if context has any values set.
140 Returns True if any field (bench, operation, component, correlation_id, or extra) is non-None.
142 Returns:
143 True if context has values, False if empty
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 )