openapi: 3.0.3

info:
  title: system-log API
  version: "1.0.0"
  description: |
    REST API сервиса **system-log** (`platform/system-log`) — приём и выборка
    записей журнала системных событий (кто, над чем и какое действие совершил).

    Сервис написан на Go (фреймворк **Fiber v2**). Роутинг собирается в
    `internal/controller/http/v0`: базовая группа `/api` → подгруппа `v0` →
    ресурс `/system_log`. Плюс служебный эндпоинт `/ping`.

    ### Аутентификация
    Аутентификация на уровне приложения не настроена — эндпоинты доступны без
    авторизации. Ограничение доступа обеспечивается сетевым слоем (ClusterIP /
    Istio), а не самим сервисом.

    ### Пагинация
    Списочные ответы возвращают объект `{ count, limit, offset, next, prev, result }`.
    Управление — параметрами `limit` и `offset`. Значения по умолчанию:
    `limit = 100`, `offset = 0` (константы `_defaultLimit` / `_defaultOffset`
    в `internal/controller/http/v0/systemlog/controller.go`). Ссылки `next`/`prev`
    формируются в `pkg/pagination`. У эндпоинта поиска (`POST /search`) полей
    `next`/`prev` нет.

    ### Фильтрация
    Массивные фильтры (`actor_ids`, `event_names`, `model_names`, `instance_ids`,
    `target_ids`, `company_ids`, `statuses`, `message`) в GET-запросе передаются
    списком значений через запятую и парсятся Fiber `QueryParser`. Поля
    `instance_uuids` и `resource_uuids` разбираются вручную (`strings.Split` +
    `uuid.Parse`) — при невалидном UUID возвращается `400`.

    ### Замечания (расхождения кода)
    - `metadata` в фильтре объявлено как `map[string]interface{}` с query-тегом,
      но при передаче в query-строке Fiber не восстановит вложенный объект —
      фильтрация по `metadata` практически применима через тело `POST /search`.
    - Поле `is_processed_by_worker` присутствует в фильтре, но отдаётся в выборку
      наравне с остальными; используется воркером обогащения.
    - Ответ `POST /system_log/` при успехе — простая строка `"OK"` (не объект).

servers:
  - url: https://api.sarex.io
    description: Production (ingress)
  - url: https://stage-api.sarex.io
    description: Stage (ingress)
  - url: http://backend-svc.system-log.svc.cluster.local
    description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000)
  - url: http://localhost:8888
    description: Локальный запуск (config.env)

tags:
  - name: system_log
    description: Записи журнала системных событий
  - name: service
    description: Служебные эндпоинты

