|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | +) |
| 9 | + |
| 10 | +// Health probes are not traffic. A kubelet asks /healthz and /readyz every |
| 11 | +// few seconds for the life of a pod, and uvicorn's access log writes one line |
| 12 | +// per answer: on a cell that was 180 lines per pod every ten minutes, all of |
| 13 | +// them "200 OK", shipped to the telemetry store as if they were requests. The |
| 14 | +// trace of a real request already carries what an access line says; a probe |
| 15 | +// hit says nothing anyone reads. |
| 16 | +// |
| 17 | +// The service is not asked to know this. The agent hands uvicorn a logging |
| 18 | +// configuration — uvicorn's own default, with one filter on the access |
| 19 | +// handler — and the filter's module, in both places a service runs: written |
| 20 | +// beside the project for a native run (`uv run uvicorn` puts the working |
| 21 | +// directory on sys.path) and copied into the image by the builder. A service |
| 22 | +// that configures its own logging is untouched: --log-config only shapes |
| 23 | +// uvicorn's loggers. |
| 24 | + |
| 25 | +// uvicornLogConfigFile and uvicornLoggingModule are the two files, named so a |
| 26 | +// service's .gitignore can name them: they are the agent's, regenerated on |
| 27 | +// every start, never the service's source. |
| 28 | +const ( |
| 29 | + uvicornLogConfigFile = "uvicorn-log-config.json" |
| 30 | + uvicornLoggingModule = "codefly_uvicorn_logging.py" |
| 31 | +) |
| 32 | + |
| 33 | +// probePaths is the set a kubelet (and a mesh, and a load balancer) probes. |
| 34 | +const probePaths = `^/(healthz|readyz|livez|health|ready|live|ping)$` |
| 35 | + |
| 36 | +// uvicornLoggingModuleSource is the filter: uvicorn's access record carries |
| 37 | +// the request as its args (client address, method, path, HTTP version, status |
| 38 | +// code), and a probe path is dropped before the handler formats it. |
| 39 | +const uvicornLoggingModuleSource = `"""Codefly's uvicorn logging filter. Generated by the python-fastapi agent; |
| 40 | +regenerated on every start, so edits here do not survive. Health probes are |
| 41 | +not traffic: an access-log line for /healthz or /readyz is dropped before it is |
| 42 | +formatted, and every other line is written exactly as uvicorn writes it.""" |
| 43 | +import logging |
| 44 | +import re |
| 45 | +
|
| 46 | +PROBE_PATHS = re.compile(r"` + probePaths + `") |
| 47 | +
|
| 48 | +
|
| 49 | +class ProbeFilter(logging.Filter): |
| 50 | + def filter(self, record: logging.LogRecord) -> bool: |
| 51 | + args = record.args |
| 52 | + if isinstance(args, tuple) and len(args) >= 3 and isinstance(args[2], str): |
| 53 | + path = args[2].split("?", 1)[0] |
| 54 | + if PROBE_PATHS.match(path): |
| 55 | + return False |
| 56 | + return True |
| 57 | +` |
| 58 | + |
| 59 | +// uvicornLogConfig is uvicorn's default logging configuration (its |
| 60 | +// config.LOGGING_CONFIG) with the probe filter on the access handler. Restated |
| 61 | +// rather than merged because --log-config replaces the default whole. |
| 62 | +func uvicornLogConfig() map[string]any { |
| 63 | + return map[string]any{ |
| 64 | + "version": 1, |
| 65 | + "disable_existing_loggers": false, |
| 66 | + "filters": map[string]any{ |
| 67 | + "probes": map[string]any{"()": "codefly_uvicorn_logging.ProbeFilter"}, |
| 68 | + }, |
| 69 | + "formatters": map[string]any{ |
| 70 | + "default": map[string]any{ |
| 71 | + "()": "uvicorn.logging.DefaultFormatter", |
| 72 | + "fmt": "%(levelprefix)s %(message)s", |
| 73 | + "use_colors": nil, |
| 74 | + }, |
| 75 | + "access": map[string]any{ |
| 76 | + "()": "uvicorn.logging.AccessFormatter", |
| 77 | + "fmt": `%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s`, |
| 78 | + }, |
| 79 | + }, |
| 80 | + "handlers": map[string]any{ |
| 81 | + "default": map[string]any{"formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr"}, |
| 82 | + "access": map[string]any{"formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", "filters": []string{"probes"}}, |
| 83 | + }, |
| 84 | + "loggers": map[string]any{ |
| 85 | + "uvicorn": map[string]any{"handlers": []string{"default"}, "level": "INFO", "propagate": false}, |
| 86 | + "uvicorn.error": map[string]any{"level": "INFO"}, |
| 87 | + "uvicorn.access": map[string]any{"handlers": []string{"access"}, "level": "INFO", "propagate": false}, |
| 88 | + }, |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +// writeUvicornLogging writes the configuration and the filter module into dir, |
| 93 | +// the directory uvicorn runs from. Both are rewritten whole on every call. |
| 94 | +func writeUvicornLogging(dir string) error { |
| 95 | + config, err := json.MarshalIndent(uvicornLogConfig(), "", " ") |
| 96 | + if err != nil { |
| 97 | + return fmt.Errorf("render uvicorn logging configuration: %w", err) |
| 98 | + } |
| 99 | + for name, content := range map[string][]byte{ |
| 100 | + uvicornLogConfigFile: append(config, '\n'), |
| 101 | + uvicornLoggingModule: []byte(uvicornLoggingModuleSource), |
| 102 | + } { |
| 103 | + if err := os.WriteFile(filepath.Join(dir, name), content, 0o644); err != nil { |
| 104 | + return fmt.Errorf("write %s: %w", name, err) |
| 105 | + } |
| 106 | + } |
| 107 | + return nil |
| 108 | +} |
0 commit comments