familia 2.0.0.pre17 → 2.0.0.pre18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +60 -0
  3. data/CLAUDE.md +9 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +13 -0
  6. data/bin/irb +1 -1
  7. data/docs/guides/core-field-system.md +48 -26
  8. data/docs/migrating/v2.0.0-pre18.md +58 -0
  9. data/docs/qodo-merge-compliance.md +96 -0
  10. data/lib/familia/base.rb +0 -2
  11. data/lib/familia/connection/middleware.rb +58 -4
  12. data/lib/familia/connection.rb +1 -1
  13. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  14. data/lib/familia/data_type/serialization.rb +5 -5
  15. data/lib/familia/data_type.rb +2 -2
  16. data/lib/familia/encryption/encrypted_data.rb +12 -2
  17. data/lib/familia/encryption/manager.rb +11 -4
  18. data/lib/familia/features/autoloader.rb +3 -1
  19. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  20. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  21. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
  22. data/lib/familia/features/safe_dump.rb +2 -3
  23. data/lib/familia/horreum/database_commands.rb +1 -1
  24. data/lib/familia/horreum/definition.rb +6 -37
  25. data/lib/familia/horreum/management.rb +17 -12
  26. data/lib/familia/horreum/persistence.rb +1 -1
  27. data/lib/familia/horreum/serialization.rb +91 -73
  28. data/lib/familia/horreum.rb +10 -6
  29. data/lib/familia/identifier_extractor.rb +60 -0
  30. data/lib/familia/logging.rb +271 -112
  31. data/lib/familia/refinements.rb +0 -1
  32. data/lib/familia/version.rb +1 -1
  33. data/lib/familia.rb +2 -2
  34. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  35. data/pr_agent.toml +31 -0
  36. data/pr_compliance_checklist.yaml +45 -0
  37. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  38. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  39. data/try/edge_cases/json_serialization_try.rb +2 -2
  40. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  41. data/try/edge_cases/race_conditions_try.rb +1 -1
  42. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  43. data/try/edge_cases/string_coercion_try.rb +1 -1
  44. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  45. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  46. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  47. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  48. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  49. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  50. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  51. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  52. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  53. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  54. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  55. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  56. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  57. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  58. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  59. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  60. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  61. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  62. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  63. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  64. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  65. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  66. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  67. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  68. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  69. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  70. data/try/features/expiration/expiration_try.rb +1 -1
  71. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  72. data/try/features/feature_dependencies_try.rb +1 -1
  73. data/try/features/feature_improvements_try.rb +1 -1
  74. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  75. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  76. data/try/features/quantization/quantization_try.rb +1 -1
  77. data/try/features/real_feature_integration_try.rb +17 -14
  78. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  79. data/try/features/relationships/indexing_try.rb +6 -1
  80. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  81. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  82. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  83. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  84. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  85. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  86. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  87. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  88. data/try/features/relationships/relationships_performance_try.rb +1 -1
  89. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  90. data/try/features/relationships/relationships_try.rb +1 -1
  91. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  92. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  93. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  94. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  95. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  96. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  97. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  98. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  99. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  100. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  101. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  102. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  103. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  104. data/try/{core → integration/connection}/pools_try.rb +1 -1
  105. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  106. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  107. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  108. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  109. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  110. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  111. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  112. data/try/{core → integration}/create_method_try.rb +1 -1
  113. data/try/integration/cross_component_try.rb +1 -1
  114. data/try/{core → integration}/database_consistency_try.rb +11 -8
  115. data/try/{core → integration}/familia_extended_try.rb +1 -1
  116. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  117. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  118. data/try/{models → integration/models}/customer_try.rb +1 -1
  119. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  120. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  121. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  122. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  123. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  124. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  125. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  126. data/try/performance/benchmarks_try.rb +2 -2
  127. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  128. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  129. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  130. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  131. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  132. data/try/{core → unit/core}/connection_try.rb +1 -1
  133. data/try/{core → unit/core}/errors_try.rb +1 -1
  134. data/try/{core → unit/core}/extensions_try.rb +1 -1
  135. data/try/unit/core/familia_logger_try.rb +110 -0
  136. data/try/{core → unit/core}/familia_try.rb +1 -1
  137. data/try/{core → unit/core}/middleware_try.rb +41 -1
  138. data/try/{core → unit/core}/settings_try.rb +1 -1
  139. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  140. data/try/{core → unit/core}/tools_try.rb +1 -1
  141. data/try/{core → unit/core}/utils_try.rb +17 -14
  142. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  143. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  144. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  145. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  146. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  147. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  148. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  149. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  150. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  151. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  152. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
  153. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  154. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  155. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  156. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  157. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  158. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  159. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  160. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  161. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  162. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  163. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  164. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  165. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  166. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  167. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  168. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  169. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  170. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  171. metadata +134 -125
  172. data/lib/familia/distinguisher.rb +0 -85
  173. data/lib/familia/refinements/logger_trace.rb +0 -60
  174. data/try/refinements/logger_trace_methods_try.rb +0 -44
  175. /data/try/{debugging → support/debugging}/README.md +0 -0
  176. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  177. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  178. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  179. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  180. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  181. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  182. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  183. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  184. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  185. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  186. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  187. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  188. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  189. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  190. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  191. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  192. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  193. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  194. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  195. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  196. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  197. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  198. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  199. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  200. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  201. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  202. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  203. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  204. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  205. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  206. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  207. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  208. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  209. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  210. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  211. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  212. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  213. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  214. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  215. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  216. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  217. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  218. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  219. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  220. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -3,151 +3,310 @@