paths:
  # ==========================================================================
  # Service
  # ==========================================================================
  /ping:
    get:
      tags: [service]
      summary: Проверка живости
      description: Liveness/readiness-проба (используется в probes деплоймента). Всегда `200 OK` без тела.
      operationId: ping
      security: []
      responses:
        '200':
          description: OK

  # ==========================================================================
  # System log
  # ==========================================================================
  /api/v0/system_log/:
    get:
      tags: [system_log]
      summary: Список записей журнала (с фильтрами)
      description: |
        Возвращает отфильтрованные записи журнала с пагинацией.
        Массивные параметры принимают список значений через запятую.
      operationId: getSystemLogRecords
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: actor_ids
          in: query
          description: ID авторов событий; список значений через запятую
          example: "1,2,3"
          schema: { type: string }
        - name: event_names
          in: query
          description: Названия событий (напр. create, edit, delete, transfer); через запятую
          example: "create,edit,delete"
          schema: { type: string }
        - name: model_names
          in: query
          description: Названия моделей (напр. document, bundle, project); через запятую
          example: "document,bundle"
          schema: { type: string }
        - name: instance_ids
          in: query
          description: ID сущностей; список значений через запятую
          example: "1,2,3"
          schema: { type: string }
        - name: instance_uuids
          in: query
          description: UUID сущностей; список значений через запятую (валидируются как UUID)
          example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          schema: { type: string }
        - name: resource_uuids
          in: query
          description: UUID ресурсов; список значений через запятую (валидируются как UUID)
          schema: { type: string }
        - name: registered_at_gte
          in: query
          description: Нижняя граница времени регистрации (>=)
          example: "2025-02-16T15:04:05Z"
          schema: { type: string, format: date-time }
        - name: registered_at_lte
          in: query
          description: Верхняя граница времени регистрации (<=)
          example: "2025-02-16T15:04:05Z"
          schema: { type: string, format: date-time }
        - name: instance_path
          in: query
          description: Путь сущности (ltree), точное совпадение
          example: "1.79899.80131"
          schema: { type: string }
        - name: target_ids
          in: query
          description: ID целей; список значений через запятую
          example: "1,2,3"
          schema: { type: string }
        - name: target_path
          in: query
          description: Путь цели (ltree), точное совпадение
          example: "4.3.2.1"
          schema: { type: string }
        - name: company_ids
          in: query
          description: ID компаний; список значений через запятую
          example: "1,2,3"
          schema: { type: string }
        - name: statuses
          in: query
          description: Подстатусы события; список значений через запятую
          example: "add_bundle,delete_bundle,rename_document"
          schema: { type: string }
        - name: message
          in: query
          description: Сообщения; список значений через запятую
          schema: { type: string }
        - name: is_processed_by_worker
          in: query
          description: Признак обработки записи воркером обогащения
          schema: { type: boolean }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SystemLogListResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [system_log]
      summary: Создать записи журнала
      description: |
        Принимает пакет записей в поле `system_logs`. Для каждой записи должно
        быть задано **ровно одно** из `instance_id` / `instance_uuid`
        (не оба и не ни одного — иначе `400`). Обязательны `event_name` и
        `model_name`. При успехе возвращается строка `"OK"`.
      operationId: createSystemLogRecords
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SystemLogCreateRequest' }
      responses:
        '200':
          description: Записи созданы
          content:
            application/json:
              schema:
                type: string
                example: "OK"
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v0/system_log/search:
    post:
      tags: [system_log]
      summary: Поиск записей журнала (фильтры в теле)
      description: |
        Аналог GET-выборки, но фильтры передаются в теле запроса
        (`filters`). Позволяет использовать сложные фильтры (в т.ч. `metadata`),
        которые неудобно кодировать в query-строке. В ответе нет `next`/`prev`.
      operationId: searchSystemLogs
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SystemLogSearchRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SystemLogSearchResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'

# ============================================================================
components:
  parameters:
    Limit:
      name: limit
      in: query
      description: Кол-во записей на странице
      schema: { type: integer, format: uint64, default: 100 }
    Offset:
      name: offset
      in: query
      description: Смещение от начала выборки
      schema: { type: integer, format: uint64, default: 0, minimum: 0 }

  responses:
    BadRequest:
      description: Ошибка разбора/валидации запроса
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ControllerError' }
    InternalError:
      description: Внутренняя ошибка сервиса
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ControllerError' }

  schemas:
    # ---- Общие ----
    ControllerError:
      type: object
      description: Формат ошибки (`internal/controller/errors.go`, `ErrorToResponse`).
      properties:
        message: { type: string, example: "error parse body" }
        status_code: { type: integer, example: 400 }

    # ---- Доменная запись ----
    SystemLog:
      type: object
      description: Запись журнала системных событий (`internal/entity/system_log.go`).
      properties:
        actor_id:
          type: integer
          format: uint64
          description: ID автора события
          example: 1023
        event_name:
          type: string
          description: Название события (напр. edit, create, copy)
          example: "edit"
        model_name:
          type: string
          description: Над какой сущностью произошло событие (напр. document, inspection)
          example: "document"
        instance_id:
          type: integer
          format: uint64
          nullable: true
          description: ID сущности (для моделей с числовым ключом)
          example: 80131
        instance_uuid:
          type: string
          format: uuid
          nullable: true
          description: UUID сущности (для моделей с UUID-ключом)
        registered_at:
          type: string
          format: date-time
          nullable: true
          description: Время регистрации события
          example: "2023-05-04T08:09:53.852916Z"
        instance_path:
          type: string
          nullable: true
          description: Путь сущности (ltree)
          example: "1.79899.80131"
        target_id:
          type: integer
          format: uint64
          nullable: true
          description: ID цели события
        company_id:
          type: integer
          format: uint64
          nullable: true
          description: ID компании (может обогащаться воркером)
          example: 1
        metadata:
          type: object
          additionalProperties: true
          description: Произвольные дополнительные данные о событии
          example: { "document_name": "КСГ.pdf", "count_bundle": 3 }
        message:
          type: string
          nullable: true
          description: Произвольное сообщение
          example: "bundle was deleted from document"
        status:
          type: string
          nullable: true
          description: Подстатус события
          example: "delete_bundle"
        resource_uuid:
          type: string
          format: uuid
          nullable: true
          description: UUID связанного ресурса
        target_path:
          type: string
          nullable: true
          description: Путь цели (ltree)
      required: [event_name, model_name]

    # ---- Запросы ----
    SystemLogCreateRequest:
      type: object
      description: >
        Пакет записей для создания. Для каждой записи обязательно ровно одно из
        instance_id / instance_uuid.
      properties:
        system_logs:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/SystemLog' }
      required: [system_logs]

    SystemLogFilter:
      type: object
      description: Набор фильтров выборки (`entity.SystemLogFilter`).
      properties:
        limit: { type: integer, format: uint64, default: 100 }
        offset: { type: integer, format: uint64, default: 0 }
        actor_ids:
          type: array
          items: { type: integer, format: uint64 }
        event_names:
          type: array
          items: { type: string }
        model_names:
          type: array
          items: { type: string }
        instance_ids:
          type: array
          items: { type: integer, format: uint64 }
        instance_uuids:
          type: array
          items: { type: string, format: uuid }
        registered_at_gte: { type: string, format: date-time, nullable: true }
        registered_at_lte: { type: string, format: date-time, nullable: true }
        instance_path: { type: string, nullable: true }
        target_ids:
          type: array
          items: { type: integer, format: uint64 }
        target_path: { type: string, nullable: true }
        company_ids:
          type: array
          items: { type: integer, format: uint64 }
        statuses:
          type: array
          items: { type: string }
        message: { type: string, nullable: true }
        metadata:
          type: object
          additionalProperties: true
        resource_uuids:
          type: array
          items: { type: string, format: uuid }
        is_processed_by_worker: { type: boolean, nullable: true }

    SystemLogSearchRequest:
      type: object
      description: Тело запроса поиска (`SearchParams` в `dto.go`).
      properties:
        limit: { type: integer, format: uint64, default: 100 }
        offset: { type: integer, format: uint64, default: 0 }
        filters: { $ref: '#/components/schemas/SystemLogFilter' }

    # ---- Ответы ----
    SystemLogListResponse:
      type: object
      description: Ответ списочной выборки (`entity.SystemLogRecordsRequest`).
      properties:
        count: { type: integer, format: uint64, example: 1226 }
        limit: { type: integer, format: uint64, nullable: true, example: 100 }
        offset: { type: integer, format: uint64, nullable: true, example: 0 }
        next:
          type: string
          nullable: true
          example: "http://localhost:8888/api/v0/system_log?limit=100&offset=100"
        prev:
          type: string
          nullable: true
        result:
          type: array
          items: { $ref: '#/components/schemas/SystemLog' }
      required: [count, result]

    SystemLogSearchResponse:
      type: object
      description: Ответ поиска (`entity.SystemLogRecordsSearch`) — без next/prev.
      properties:
        count: { type: integer, format: uint64, example: 1226 }
        limit: { type: integer, format: uint64, nullable: true, example: 100 }
        offset: { type: integer, format: uint64, nullable: true, example: 0 }
        result:
          type: array
          items: { $ref: '#/components/schemas/SystemLog' }
      required: [count, result]
