agente de servidor
Python
El único agente capaz de distinguir computar de esperar. Todos los demás informan de que tu código se volvió más lento y ahí se quedan — este dice cuál de las dos cosas pasó, y necesitan arreglos opuestos.
- paquete
sixty-shen PyPI- funciona sobre
- Python 3.8 o posterior. Sin dependencias.
- código
- sixty-sh/sixty-python
Cómo instalarlo
Django, Flask, FastAPI or any WSGI/ASGI app: functions, HTTP routes and SQL, plus the CPU and waiting split only Python can measure.
La instalación está escrita como un prompt para el agente de código que ya tienes abierto, no como una lista de tareas para ti. Es deliberado: nombra lo que tiene que ser cierto cuando la instalación esté terminada en vez de qué archivos editar, porque dónde va el código depende del framework y ponerlo en el sitio equivocado falla en silencio. Un agente puede leer tu repositorio y deducirlo; un párrafo en una página de documentación no.
El mismo texto es lo que devuelve install_sixty a través de el servidor MCP y lo que el colector sirve en /v1/setup?kind=python. Hay una sola copia.
la instalación de Python, completa
Install the sixty agent in this Python service so its functions, HTTP routes
and database queries report to sixty.
Before editing, inspect whether this service makes LLM or agent calls. If it
does, ask the user: "Do you want LLM monitoring?" If yes, use sixty.agent,
sixty.generation and sixty.tool around custom orchestration, and call
sixty.instrument_openai(client) or sixty.instrument_anthropic(client) for
those SDKs. Never record prompts, outputs, tool arguments or tool results.
1. Add sixty-sh to this project's runtime dependencies — the same place the
web framework is declared (pyproject.toml, requirements.txt, Pipfile), not
a dev or test group. It runs in production; that is the entire point. It
has no dependencies of its own.
2. Call sixty.init() once, as early in process startup as you can get it, and
before any database connection is opened. Put it at the top of the module
the deployed process actually starts — wsgi.py, asgi.py, main.py, manage.py
for a management command — not inside a function that runs per request.
3. Wrap the application so requests become operations. Work out which of these
this project is; do exactly one:
a. Flask — call instrument_flask(app) from "sixty.instrument.flask" after
the app and its routes exist. It wraps app.wsgi_app and names operations
by the matched url_rule.
b. Django — put "sixty.instrument.django.SixtyMiddleware" FIRST in the
MIDDLEWARE list, so the span covers the rest of the middleware rather
than sitting inside it. Operations are named by the route as written in
urls.py.
c. FastAPI, Starlette, Litestar, Quart, or anything else ASGI — wrap with
SixtyASGIMiddleware from "sixty.instrument.asgi".
d. Any other WSGI application — wrap the WSGI callable with SixtyMiddleware
from "sixty.instrument.wsgi".
4. Mark the functions worth measuring. This step is what turns "this endpoint
got slow" into "this function started issuing 14 queries", and skipping it
leaves the feed with routes and queries and nothing in between:
- Put @sixty.trace on the functions that do the work — the service layer,
the repository, whatever this project calls the code between the view and
the database. Not on view functions the middleware already covers.
- Or, for a module of them, call sixty.instrument_module(sys.modules[__name__])
at the bottom of the file; it wraps every public function that module
defines and leaves imported ones alone.
- Leave anything called hundreds of thousands of times a second alone. A
span costs a couple of microseconds, which is nothing next to a request
and everything next to a tight inner loop.
5. Set these environment variables wherever the service is deployed:
SIXTY_API_KEY = a secret key starting sixty_sk_ — ask me for it. Do not
invent one, and do not commit it.
SIXTY_SERVICE = my-app
SIXTY_ENDPOINT = https://ingest.sixty.sh
The release identifier is picked up automatically on Vercel, Render,
Railway, Fly, Heroku and GitHub Actions. If this deploys some other way,
set SIXTY_RELEASE to the commit SHA — without one, every measurement lands
in a single nameless bucket and no comparison can ever be made.
Constraints — correctness requirements, not style preferences:
- Do NOT change any application behaviour. This is instrumentation only: no
refactors, no reordering of business logic, no "while I was in here" fixes.
- Do NOT call init() at import time in a module that is also imported by test
collection or by a build step. Without a key it is inert, but a flush thread
started in a test runner is a surprise nobody asked for.
- Do NOT wrap generators or async generators with @sixty.trace. Their work
happens between next() calls, so the measurement would be of constructing an
object. Wrap whatever drains them.
- Do NOT add any analytics, user id, session id, or cookie to what is
reported. The agent is deliberately anonymous and must stay that way.
- Queries are instrumented through psycopg (2 and 3) automatically. If this
project talks to its database some other way, tell me rather than wiring
something up — measuring it may need work in the agent.
If this service runs under gunicorn, uwsgi or any pre-fork server, note how
many workers it runs: each one reports independently and the collector merges
them, which is correct, but it is worth knowing when you read the numbers.
When you are done, tell me which files you changed and what the deployed start
command now is, so I can confirm data is arriving.Necesita una clave secreta — empieza por sixty_sk_ y se queda en el servidor. Genera una en la página de Ajustes cuando hayas iniciado sesión.
Qué mide
| señal | unidad | qué significa |
|---|---|---|
rows | rows per call | this query returns more rows than it used to |
fanout | queries per call | this operation now issues more database calls per invocation — an N+1 |
latency | ms per call | this operation takes longer end to end than it used to |
self_latency | ms per call | the time spent in this function itself got longer — its children did not |
payload | bytes per call | the serialized result of this operation got bigger |
errors | error rate | a larger fraction of calls are throwing |
runaway | calls per minute | this operation is being called far more often than anything triggers it |
repeated_query | times per request | the identical query runs several times within one request |
overfetch | rows per call | far more rows are fetched than the code appears to use |
unbounded | rows per call | this query has no upper bound on what it can return |
recursion | levels deep | this operation calls itself, deeper than it should |
new_error | occurrences | an error that did not occur in the previous release |
missing_tenancy | — | This reads a table of per-person data without saying whose rows it wants. Unless your database is filtering it for you, everyone gets everyone else's. |
collapse | — | This is handing back roughly half the data it used to, or less. If that was not deliberate, something is filtering out rows that somebody expects to see. |
vanished | — | It was being used steadily until this release and has not been used once since. Usually the link, button, or redirect that led here stopped working. |
traffic_drop | — | This is still being used, but a fraction as often, and its share of your traffic fell too — so it is not just a quiet period. |
cpu | ms of CPU per call | this function burns more processor time per call than it used to — it is doing more work, not waiting longer |
blocked | ms of waiting per call | this operation spends longer waiting for its turn while doing exactly the same amount of work |
Dónde se engancha
- Flask — instrument_flask(app) — las operaciones se nombran por la url_rule que casó.
- Django — SixtyMiddleware, el primero en MIDDLEWARE — nombradas por la ruta tal y como está escrita en urls.py.
- FastAPI, Starlette, Litestar, Quart — SixtyASGIMiddleware, o cualquier otra aplicación ASGI.
- Cualquier cosa WSGI — Pyramid, Bottle, wsgiref, un framework que todavía no ha escrito nadie.
- Tus propias funciones — @sixty.trace en la capa de servicio, o instrument_module() para un módulo entero de una vez.
Bases de datos
- psycopg — Versiones 2 y 3, parcheado en el cursor. Filas, forma de la sentencia y atribución a la función que la llamó.
Qué hace solo este
- CPU por llamada — time.thread_time() es por hilo y cuesta unos 100 ns leerlo, y un span síncrono es dueño de su hilo durante toda su duración — así que el número es exacto en lugar de repartido.
- Espera por llamada — Tiempo propio que no fue computar: un lock retenido durante más trabajo, un pool sin hueco libre, una extensión en C reteniendo el GIL. Todos los demás números por llamada siguen siendo correctos mientras esto ocurre, y por eso no lo detecta nada más.
Qué no puede hacer
- Un span que abarca un await comparte su hilo con lo que sea que haya ejecutado el bucle, así que reporta cero CPU en lugar de una inflada. Un servicio asyncio obtiene CPU por función síncrona y por consulta, pero no por petición.
- No se capturan planes de consulta. Postgres está ahí y el EXPLAIN funcionaría; el agente todavía no lo emite.
- psycopg es el único driver instrumentado. SQLAlchemy sobre psycopg se mide, porque el cursor de debajo lo está; asyncpg y los drivers de MySQL no.
- Bajo gunicorn o uwsgi cada worker reporta por separado y el colector los fusiona. Eso es correcto, y conviene saberlo cuando leas un número por proceso.
Configuración
Todos los agentes leen las mismas cuatro variables, y DRIFT_* sigue respondiendo allí donde lo hace SIXTY_* — el producto se renombró y ese nombre no es nuestro para retirarlo de los despliegues de otra gente.
SIXTY_API_KEY | Sin ella el agente se queda inerte y lo dice. Nunca adivina, nunca reintenta contra un endpoint desconocido, y nunca lanza una excepción. |
|---|---|
SIXTY_SERVICE | Cómo llamar a este servicio. Por defecto, el nombre del proyecto donde sea legible. |
SIXTY_RELEASE | La que más importa. Se recoge automáticamente en Vercel, Render, Railway, Fly, Heroku y GitHub Actions; en cualquier otro sitio, ponla al SHA del commit. Sin ella cada medición cae en un único cubo sin nombre y no se puede hacer ninguna comparación. |
SIXTY_ENDPOINT | Dónde reportar. Por defecto http://localhost:4319, que es correcto en un portátil y erróneo en cuanto la aplicación se sirve a alguien más. |
El resto — intervalo de envío, tasa de muestreo, qué instrumentar — está en el propio README del paquete, que es donde puede seguir siendo cierto según cambia el agente.