3
3
  require 'pathname'
4
4
  require 'logger'
5
5
 
6
+ # Familia - Logbook
7
+ #
6
8
  module Familia
7
- @logger = Logger.new($stdout)
8
- @logger.progname = name
9
- @logger.formatter = proc do |severity, datetime, _progname, msg|
10
- severity_letter = severity[0] # Get the first letter of the severity
11
- pid = Process.pid
12
- thread_id = Thread.current.object_id
13
- fiber_id = Fiber.current.object_id
14
- full_path, line = caller(5..5).first.split(':')[0..1]
15
- parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
16
- relative_path = full_path.sub(parent_path.to_s, 'familia')
17
- utc_datetime = datetime.utc.strftime('%m-%d %H:%M:%S.%6N')
18
-
19
- # Get the severity letter from the thread local variable or use
20
- # the default. The thread local variable is set in the trace
21
- # method in the Familia::Refinements::LoggerTrace module. The name of the
22
- # variable `severity_letter` is arbitrary and could be anything.
23
- severity_letter = Fiber[:severity_letter] || severity_letter
24
-
25
- "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}/#{fiber_id}: #{msg} [#{relative_path}:#{line}]\n"
9
+ # Custom Logger subclass with TRACE level support.
10
+ #
11
+ # FamiliaLogger extends Ruby's standard Logger with a TRACE level for
12
+ # extremely detailed debugging output. The TRACE level is numerically
13
+ # equal to DEBUG (0) but distinguishes itself via a thread-local marker
14
+ # that the LogFormatter uses to output 'T' instead of 'D'.
15
+ #
16
+ # @example Basic usage
17
+ # logger = Familia::FamiliaLogger.new($stderr)
18
+ # logger.level = Familia::FamiliaLogger::TRACE
19
+ # logger.trace "Detailed trace message"
20
+ # # => T, 10-05 20:43:09.843 pid:123 [456/789]: Detailed trace message
21
+ #
22
+ # @example With progname
23
+ # logger.trace("MyApp") { "Trace with progname" }
24
+ #
25
+ # @see Familia::LogFormatter
26
+ #
27
+ class FamiliaLogger < Logger
28
+ # TRACE severity level (numerically equal to DEBUG=0).
29
+ #
30
+ # Uses the same numeric level as DEBUG but signals via thread-local
31
+ # marker to output 'T' prefix instead of 'D'. This approach works
32
+ # around Logger's limitation with negative severity values.
33
+ #
34
+ # Standard Logger levels: DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4, UNKNOWN=5
35
+ TRACE = 0
36
+
37
+ # Log a TRACE level message.
38
+ #
39
+ # This method behaves like the standard Logger methods (debug, info, etc.)
40
+ # but outputs with a 'T' severity letter when used with LogFormatter.
41
+ #
42
+ # @param progname [String, nil] Program name to include in log output
43
+ # @yield Block that returns the message to log (lazy evaluation)
44
+ # @return [true] Always returns true
45
+ #
46
+ # @example Simple message
47
+ # logger.trace("Entering complex calculation")
48
+ #
49
+ # @example With block for lazy evaluation
50
+ # logger.trace { "Expensive: #{expensive_debug_info}" }
51
+ #
52
+ # @example With progname
53
+ # logger.trace("MyApp") { "Application trace" }
54
+ #
55
+ # @note Sets Fiber[:familia_trace_mode] during execution to
56
+ # signal LogFormatter to output 'T' instead of 'D'
57
+ #
58
+ def trace(progname = nil, &)
59
+ # Store marker in thread-local to signal this is TRACE not DEBUG
60
+ # Track whether we set the flag to avoid clearing it in nested calls
61
+ was_already_tracing = Fiber[:familia_trace_mode]
62
+ Fiber[:familia_trace_mode] = true
63
+ add(TRACE, nil, progname, &)
64
+ ensure
65
+ # Only clear the flag if we set it (not already tracing)
66
+ Fiber[:familia_trace_mode] = false unless was_already_tracing
67
+ end
26
68
  end
27
69
 
28
- # The Logging module provides a set of methods and constants for logging messages
29
- # at various levels of severity. It is designed to be used with the Ruby Logger class
30
- # to facilitate logging in applications.
70
+ # Custom formatter for Familia logger output.
71
+ #
72
+ # LogFormatter produces structured log output with severity letters,
73
+ # timestamps, process/thread/fiber IDs, and the log message.
31
74
  #
32
- # == Constants:
33
- # Logger::TRACE::
34
- # A custom log level for trace messages, typically used for very detailed
35
- # debugging information.
75
+ # Output format:
76
+ # SEVERITY, MM-DD HH:MM:SS.mmm pid:PID [THREAD_ID/FIBER_ID]: MESSAGE
36
77
  #
37
- # == Methods:
38
- # trace::
39
- # Logs a message at the TRACE level. This method is only available if the
40
- # Familia::Refinements::LoggerTrace is used.
78
+ # @example Output
79
+ # I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Connection established
80
+ # T, 10-05 20:43:10.123 pid:12345 [67890/54321]: [LOAD] redis -> user:123
41
81
  #
42
- # debug::
43
- # Logs a message at the DEBUG level. This is used for low-level system information
44
- # for debugging purposes.
82
+ # Severity letters:
83
+ # T = TRACE (when Fiber[:familia_trace_mode] is set, or level 0 when not using FamiliaLogger)
84
+ # D = DEBUG
85
+ # I = INFO
86
+ # W = WARN
87
+ # E = ERROR
88
+ # F = FATAL
89
+ # U = UNKNOWN
45
90
  #
46
- # info::
47
- # Logs a message at the INFO level. This is used for general information about
48
- # system operation.
91
+ # @example Use with FamiliaLogger for TRACE support
92
+ # logger = Familia::FamiliaLogger.new($stderr)
93
+ # logger.formatter = Familia::LogFormatter.new
94
+ # logger.trace("Trace message") # => T, ...
95
+ # logger.debug("Debug message") # => D, ...
49
96
  #
50
- # warn::
51
- # Logs a message at the WARN level. This is used for warning messages, typically
52
- # for non-critical issues that require attention.
97
+ # @example Use with standard Logger (level 0 becomes 'T')
98
+ # logger = Logger.new($stderr)
99
+ # logger.formatter = Familia::LogFormatter.new
100
+ # logger.debug("Debug message") # => T, ... (because DEBUG=0)
53
101
  #
54
- # error::
55
- # Logs a message at the ERROR level. This is used for error messages, typically
56
- # for critical issues that require immediate attention.
102
+ # @note When used with FamiliaLogger, checks Fiber[:familia_trace_mode] to
103
+ # distinguish TRACE from DEBUG. When used with standard Logger, treats
104
+ # level 0 as TRACE since DEBUG and TRACE share the same numeric level.
57
105
  #
