Coverage for app / backend / src / couchers / models / logging.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-19 14:14 +0000

1import enum 

2from datetime import datetime 

3from typing import Any 

4 

5from sqlalchemy import BigInteger, Boolean, DateTime, Enum, Float, Index, String, func 

6from sqlalchemy import LargeBinary as Binary 

7from sqlalchemy.dialects.postgresql import JSONB 

8from sqlalchemy.orm import Mapped, mapped_column 

9from sqlalchemy.sql import expression 

10 

11from couchers.config import config 

12from couchers.models.base import Base 

13 

14 

15class EventSource(enum.Enum): 

16 backend = enum.auto() 

17 frontend = enum.auto() 

18 

19 

20class APICall(Base, kw_only=True): 

21 """ 

22 API call logs 

23 """ 

24 

25 __tablename__ = "api_calls" 

26 __table_args__ = {"schema": "logging"} 

27 

28 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

29 

30 # whether the call was made using an api key or session cookies 

31 is_api_key: Mapped[bool] = mapped_column(Boolean, server_default=expression.false()) 

32 

33 # backend version (normally e.g. develop-31469e3), allows us to figure out which proto definitions were used 

34 # note that `default` is a python side default, not hardcoded into DB schema 

35 version: Mapped[str] = mapped_column(String, default=config["VERSION"]) 

36 

37 # approximate time of the call 

38 time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

39 

40 # the method call name, e.g. "/org.couchers.api.core.API/ListFriends" 

41 method: Mapped[str] = mapped_column(String) 

42 

43 # gRPC status code name, e.g. FAILED_PRECONDITION, None if success 

44 status_code: Mapped[str | None] = mapped_column(String, default=None) 

45 

46 # handler duration (excluding serialization, etc) 

47 duration: Mapped[float] = mapped_column(Float) 

48 

49 # user_id of caller, None means not logged in 

50 user_id: Mapped[int | None] = mapped_column(BigInteger, default=None) 

51 

52 # sanitized request bytes 

53 request: Mapped[bytes | None] = mapped_column(Binary, default=None) 

54 

55 # sanitized response bytes 

56 response: Mapped[bytes | None] = mapped_column(Binary, default=None) 

57 

58 # whether response bytes have been truncated 

59 response_truncated: Mapped[bool] = mapped_column(Boolean, server_default=expression.false()) 

60 

61 # the exception traceback, if any 

62 traceback: Mapped[str | None] = mapped_column(String, default=None) 

63 

64 # human readable perf report 

65 perf_report: Mapped[str | None] = mapped_column(String, default=None) 

66 

67 # details of the browser, if available 

68 ip_address: Mapped[str | None] = mapped_column(String, default=None) 

69 user_agent: Mapped[str | None] = mapped_column(String, default=None) 

70 

71 sofa: Mapped[str | None] = mapped_column(String, default=None) 

72 

73 

74class EventLog(Base, kw_only=True): 

75 """ 

76 Analytics event log for tracking user behavior and business metrics. 

77 

78 Append-only table for ELT extraction. Do not query this table for user-facing features. 

79 """ 

80 

81 __tablename__ = "event_log" 

82 

83 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

84 

85 # when the row was inserted into the DB 

86 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

87 

88 # when the event actually happened (same as created for backend; may differ for frontend events) 

89 occurred: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

90 

91 # backend/frontend version 

92 version: Mapped[str] = mapped_column(String, default=config["VERSION"]) 

93 

94 # sofa, null for background/system events 

95 sofa: Mapped[str | None] = mapped_column(String, default=None) 

96 

97 # hierarchical event type, e.g. "host_request.sent", "account.login" 

98 event_type: Mapped[str] = mapped_column(String) 

99 

100 # user who triggered the event, nullable for system events 

101 user_id: Mapped[int | None] = mapped_column(BigInteger, default=None) 

102 

103 # flexible event-specific properties 

104 properties: Mapped[dict[str, Any]] = mapped_column(JSONB) 

105 

106 # numeric value (duration, count, etc.) 

107 value: Mapped[float] = mapped_column(Float, server_default="1.0", default=1.0) 

108 

109 # where the event originated 

110 source: Mapped[EventSource] = mapped_column(Enum(EventSource)) 

111 

112 __table_args__ = ( 

113 Index("ix_logging_event_log_created", "created"), 

114 Index("ix_logging_event_log_event_type_created", "event_type", "created"), 

115 Index("ix_logging_event_log_user_id_created", "user_id", "created"), 

116 {"schema": "logging"}, 

117 )