Module: FiberAudit::Runtime::JSONL::Schema

Defined in:
lib/fiber_audit/runtime/jsonl/schema.rb

Overview

Versioned builders and validators stay together to keep one schema authority. rubocop:disable Metrics/ModuleLength

Constant Summary collapse

SCHEMA_VERSION =
'1.0'
RECORD_TYPES =
%w[session_start event session_end].freeze
ENVELOPE_KEYS =
%w[
  schema_version record_type session_id sequence recorded_at monotonic_ns payload
].freeze
POLICY_KEYS =
%w[
  redaction sampling_rate max_events_per_second max_events_per_session
  max_record_bytes max_session_bytes fail_open
].freeze
EVENT_KEYS =
%w[
  kind source duration_ns operation location execution_context thread_id fiber_id measurements
].freeze
LOCATION_KEYS =
%w[path line column].freeze
END_KEYS =
%w[status events_observed events_emitted dropped internal_errors].freeze
DROPPED_KEYS =
%w[
  sampling rate_limit session_event_limit session_byte_limit record_size_limit
].freeze
UUID =
Validation::UUID
TIMESTAMP =
/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z\z/

Class Method Summary collapse

Class Method Details

.dump(record, max_record_bytes:) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/fiber_audit/runtime/jsonl/schema.rb', line 121

def dump(record, max_record_bytes:)
  validate!(record)
  unless max_record_bytes.is_a?(Integer) && max_record_bytes.positive?
    raise RuntimeContractError, 'max_record_bytes must be a positive Integer'
  end

  json = "#{::JSON.generate(record)}\n"
  if json.bytesize > max_record_bytes
    raise RuntimeSafetyError,
          "runtime JSONL record is #{json.bytesize} bytes; limit is #{max_record_bytes}"
  end

  json.freeze
rescue ::JSON::GeneratorError => e
  raise RuntimeContractError, "runtime JSONL record is not JSON-safe: #{e.message}"
end

.end_record(session_id:, sequence:, summary:) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/fiber_audit/runtime/jsonl/schema.rb', line 75

def end_record(session_id:, sequence:, summary:)
  require_type!(summary, SessionSummary, 'summary')
  build_record(
    record_type: 'session_end',
    session_id: session_id,
    sequence: sequence,
    recorded_at: summary.ended_at,
    monotonic_ns: summary.ended_monotonic_ns,
    payload: {
      'status' => summary.status.to_s,
      'events_observed' => summary.events_observed,
      'events_emitted' => summary.events_emitted,
      'dropped' => {
        'sampling' => summary.sampled_out,
        'rate_limit' => summary.rate_limited,
        'session_event_limit' => summary.session_event_limited,
        'session_byte_limit' => summary.session_byte_limited,
        'record_size_limit' => summary.oversize
      },
      'internal_errors' => summary.internal_errors
    }
  )
end

.event_record(session_id:, sequence:, event:) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/fiber_audit/runtime/jsonl/schema.rb', line 53

def event_record(session_id:, sequence:, event:)
  require_type!(event, Event, 'event')
  build_record(
    record_type: 'event',
    session_id: session_id,
    sequence: sequence,
    recorded_at: event.occurred_at,
    monotonic_ns: event.monotonic_ns,
    payload: {
      'kind' => event.kind.to_s,
      'source' => event.source.to_s,
      'duration_ns' => event.duration_ns,
      'operation' => event.operation,
      'location' => location_payload(event.location),
      'execution_context' => event.execution_context.to_s,
      'thread_id' => event.thread_id,
      'fiber_id' => event.fiber_id,
      'measurements' => event.measurements
    }
  )
end

.start_record(session) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/fiber_audit/runtime/jsonl/schema.rb', line 37

def start_record(session)
  require_type!(session, Session, 'session')
  build_record(
    record_type: 'session_start',
    session_id: session.id,
    sequence: 0,
    recorded_at: session.started_at,
    monotonic_ns: session.started_monotonic_ns,
    payload: {
      'tool_version' => session.tool_version,
      'ruby_version' => session.ruby_version,
      'policy' => policy_payload(session.policy)
    }
  )
end

.validate!(record) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/fiber_audit/runtime/jsonl/schema.rb', line 99

def validate!(record)
  require_exact_keys!(record, ENVELOPE_KEYS, 'record')
  require_value!(record['schema_version'] == SCHEMA_VERSION, 'schema_version must be 1.0')
  require_value!(RECORD_TYPES.include?(record['record_type']), 'record_type is invalid')
  validate_uuid!(record['session_id'])
  validate_sequence!(record['sequence'], record['record_type'])
  parse_timestamp(record['recorded_at'], 'recorded_at')
  require_non_negative_integer!(record['monotonic_ns'], 'monotonic_ns')

  case record['record_type']
  when 'session_start' then validate_start_payload!(record['payload'])
  when 'event' then validate_event_payload!(record)
  when 'session_end' then validate_end_payload!(record)
  end

  record
rescue RuntimeContractError
  raise
rescue StandardError => e
  raise RuntimeContractError, "invalid runtime JSONL record: #{e.message}"
end