58
- # fatal::
59
- # Logs a message at the FATAL level. This is used for very severe error events
60
- # that will presumably lead the application to abort.
106
+ # @see FamiliaLogger#trace
107
+ #
108
+ class LogFormatter < Logger::Formatter
109
+ # Severity string to letter mapping.
110
+ #
111
+ # Maps severity string labels to single-letter codes for compact output.
112
+ # Note: TRACE is handled via Fiber check in #call for FamiliaLogger.
113
+ SEVERITY_LETTERS = {
114
+ 'DEBUG' => 'D',
115
+ 'INFO' => 'I',
116
+ 'WARN' => 'W',
117
+ 'ERROR' => 'E',
118
+ 'FATAL' => 'F',
119
+ 'UNKNOWN' => 'U',
120
+ 'ANY' => 'T' # ANY is Logger's label for severity < 0, treat as TRACE
121
+ }.freeze
122
+
123
+ # Format a log message with severity, timestamp, and context.
124
+ #
125
+ # @param severity [String] Severity label (e.g., "INFO", "DEBUG", "UNKNOWN")
126
+ # @param datetime [Time] Timestamp of the log message
127
+ # @param _progname [String] Program name (unused, kept for Logger compatibility)
128
+ # @param msg [String] The log message
129
+ # @return [String] Formatted log line with newline
130
+ #
131
+ # @example
132
+ # formatter = Familia::LogFormatter.new
133
+ # formatter.call("INFO", Time.now, nil, "Test message")
134
+ # # => "I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Test message\n"
135
+ #
136
+ def call(severity, datetime, _progname, msg)
137
+ # Check if we're in trace mode (TRACE uses same level as DEBUG but marks itself)
138
+ # FamiliaLogger sets Fiber[:familia_trace_mode] when trace() is called
139
+ severity_letter = if Fiber[:familia_trace_mode]
140
+ 'T'
141
+ else
142
+ SEVERITY_LETTERS.fetch(severity, severity[0])
143
+ end
144
+
145
+ utc_datetime = datetime.utc.strftime('%m-%d %H:%M:%S.%3N')
146
+ pid = Process.pid
147
+ thread_id = Thread.current.object_id
148
+ fiber_id = Fiber.current.object_id
149
+
150
+ "#{severity_letter}, #{utc_datetime} pid:#{pid} [#{thread_id}/#{fiber_id}]: #{msg}\n"
151
+ end
152
+ end
153
+
154
+ # The Logging module provides logging capabilities for Familia.
155
+ #
156
+ # Familia uses a custom FamiliaLogger that extends the standard Ruby Logger
157
+ # with a TRACE level for detailed debugging output.
158
+ #
159
+ # == Log Levels (from most to least verbose):
160
+ # - TRACE: Extremely detailed debugging (controlled by FAMILIA_TRACE env var)
161
+ # - DEBUG: Detailed debugging information
162
+ # - INFO: General informational messages
163
+ # - WARN: Warning messages
164
+ # - ERROR: Error messages
165
+ # - FATAL: Fatal errors that cause termination
61
166
  #
62
167
  # == Usage:
63
- # To use the Logging module, you need to include the Familia::Refinements::LoggerTrace module
64
- # and use the `using` keyword to enable the refinement. This will add the TRACE
65
- # log level and the trace method to the Logger class.
66
- #
67
- # Example:
68
- # require 'logger'
69
- #
70
- # module Familia::Refinements::LoggerTrace
71
- # refine Logger do
72
- # TRACE = 0
73
- #
74
- # def trace(progname = nil, &block)
75
- # add(TRACE, nil, progname, &block)
76
- # end
77
- # end
78
- # end
79
- #
80
- # using Familia::Refinements::LoggerTrace
81
- #
82
- # logger = Logger.new(STDOUT)
83
- # logger.trace("This is a trace message")
84
- # logger.debug("This is a debug message")
85
- # logger.info("This is an info message")
86
- # logger.warn("This is a warning message")
87
- # logger.error("This is an error message")
88
- # logger.fatal("This is a fatal message")
89
- #
90
- # In this example, the Familia::Refinements::LoggerTrace module is defined with a refinement
91
- # for the Logger class. The TRACE constant and trace method are added to the Logger
92
- # class within the refinement. The `using` keyword is used to apply the refinement
93
- # in the scope where it's needed.
94
- #
95
- # == Conditions:
96
- # The trace method and TRACE log level are only available if the Familia::Refinements::LoggerTrace
97
- # module is used with the `using` keyword. Without this, the Logger class will not
98
- # have the trace method or the TRACE log level.
99
- #
100
- # == Minimum Ruby Version:
101
- # This module requires Ruby 2.0.0 or later to use refinements.
168
+ # # Use default logger
169
+ # Familia.info "Connection established"
170
+ # Familia.warn "Cache miss"
171
+ #
172
+ # # Set custom logger
173
+ # Familia.logger = Logger.new('familia.log')
174
+ #
175
+ # # Trace-level debugging (requires FAMILIA_TRACE=true)
176
+ # Familia.trace :LOAD, redis_client, "user:123", "from cache"
102
177
  #
103
178
  module Logging
104
- attr_reader :logger
105
-
106
- # Gives our logger the ability to use our trace method.
107
- using Familia::Refinements::LoggerTrace if Familia::Refinements::LoggerTrace::ENABLED
179
+ # Get the logger instance, initializing with defaults if not yet set
180
+ #
181
+ # @return [FamiliaLogger] the logger instance
182
+ #
183
+ # @example Set a custom logger
184
+ # Familia.logger = Logger.new('familia.log')
185
+ #
186
+ # @example Use the default logger
187
+ # Familia.logger.info "Connection established"
188
+ #
189
+ def logger
190
+ @logger ||= FamiliaLogger.new($stderr).tap do |log|
191
+ log.progname = name
192
+ log.formatter = LogFormatter.new
193
+ end
194
+ end
108
195
 
109
- def info(*msg)
110
- @logger.info(*msg)
196
+ # Set a custom logger instance.
197
+ #
198
+ # Allows replacing the default FamiliaLogger with any Logger-compatible
199
+ # object. Useful for integrating with application logging frameworks.
200
+ #
201
+ # @param new_logger [Logger] The logger to use
202
+ # @return [Logger] The logger that was set
203
+ #
204
+ # @example Use Rails logger
205
+ # Familia.logger = Rails.logger
206
+ #
207
+ # @example Custom file logger
208
+ # Familia.logger = Logger.new('familia.log').tap do |log|
209
+ # log.level = Logger::INFO
210
+ # end
211
+ #
212
+ def logger=(new_logger)
213
+ @logger = new_logger
111
214
  end
112
215
 
113
- def warn(*msg)
114
- @logger.warn(*msg)
216
+ # Log an informational message.
217
+ #
218
+ # @param msg [String] The message to log
219
+ # @return [true]
220
+ #
221
+ # @example
222
+ # Familia.info "Redis connection established"
223
+ #
224
+ def info(msg)
225
+ logger.info(msg)
115
226
  end
116
227
 
117
- def ld(*msg)
118
- return unless Familia.debug?
228
+ # Log a warning message.
229
+ #
230
+ # @param msg [String] The message to log
231
+ # @return [true]
232
+ #
233
+ # @example
234
+ # Familia.warn "Cache miss for key: user:123"
235
+ #
236
+ def warn(msg)
237
+ logger.warn(msg)
238
+ end
119
239
 
120
- @logger.debug(*msg)
240
+ # Log a debug message (only when Familia.debug? is true).
241
+ #
242
+ # Short for "log debug". Only outputs when FAMILIA_DEBUG environment
243
+ # variable is set to '1' or 'true'.
244
+ #
245
+ # @param msg [String] The message to log
246
+ # @return [true, nil] Returns true if logged, nil if debug disabled
247
+ #
248
+ # @example
249
+ # Familia.ld "Cache lookup for user:123"
250
+ # # Only outputs when FAMILIA_DEBUG=true
251
+ #
252
+ def ld(msg)
253
+ logger.debug(msg) if Familia.debug?
121
254
  end
122
255
 
123
- def le(*msg)
124
- @logger.error(*msg)
256
+ # Log an error message.
257
+ #
258
+ # Short for "log error".
259
+ #
260
+ # @param msg [String] The message to log
261
+ # @return [true]
262
+ #
263
+ # @example
264
+ # Familia.le "Failed to deserialize value: #{e.message}"
265
+ #
266
+ def le(msg)
267
+ logger.error(msg)
125
268
  end
