Skip to content
Draft
1 change: 0 additions & 1 deletion .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
{"lib/logflare_web/controllers/api/endpoint_controller.ex", :pattern_match},
{"lib/logflare_web/controllers/billing_controller.ex", :unused_fun},
{"lib/logflare_web/controllers/billing_controller.ex", :pattern_match_cov},
{"lib/logflare_web/controllers/health_check_controller.ex", :pattern_match},
{"lib/logflare_web/controllers/source_controller.ex", :pattern_match},
{"lib/logflare_web/live/monaco_editor_component.ex", :pattern_match},
{"lib/logflare_web/live/query_live.ex", :no_return},
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ gate to go green - fix the code.
that have pre-existing findings. They are off so the gate is green on existing
code. Fixing a backlog check's findings and removing it from that list is a
welcome change on its own. Never add a check to the list to go green.
- `mix test.slop` runs `ex_dna --max-clones 29`, a duplication ratchet. A new
clone fails CI. When you remove clones, lower the number. Never raise it.
- `mix test.slop` runs `ex_dna --max-clones <Current clone number>`,
This tool counts duplicated code (clones), and the number should only go down,
never up. If you add a new clone, CI fails. If you remove clones,
lower the number. Never raise it.
- `mix test.structure` runs `reach.check --smells` against
`.reach.baseline.json` (194 accepted findings), so only **new** structural
smells fail. To accept a
Expand Down
32 changes: 12 additions & 20 deletions lib/logflare/auth/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,25 @@ defmodule Logflare.Auth.Cache do
Cachex `expiration`.
"""

@behaviour Logflare.Cache
@behaviour Logflare.ContextCache

alias Logflare.Auth
alias Logflare.Cache.CachexOps
alias Logflare.OauthAccessTokens.OauthAccessToken
alias Logflare.User
alias Logflare.Utils

def child_spec(_) do
stats = Application.get_env(:logflare, :cache_stats, false)

%{
id: __MODULE__,
start:
{Cachex, :start_link,
[
__MODULE__,
[
hooks:
[
if(stats, do: Utils.cache_stats()),
Utils.cache_limit(100_000)
]
|> Enum.filter(& &1),
expiration: Utils.cache_expiration_min(5, 2)
]
]}
}
CachexOps.child_spec(__MODULE__,
limit: 100_000,
ttl: to_timeout(minute: 5),
purge_interval: to_timeout(minute: 2)
)
end

@impl Logflare.Cache
def healthy?, do: CachexOps.healthy?(__MODULE__)

@spec verify_access_token(OauthAccessToken.t() | String.t()) ::
{:ok, OauthAccessToken.t(), User.t()} | {:error, term()}
def verify_access_token(access_token_or_api_key),
Expand Down
32 changes: 8 additions & 24 deletions lib/logflare/backends/cache.ex
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
defmodule Logflare.Backends.Cache do
@moduledoc false

@behaviour Logflare.Cache
@behaviour Logflare.ContextCache

alias Logflare.Backends
alias Logflare.Utils
import Cachex.Spec
alias Logflare.Cache.CachexOps

def child_spec(_) do
stats = Application.get_env(:logflare, :cache_stats, false)

%{
id: __MODULE__,
start:
{Cachex, :start_link,
[
__MODULE__,
[
warmers: [
warmer(required: false, module: Backends.CacheWarmer, name: Backends.CacheWarmer)
],
hooks:
[
if(stats, do: Utils.cache_stats()),
Utils.cache_limit(100_000)
]
|> Enum.filter(& &1),
expiration: Utils.cache_expiration_min()
]
]}
}
CachexOps.child_spec(__MODULE__, limit: 100_000, warmer: Backends.CacheWarmer)
end

@impl Logflare.Cache
def healthy?, do: CachexOps.healthy?(__MODULE__)

def list_backends(arg), do: apply_repo_fun(__ENV__.function, [arg])
def get_backend(arg), do: apply_repo_fun(__ENV__.function, [arg])

Expand Down
32 changes: 12 additions & 20 deletions lib/logflare/billing/cache.ex
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
defmodule Logflare.Billing.Cache do
@moduledoc false

@behaviour Logflare.Cache
@behaviour Logflare.ContextCache

alias Logflare.Billing
alias Logflare.Utils
alias Logflare.Cache.CachexOps

def child_spec(_) do
stats = Application.get_env(:logflare, :cache_stats, false)

%{
id: __MODULE__,
start:
{Cachex, :start_link,
[
__MODULE__,
[
hooks:
[
if(stats, do: Utils.cache_stats()),
Utils.cache_limit(100_000)
]
|> Enum.filter(& &1),
expiration: Utils.cache_expiration_min(180, 10)
]
]}
}
CachexOps.child_spec(__MODULE__,
limit: 100_000,
ttl: to_timeout(hour: 3),
purge_interval: to_timeout(minute: 10)
)
end

@impl Logflare.Cache
def healthy?, do: CachexOps.healthy?(__MODULE__)

def get_billing_account_by(keyword) do
apply_fun(__ENV__.function, [keyword])
end
Expand Down
54 changes: 54 additions & 0 deletions lib/logflare/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
defmodule Logflare.Cache do
@moduledoc """
Operational contract of an application cache, independent of its storage backend.

Every callback is optional. Generic code calls the functions of this module, which fall back to
`Logflare.Cache.CachexOps` for callbacks the cache does not implement.
"""

alias Logflare.Cache.CachexOps

@typedoc """
Counters since the last `c:reset/0`. Rates are percentages (0-100); `total_heap_size` is in bytes.
"""
@type stats() :: %{
evictions: non_neg_integer(),
expirations: non_neg_integer(),
operations: non_neg_integer(),
hits: non_neg_integer(),
misses: non_neg_integer(),
hit_rate: number(),
miss_rate: number(),
total_heap_size: non_neg_integer()
}

@doc "Whether the cache on this node can serve requests."
@callback healthy?() :: boolean()

@callback stats() :: stats()

@doc "Clears all entries and statistics."
@callback reset() :: :ok

@optional_callbacks healthy?: 0, stats: 0, reset: 0

@spec healthy?(module()) :: boolean()
def healthy?(cache), do: dispatch(cache, :healthy?, [])

@spec stats(module()) :: stats()
def stats(cache), do: dispatch(cache, :stats, [])

@spec reset(module()) :: :ok
def reset(cache), do: dispatch(cache, :reset, [])

@doc """
Calls `fun` on `cache` when the cache implements it, otherwise the `Logflare.Cache.CachexOps`
function of the same name with `cache` prepended to `args`.
"""
@spec dispatch(module(), atom(), list()) :: term()
def dispatch(cache, fun, args) do
if Code.ensure_loaded?(cache) and function_exported?(cache, fun, length(args)),
do: apply(cache, fun, args),
else: apply(CachexOps, fun, [cache | args])
end
end
Loading
Loading