diff --git a/README.md b/README.md index c965984..8168620 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,15 @@ This service runs OPA as a standalone authorization engine for `backend/access_c docker compose -f accsess/docker-compose.yaml up -d ``` +If you want request logging only on development, start the stack with +`APP_ENV=DEVELOP` and enable the `develop` profile. In that mode, OPA sends +decision logs to a sidecar service, and the log file is written to +`accsess/logs/opa.log` on the host through a Docker volume. + +```bash +APP_ENV=DEVELOP COMPOSE_PROFILES=develop docker compose -f accsess/docker-compose.yaml up -d +``` + ## Decision endpoints - Single feature: `POST /v1/data/croplogic/authz/decision` diff --git a/config/opa-config.DEVELOP.yaml b/config/opa-config.DEVELOP.yaml new file mode 100644 index 0000000..5c0bfb5 --- /dev/null +++ b/config/opa-config.DEVELOP.yaml @@ -0,0 +1,11 @@ +services: + requestlog: + url: http://opa-log-receiver:8282/logs +labels: + app: croplogic-authz +plugins: {} +decision_logs: + service: requestlog + reporting: + min_delay_seconds: 1 + max_delay_seconds: 5 diff --git a/config/opa-config.default.yaml b/config/opa-config.default.yaml new file mode 100644 index 0000000..6ceb01f --- /dev/null +++ b/config/opa-config.default.yaml @@ -0,0 +1,4 @@ +services: {} +labels: + app: croplogic-authz +plugins: {} diff --git a/docker-compose.yaml b/docker-compose.yaml index 251a2ed..7b0b08a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,16 +6,38 @@ services: - run - --server - --addr=0.0.0.0:8181 + - --config-file=/config/opa-config.${APP_ENV:-default}.yaml - /policies + environment: + APP_ENV: ${APP_ENV:-} ports: - "8181:8181" volumes: - ./policies:/policies:ro - - ./config/opa-config.yaml:/config/opa-config.yaml:ro + - ./config/opa-config.default.yaml:/config/opa-config.default.yaml:ro + - ./config/opa-config.DEVELOP.yaml:/config/opa-config.DEVELOP.yaml:ro restart: unless-stopped networks: - crop_network + + opa-log-receiver: + image: docker.iranserver.com/python:3.10 + container_name: croplogic-accsess-opa-log-receiver + profiles: + - develop + command: + - python + - /app/scripts/opa_log_receiver.py + environment: + OPA_REQUEST_LOG_FILE: /logs/opa.log + OPA_REQUEST_LOG_PORT: "8282" + volumes: + - ./scripts/opa_log_receiver.py:/app/scripts/opa_log_receiver.py:ro + - ./logs:/logs + restart: unless-stopped + networks: + - crop_network networks: crop_network: - external: true \ No newline at end of file + external: true diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/logs/opa.log b/logs/opa.log new file mode 100644 index 0000000..39853f6 --- /dev/null +++ b/logs/opa.log @@ -0,0 +1,4 @@ +{"timestamp": "2026-04-09T20:07:30.617741+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "401", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "3ee2fa07-ce00-4c79-9782-76edae020652", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["feature1", "feature2", "feature3"]}, "result": {"features": {"feature1": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}, "feature2": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}, "feature3": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.1:59682", "timestamp": "2026-04-09T20:07:29.762128957Z", "metrics": {"counter_server_query_cache_hit": 0, "timer_rego_input_parse_ns": 57527, "timer_rego_query_compile_ns": 93329, "timer_rego_query_eval_ns": 199196, "timer_server_handler_ns": 471175}, "req_id": 1}]} +{"timestamp": "2026-04-09T20:08:21.624001+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "544", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "b6ca9264-4576-4826-ac70-067ad6850019", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_management"], "resource": {"crop_types": [], "cultivation_types": [], "customization": [], "farm_id": null, "farm_types": [], "power_sensor": [], "sensor_codes": [], "subscription_plan_codes": []}, "route": "/api/farm-hub/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_management": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:35524", "timestamp": "2026-04-09T20:08:19.762624801Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 71833, "timer_rego_query_eval_ns": 127252, "timer_server_handler_ns": 231704}, "req_id": 2}]} +{"timestamp": "2026-04-09T20:08:43.385941+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "621", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "b52a1754-aaf8-4c7d-9bfb-1d25d803f007", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_dashboard"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-dashboard/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_dashboard": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:41130", "timestamp": "2026-04-09T20:08:43.104063998Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 83718, "timer_rego_query_eval_ns": 141627, "timer_server_handler_ns": 263982}, "req_id": 3}]} +{"timestamp": "2026-04-09T20:12:48.961473+00:00", "path": "/logs/logs", "headers": {"Host": "opa-log-receiver:8282", "User-Agent": "Open Policy Agent/1.15.2 (linux, amd64)", "Content-Length": "625", "Content-Encoding": "gzip", "Content-Type": "application/json", "Accept-Encoding": "gzip"}, "body": [{"labels": {"app": "croplogic-authz", "id": "c211530e-d6bb-4067-abed-57fe193b6e5b", "version": "1.15.2"}, "decision_id": "98adca8f-68fb-47d6-8162-59c2cb83d18b", "path": "croplogic/authz/batch_decision", "input": {"action": "view", "features": ["farm_management"], "resource": {"crop_types": ["\u0630\u0631\u062a", "\u06af\u0646\u062f\u0645"], "cultivation_types": [], "customization": [], "farm_id": "11111111-1111-1111-1111-111111111111", "farm_types": ["\u0632\u0631\u0627\u0639\u06cc"], "power_sensor": ["solar"], "sensor_codes": ["sensor_7_soil_moisture_sensor_v1_2"], "subscription_plan_codes": []}, "route": "/api/farm-hub/11111111-1111-1111-1111-111111111111/", "user": {"email": "admin@example.com", "id": 1, "is_staff": true, "is_superuser": true, "phone_number": "0912345678", "role": "farmer", "username": "admin"}}, "result": {"features": {"farm_management": {"allow": true, "allow_rules": [], "deny_rules": [], "matched_rules": []}}}, "requested_by": "172.29.0.6:46450", "timestamp": "2026-04-09T20:12:47.941138139Z", "metrics": {"counter_server_query_cache_hit": 1, "timer_rego_input_parse_ns": 97369, "timer_rego_query_eval_ns": 181108, "timer_server_handler_ns": 317548}, "req_id": 4}]} diff --git a/scripts/__pycache__/opa_log_receiver.cpython-314.pyc b/scripts/__pycache__/opa_log_receiver.cpython-314.pyc new file mode 100644 index 0000000..9c408b4 Binary files /dev/null and b/scripts/__pycache__/opa_log_receiver.cpython-314.pyc differ diff --git a/scripts/opa_log_receiver.py b/scripts/opa_log_receiver.py new file mode 100644 index 0000000..da93533 --- /dev/null +++ b/scripts/opa_log_receiver.py @@ -0,0 +1,44 @@ +import json +import os +import gzip +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + +LOG_FILE = os.environ.get("OPA_REQUEST_LOG_FILE", "/logs/opa.log") +PORT = int(os.environ.get("OPA_REQUEST_LOG_PORT", "8282")) + + +class DecisionLogHandler(BaseHTTPRequestHandler): + def do_POST(self): + content_length = int(self.headers.get("Content-Length", "0")) + raw_payload = self.rfile.read(content_length) if content_length else b"" + content_encoding = self.headers.get("Content-Encoding", "").lower() + + if content_encoding == "gzip" and raw_payload: + raw_payload = gzip.decompress(raw_payload) + + payload = raw_payload.decode("utf-8") if raw_payload else "" + + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "path": self.path, + "headers": dict(self.headers.items()), + "body": json.loads(payload) if payload else None, + } + + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) + with open(LOG_FILE, "a", encoding="utf-8") as log_file: + log_file.write(json.dumps(entry, ensure_ascii=True) + "\n") + + self.send_response(200) + self.end_headers() + self.wfile.write(b"ok") + + def log_message(self, format, *args): + return + + +if __name__ == "__main__": + server = ThreadingHTTPServer(("0.0.0.0", PORT), DecisionLogHandler) + server.serve_forever()