126
269
 
127
- # Logs a trace message for debugging purposes if Familia.debug? is true.
270
+ # Logs a structured trace message for debugging Familia operations.
271
+ #
272
+ # This method only executes when both FAMILIA_TRACE and FAMILIA_DEBUG
273
+ # environment variables are enabled.
128
274
  #
129
275
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
130
276
  # :FROMREDIS, :LOAD, :EXISTS).
131
- # @param instance_id
132
- # @param ident [String] An identifier or key related to the operation being
133
- # traced.
134
- # @param extra_context [Array<String>, String, nil] Any extra details to include.
135
- #
136
- # @example Familia.trace :LOAD, Familia.dbclient(uri), objkey if Familia.debug?
277
+ # @param instance_id [Object] The object instance being traced (e.g., Redis client)
278
+ # @param ident [String] An identifier or key related to the operation being traced
279
+ # @param extra_context [String, nil] Any extra details to include
137
280
  #
138
281
  # @return [nil]
139
282
  #
140
- # @note This method only executes if Familia::Refinements::LoggerTrace::ENABLED is true.
141
- # @note The dbclient can be a Database object, Redis::Future (used in
142
- # pipelined and multi blocks), or nil (when the database connection isn't
143
- # relevant).
283
+ # @example
284
+ # Familia.trace :LOAD, redis_client, "user:123", "from cache"
285
+ # # Output: T, 10-05 20:43:09.843 pid:123 [456/789]: [LOAD] #<Redis> -> user:123 <-from cache
286
+ #
287
+ # @note Controlled by FAMILIA_TRACE environment variable (set to '1', 'true', or 'yes')
288
+ # @note The instance_id can be a Redis client, Redis::Future, or nil
144
289
  #
145
290
  def trace(label, instance_id = nil, ident = nil, extra_context = nil)
146
- return unless Familia::Refinements::LoggerTrace::ENABLED
291
+ return unless trace_enabled? && Familia.debug?
292
+
293
+ ident_str = ident.nil? ? '<nil>' : ident.to_s
294
+ logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
295
+ end
147
296
 
148
- # Let the other values show nothing when nil, but make it known for the focused value
149
- ident_str = (ident.nil? ? '<nil>' : ident).to_s
150
- @logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
297
+ private
298
+
299
+ # Check if trace logging is enabled via FAMILIA_TRACE environment variable.
300
+ #
301
+ # Trace logging is enabled when FAMILIA_TRACE is set to '1', 'true',
302
+ # or 'yes' (case-insensitive). Checks the environment variable on every
303
+ # call to support dynamic changes in test environments.
304
+ #
305
+ # @return [Boolean] true if trace logging is enabled
306
+ # @api private
307
+ #
308
+ def trace_enabled?
309
+ %w[1 true yes].include?(ENV.fetch('FAMILIA_TRACE', 'false').downcase)
151
310
  end
152
311
  end
153
312
  end
@@ -1,6 +1,5 @@
1
1
  # lib/familia/refinements.rb
2
2
 
3
3
  require_relative 'refinements/dear_json'
4
- require_relative 'refinements/logger_trace'
5
4
  require_relative 'refinements/stylize_words'
6
5
  require_relative 'refinements/time_literals'
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre17'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre18'.freeze unless defined?(Familia::VERSION)
6
6
  end
data/lib/familia.rb CHANGED
@@ -39,7 +39,7 @@ module Familia
39
39
  using Refinements::StylizeWords
40
40
 
41
41
  class << self
42
- attr_accessor :debug # rubocop:disable ThreadSafety/ClassAndModuleAttributes
42
+ attr_accessor :debug
43
43
  attr_reader :members
44
44
 
45
45
  def included(member)
@@ -139,7 +139,7 @@ module Familia
139
139
  require_relative 'familia/connection'
140
140
  require_relative 'familia/settings'
141
141
  require_relative 'familia/utils'
142
- require_relative 'familia/distinguisher'
142
+ require_relative 'familia/identifier_extractor'
143
143
  require_relative 'familia/json_serializer'
