Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add instrumentation #33

Merged
merged 3 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading