Skip to content

Commit

Permalink
Merge pull request #33 from evo-company/add-instrumentation
Browse files Browse the repository at this point in the history
Add instrumentation
  • Loading branch information
n4mespace authored Feb 19, 2024
2 parents addac92 + ad79e4b commit b618dbd
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 85 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ RUN cd ui \
&& npm run build

FROM base as dev
RUN pdm install --no-lock -G dev -G lint --no-editable
RUN pdm install --no-lock -G dev -G sentry -G lint --no-editable

FROM base as test
RUN pdm install --no-lock -G test
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Installation

On PyPi: https://pypi.org/project/evo-featureflags-server

To install with Sentry integration:
`pip3 install evo-featureflags-server[sentry]`

To install client library follow instructions
here: [evo-featureflags-client](https://github.com/evo-company/featureflags-py)

Expand Down
8 changes: 8 additions & 0 deletions configs/local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ ldap:

instrumentation:
prometheus_port: 9100

sentry:
enabled: false
dsn: null
env: local
enable_tracing: true
traces_sample_rate: 1
shutdown_timeout: 1
8 changes: 8 additions & 0 deletions configs/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ ldap:

instrumentation:
prometheus_port: null

sentry:
enabled: false
dsn: null
env: local
enable_tracing: true
traces_sample_rate: 1
shutdown_timeout: 1
6 changes: 6 additions & 0 deletions featureflags/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import Annotated

import typer
Expand All @@ -6,6 +7,7 @@

configure_logging(__package__)

log = logging.getLogger(__name__)
cli = typer.Typer()


Expand All @@ -16,13 +18,15 @@
def alembic(args: Annotated[list[str], typer.Argument()]) -> None:
from featureflags.alembic import main as alembic_main

log.info("Executing command: `alembic`")
alembic_main(args)


@cli.command(name="web", help="Run web server")
def web() -> None:
from featureflags.web.app import main as web_main

log.info("Executing command: `web`")
web_main()


Expand All @@ -34,6 +38,7 @@ def rpc() -> None:

from featureflags.rpc.app import main as rpc_main

log.info("Executing command: `rpc`")
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
asyncio.run(rpc_main())

Expand All @@ -42,4 +47,5 @@ def rpc() -> None:
def http() -> None:
from featureflags.http.app import main as http_main

log.info("Executing command: `http`")
http_main()
10 changes: 10 additions & 0 deletions featureflags/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ class RpcSettings(BaseSettings):
host: str = "0.0.0.0"


class SentrySettings(BaseSettings):
enabled: bool = False
dsn: str | None = None
env: str | None = None
enable_tracing: bool = True
traces_sample_rate: float = 1.0
shutdown_timeout: int = 1


class Config(BaseSettings):
debug: bool
secret: str = Field(..., alias="SECRET")
Expand All @@ -80,6 +89,7 @@ class Config(BaseSettings):
ldap: LdapSettings
logging: LoggingSettings
instrumentation: InstrumentationSettings
sentry: SentrySettings

app: AppSettings
rpc: RpcSettings
Expand Down
66 changes: 3 additions & 63 deletions featureflags/graph/actions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from datetime import datetime
from uuid import UUID, uuid4
from uuid import UUID

from aiopg.sa import SAConnection
from sqlalchemy import and_, or_, select, update
from sqlalchemy import or_, select, update
from sqlalchemy.dialects.postgresql import insert

from featureflags.graph.constants import AUTH_SESSION_TTL
Expand All @@ -15,15 +15,13 @@
DirtyProjects,
LocalId,
)
from featureflags.graph.utils import update_map
from featureflags.graph.utils import gen_id, get_auth_user, update_map
from featureflags.models import (
AuthSession,
AuthUser,
Changelog,
Check,
Condition,
Flag,
LocalIdMap,
Operator,
Project,
Variable,
Expand All @@ -33,64 +31,6 @@
from featureflags.utils import select_scalar


async def gen_id(local_id: LocalId, *, conn: SAConnection) -> UUID:
assert local_id.scope and local_id.value, local_id

id_ = await select_scalar(
conn,
(
insert(LocalIdMap.__table__)
.values(
{
LocalIdMap.scope: local_id.scope,
LocalIdMap.value: local_id.value,
LocalIdMap.id: uuid4(),
LocalIdMap.timestamp: datetime.utcnow(),
}
)
.on_conflict_do_nothing()
.returning(LocalIdMap.id)
),
)
if id_ is None:
id_ = await select_scalar(
conn,
(
select([LocalIdMap.id]).where(
and_(
LocalIdMap.scope == local_id.scope,
LocalIdMap.value == local_id.value,
)
)
),
)
return id_


async def get_auth_user(username: str, *, conn: SAConnection) -> UUID:
user_id_select = select([AuthUser.id]).where(AuthUser.username == username)
user_id = await select_scalar(conn, user_id_select)
if user_id is None:
user_id = await select_scalar(
conn,
(
insert(AuthUser.__table__)
.values(
{
AuthUser.id: uuid4(),
AuthUser.username: username,
}
)
.on_conflict_do_nothing()
.returning(AuthUser.id)
),
)
if user_id is None:
user_id = await select_scalar(conn, user_id_select)
assert user_id is not None
return user_id


@track
async def sign_in(
username: str,
Expand Down
73 changes: 73 additions & 0 deletions featureflags/graph/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import uuid
from datetime import datetime
from uuid import UUID, uuid4

from aiopg.sa import SAConnection
from sqlalchemy import and_, select
from sqlalchemy.dialects.postgresql import insert

from featureflags.graph.types import (
LocalId,
)
from featureflags.models import (
AuthUser,
LocalIdMap,
)
from featureflags.utils import select_scalar


def is_valid_uuid(value: str) -> bool:
Expand All @@ -14,3 +29,61 @@ def update_map(map_: dict, update: dict) -> dict:
map_ = map_.copy()
map_.update(update)
return map_


async def gen_id(local_id: LocalId, *, conn: SAConnection) -> UUID:
assert local_id.scope and local_id.value, local_id

id_ = await select_scalar(
conn,
(
insert(LocalIdMap.__table__)
.values(
{
LocalIdMap.scope: local_id.scope,
LocalIdMap.value: local_id.value,
LocalIdMap.id: uuid4(),
LocalIdMap.timestamp: datetime.utcnow(),
}
)
.on_conflict_do_nothing()
.returning(LocalIdMap.id)
),
)
if id_ is None:
id_ = await select_scalar(
conn,
(
select([LocalIdMap.id]).where(
and_(
LocalIdMap.scope == local_id.scope,
LocalIdMap.value == local_id.value,
)
)
),
)
return id_


async def get_auth_user(username: str, *, conn: SAConnection) -> UUID:
user_id_select = select([AuthUser.id]).where(AuthUser.username == username)
user_id = await select_scalar(conn, user_id_select)
if user_id is None:
user_id = await select_scalar(
conn,
(
insert(AuthUser.__table__)
.values(
{
AuthUser.id: uuid4(),
AuthUser.username: username,
}
)
.on_conflict_do_nothing()
.returning(AuthUser.id)
),
)
if user_id is None:
user_id = await select_scalar(conn, user_id_select)
assert user_id is not None
return user_id
8 changes: 7 additions & 1 deletion featureflags/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ def create_app() -> FastAPI:

set_internal_user_session()

configure_metrics(port=config.instrumentation.prometheus_port)
configure_metrics(port=config.instrumentation.prometheus_port, app=app)
configure_lifecycle(app, container)

if config.sentry.enabled:
from featureflags.sentry import configure_sentry

configure_sentry(config.sentry, env_prefix="http", app=app)

return app


Expand All @@ -41,4 +46,5 @@ def main() -> None:
loop="uvloop",
http="httptools",
reload=config.http.reload,
log_level="debug" if config.debug else "info",
)
6 changes: 3 additions & 3 deletions featureflags/http/repositories/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __init__(

self._entity_cache = EntityCache()

async def get_current_version(self, project: str) -> int:
async def get_project_version(self, project: str) -> int:
async with self._db_engine.acquire() as conn:
return await select_scalar(
conn,
Expand Down Expand Up @@ -88,7 +88,7 @@ async def load(self, request: PreloadFlagsRequest) -> PreloadFlagsResponse:

await self.prepare_project(request)

current_version = await self.get_current_version(request.project)
current_version = await self.get_project_version(request.project)

result = await exec_denormalize_graph(
graph_engine=self._graph_engine,
Expand All @@ -109,7 +109,7 @@ async def sync(self, request: SyncFlagsRequest) -> SyncFlagsResponse:
is different from the requested one.
"""

current_version = await self.get_current_version(request.project)
current_version = await self.get_project_version(request.project)

if request.version != current_version:
result = await exec_denormalize_graph(
Expand Down
5 changes: 5 additions & 0 deletions featureflags/http/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def check_and_assign_value(
cls, # noqa: N805
values: dict[str, Any],
) -> dict[str, Any]:
"""
Value can be any type from `CHECK_VALUE_FIELDS`, but we want
to find one that is not not and assign to `Check.value`.
"""

for field in CHECK_VALUE_FIELDS:
if field in values and values[field] is not None:
values["value"] = values[field]
Expand Down
2 changes: 0 additions & 2 deletions featureflags/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

from featureflags.config import config

log = logging.getLogger(__name__)


def create_console_handler() -> StreamHandler:
handler = StreamHandler()
Expand Down
21 changes: 20 additions & 1 deletion featureflags/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from contextlib import AbstractContextManager
from typing import Any

from fastapi import FastAPI
from prometheus_client import start_http_server
from prometheus_client.decorator import decorator as prometheus_decorator
from prometheus_fastapi_instrumentator import Instrumentator

log = logging.getLogger(__name__)

Expand All @@ -17,9 +19,26 @@ async def wrapper(fn: Callable, *args: Any, **kwargs: Any) -> None:
return prometheus_decorator(wrapper)


def configure_metrics(port: int | None = None) -> None:
def configure_metrics(
port: int | None = None,
app: FastAPI | None = None,
) -> None:
if port:
start_http_server(port=port)
log.info("Prometheus metrics initialized")

if app:
instrumentator = Instrumentator(
should_instrument_requests_inprogress=True,
inprogress_labels=True,
excluded_handlers=["/metrics", "/~health"],
)
instrumentator.instrument(
app=app,
latency_lowr_buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1,
0.25, 0.5],
)
log.info("Http instrumentation initialized")

else:
log.info("Prometheus metrics disabled")
Loading

0 comments on commit b618dbd

Please sign in to comment.