144
144
 
145
145
  extend SecureIdentifier
@@ -1,4 +1,4 @@
1
- # lib/middleware/database_middleware.rb
1
+ # lib/middleware/database_logger.rb
2
2
 
3
3
  require 'concurrent-ruby'
4
4
 
@@ -29,21 +29,34 @@ require 'concurrent-ruby'
29
29
  # often outweigh the slight performance cost when enabled.
30
30
  module DatabaseLogger
31
31
  @logger = nil
32
- @commands = []
32
+ @commands = Concurrent::Array.new
33
+ @max_commands = 10_000
34
+ @process_start = Time.now.to_f.freeze
35
+
36
+ CommandMessage = Data.define(:command, :μs, :timeline)
33
37
 
34
38
  class << self
35
39
  # Gets/sets the logger instance used by DatabaseLogger.
36
40
  # @return [Logger, nil] The current logger instance or nil if not set.
37
41
  attr_accessor :logger
38
42
 
43
+ # Gets/sets the maximum number of commands to capture.
44
+ # @return [Integer] The maximum number of commands to capture.
45
+ attr_accessor :max_commands
46
+
39
47
  # Gets the captured commands for testing purposes.
40
- # @return [Array] Array of command hashes with :command, :duration, :timestamp
48
+ # @return [Array] Array of command hashes with :command, :duration, :timeline
41
49
  attr_reader :commands
42
50
 
51
+ # Gets the timestamp when DatabaseLogger was loaded.
52
+ # @return [Float] The timestamp when DatabaseLogger was loaded.
53
+ attr_reader :process_start
54
+
43
55
  # Clears the captured commands array.
44
56
  # @return [Array] Empty array
45
57
  def clear_commands
46
- @commands = []
58
+ @commands.clear
59
+ nil
47
60
  end
48
61
 
49
62
  # Captures commands in a block and returns them.
@@ -62,8 +75,28 @@ module DatabaseLogger
62
75
  def capture_commands
63
76
  clear_commands
64
77
  yield
65
- @commands.dup
78
+ @commands.to_a
79
+ end
80
+
81
+ # Thread-safe append with bounded size
82
+ #
83
+ # @param message [String] The message to append.
84
+ # @return [Array] The updated array of commands.
85
+ def append_command(message)
86
+ @commands.shift if @commands.size >= @max_commands
87
+ @commands << message
88
+ end
89
+
90
+ # Returns the current time in microseconds.
91
+ # This is used to measure the duration of Database commands.
92
+ #
93
+ # Alias: now_in_microseconds
94
+ #
95
+ # @return [Integer] The current time in microseconds.
96
+ def now_in_μs
97
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
66
98
  end
99
+ alias now_in_microseconds now_in_μs
67
100
  end
68
101
 
69
102
  # Logs the Database command and its execution time.
@@ -79,19 +112,19 @@ module DatabaseLogger
79
112
  # @note Commands are always captured with minimal overhead for testing purposes.
80
113
  # Logging only occurs when DatabaseLogger.logger is set.
81
114
  def call(command, _config)
82
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
115
+ block_start = DatabaseLogger.now_in_μs
83
116
  result = yield
84
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
117
+ block_duration = DatabaseLogger.now_in_μs - block_start
118
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
85
119
 
86
- # Always capture commands for testing purposes
87
- DatabaseLogger.instance_variable_get(:@commands) << {
88
- command: command.dup,
89
- duration: duration,
90
- timestamp: Time.now,
91
- }
120
+ # We intentionally use two different codepaths for getting the
121
+ # time, although they will almost always be so similar that the
122
+ # difference is negligible.
123
+ message = CommandMessage.new(command, block_duration, lifetime_duration)
124
+ DatabaseLogger.append_command(message)
92
125
 
93
126
  # Log if logger is set
94
- DatabaseLogger.logger&.debug("Redis: #{command.inspect} (#{duration}µs)")
127
+ DatabaseLogger.logger&.debug(Oj.dump(message.to_h, mode: :strict))
95
128
 
96
129
  result
97
130
  end
data/pr_agent.toml ADDED
@@ -0,0 +1,31 @@
1
+ # Qodo Merge Configuration
2
+ # Documentation: https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/
3
+
4
+ [config]
5
+ # Ensure consistent review language across all PRs
6
+ response_language = "en"
7
+
8
+ [rag_arguments]
9
+ # Enable RAG context enrichment for codebase duplication compliance checks
10
+ enable_rag = true
11
+ # Include related repositories for comprehensive context
12
+ rag_repo_list = ['delano/familia', 'delano/tryouts', 'delano/otto']
13
+
14
+ [compliance]
15
+ # Reference custom compliance checklist for project-specific rules
16
+ custom_compliance_path = "pr_compliance_checklist.yaml"
17
+
18
+ [ignore]
19
+ # Reduce noise by excluding generated files and build artifacts
20
+ glob = [
21
+ "*.lock", # Lock files (Gemfile.lock, etc.)
22
+ "*.gem", # Built gem files
23
+ "vendor/**", # Vendored dependencies
24
+ "tmp/**", # Temporary files
25
+ "log/**", # Log files
26
+ "data/**", # Data directories
27
+ "public/**", # Public assets
28
+ ".yardoc/**", # YARD documentation cache
29
+ "dump.rdb", # Redis database dumps
30
+ "appendonlydir/**", # Redis append-only file directory
31
+ ]
@@ -0,0 +1,45 @@
1
+ # Custom Compliance Checklist for Familia
2
+ # Documentation: https://qodo-merge-docs.qodo.ai/tools/compliance/
3
+
4
+ pr_compliances:
5
+ - title: "ErrorHandling"
6
+ compliance_label: true
7
+ objective: "All external API calls and database operations must have proper error handling"
8
+ success_criteria: "Try-catch blocks around external calls with appropriate logging or error handling mechanisms"
9
+ failure_criteria: "External API calls, database operations, or network requests without error handling"
10
+
11
+ - title: "TestCoverage"
12
+ compliance_label: true
13
+ objective: "New features must include corresponding tests using the Tryouts framework"
14
+ success_criteria: "Test files present in try/ directory for new functionality following *_try.rb or *.try.rb naming convention"
15
+ failure_criteria: "New code without test coverage or tests not following Tryouts framework conventions"
16
+
17
+ - title: "ChangelogFragment"
18
+ compliance_label: true
19
+ objective: "User-facing changes must include a changelog"
20
+ success_criteria: "New fragment file in changelog.d/ directory following the naming convention and RST format, or updates to CHANGELOG.rst in root directory, or explicit justification for omission"
21
+ failure_criteria: "User-facing changes without changelog fragment, CHANGELOG.rst updates, or documentation updates"
22
+
23
+ - title: "DocumentationUpdates"
24
+ compliance_label: true
25
+ objective: "API changes must be reflected in documentation"
26
+ success_criteria: "YARD documentation comments for new public methods, or updates to docs/ for significant changes"
27
+ failure_criteria: "New public APIs or significant behavior changes without documentation updates"
28
+
29
+ - title: "BackwardCompatibility"
30
+ compliance_label: true
31
+ objective: "Changes must maintain backward compatibility or document breaking changes"
32
+ success_criteria: "No breaking changes to public APIs, or breaking changes clearly documented in migration guides"
33
+ failure_criteria: "Breaking changes without migration documentation or deprecation warnings"
34
+
35
+ - title: "ThreadSafety"
36
+ compliance_label: true
37
+ objective: "Code handling shared state must be thread-safe"
38
+ success_criteria: "Proper synchronization for shared mutable state, or clear documentation of thread-safety assumptions"
39
+ failure_criteria: "Shared mutable state accessed without synchronization in concurrent contexts"
40
+
41
+ - title: "DatabaseKeyNaming"
42
+ compliance_label: true
43
+ objective: "Database key generation must follow Familia conventions"
44
+ success_criteria: "Keys use delim separator, avoid reserved keywords (ttl, db, valkey, redis), and handle empty identifiers"
45
+ failure_criteria: "Keys using reserved keywords, empty identifiers, or non-standard separators"
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test empty identifier edge cases
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../support/helpers/test_helpers'
6
6
 
7
7
 
8
8
  ## empty string identifier handling