mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
major caching improvements
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
# Standard Library
|
# Standard Library
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import BaseModel, StrictStr, validator
|
from pydantic import BaseModel, StrictStr, validator
|
||||||
@@ -57,6 +59,7 @@ class Query(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
"""Pydantic model configuration."""
|
"""Pydantic model configuration."""
|
||||||
|
|
||||||
|
extra = "allow"
|
||||||
fields = {
|
fields = {
|
||||||
"query_location": {
|
"query_location": {
|
||||||
"title": params.web.text.query_location,
|
"title": params.web.text.query_location,
|
||||||
@@ -83,10 +86,29 @@ class Query(BaseModel):
|
|||||||
"x-code-samples": [{"lang": "Python", "source": "print('stuff')"}]
|
"x-code-samples": [{"lang": "Python", "source": "print('stuff')"}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize the query with a UTC timestamp at initialization time."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Represent only the query fields."""
|
||||||
|
return (
|
||||||
|
f"Query(query_location={str(self.query_location)}, "
|
||||||
|
f"query_type={str(self.query_type)}, query_vrf={str(self.query_vrf)}, "
|
||||||
|
f"query_target={str(self.query_target)})"
|
||||||
|
)
|
||||||
|
|
||||||
def digest(self):
|
def digest(self):
|
||||||
"""Create SHA256 hash digest of model representation."""
|
"""Create SHA256 hash digest of model representation."""
|
||||||
return hashlib.sha256(repr(self).encode()).hexdigest()
|
return hashlib.sha256(repr(self).encode()).hexdigest()
|
||||||
|
|
||||||
|
def random(self):
|
||||||
|
"""Create a random string to prevent client or proxy caching."""
|
||||||
|
return hashlib.sha256(
|
||||||
|
secrets.token_bytes(8) + repr(self).encode() + secrets.token_bytes(8)
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self):
|
def summary(self):
|
||||||
"""Create abbreviated representation of instance."""
|
"""Create abbreviated representation of instance."""
|
||||||
|
@@ -57,10 +57,11 @@ class QueryResponse(BaseModel):
|
|||||||
|
|
||||||
output: StrictStr
|
output: StrictStr
|
||||||
level: constr(regex=r"success") = "success"
|
level: constr(regex=r"success") = "success"
|
||||||
id: StrictStr
|
random: StrictStr
|
||||||
cached: StrictBool
|
cached: StrictBool
|
||||||
runtime: StrictInt
|
runtime: StrictInt
|
||||||
keywords: List[StrictStr] = []
|
keywords: List[StrictStr] = []
|
||||||
|
timestamp: StrictStr
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Pydantic model configuration."""
|
"""Pydantic model configuration."""
|
||||||
@@ -69,6 +70,25 @@ class QueryResponse(BaseModel):
|
|||||||
description = "Looking glass response"
|
description = "Looking glass response"
|
||||||
fields = {
|
fields = {
|
||||||
"level": {"title": "Level", "description": "Severity"},
|
"level": {"title": "Level", "description": "Severity"},
|
||||||
|
"cached": {
|
||||||
|
"title": "Cached",
|
||||||
|
"description": "`true` if the response is from a previously cached query.",
|
||||||
|
},
|
||||||
|
"random": {
|
||||||
|
"title": "Random",
|
||||||
|
"description": "Random string to prevent client or intermediate caching.",
|
||||||
|
"example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857",
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"title": "Runtime",
|
||||||
|
"description": "Time it took to run the query in seconds.",
|
||||||
|
"example": 6,
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"title": "Timestamp",
|
||||||
|
"description": "UTC Time at which the backend application received the query.",
|
||||||
|
"example": "2020-04-18 14:45:37",
|
||||||
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"title": "Keywords",
|
"title": "Keywords",
|
||||||
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
|
||||||
|
@@ -71,7 +71,7 @@ async def query(query_data: Query, request: Request):
|
|||||||
|
|
||||||
log.info(f"Starting query execution for query {query_data.summary}")
|
log.info(f"Starting query execution for query {query_data.summary}")
|
||||||
|
|
||||||
cache_response = await cache.get(cache_key)
|
cache_response = await cache.get_dict(cache_key, "output")
|
||||||
|
|
||||||
cached = False
|
cached = False
|
||||||
if cache_response:
|
if cache_response:
|
||||||
@@ -80,6 +80,7 @@ async def query(query_data: Query, request: Request):
|
|||||||
|
|
||||||
cached = True
|
cached = True
|
||||||
runtime = 0
|
runtime = 0
|
||||||
|
timestamp = await cache.get_dict(cache_key, "timestamp")
|
||||||
|
|
||||||
elif not cache_response:
|
elif not cache_response:
|
||||||
log.debug(f"No existing cache entry for query {cache_key}")
|
log.debug(f"No existing cache entry for query {cache_key}")
|
||||||
@@ -87,18 +88,20 @@ async def query(query_data: Query, request: Request):
|
|||||||
f"Created new cache key {cache_key} entry for query {query_data.summary}"
|
f"Created new cache key {cache_key} entry for query {query_data.summary}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
timestamp = query_data.timestamp
|
||||||
# Pass request to execution module
|
# Pass request to execution module
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
cache_value = await Execute(query_data).response()
|
cache_output = await Execute(query_data).response()
|
||||||
endtime = time.time()
|
endtime = time.time()
|
||||||
elapsedtime = round(endtime - starttime, 4)
|
elapsedtime = round(endtime - starttime, 4)
|
||||||
log.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
|
log.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
|
||||||
|
|
||||||
if cache_value is None:
|
if cache_output is None:
|
||||||
raise HyperglassError(message=params.messages.general, alert="danger")
|
raise HyperglassError(message=params.messages.general, alert="danger")
|
||||||
|
|
||||||
# Create a cache entry
|
# Create a cache entry
|
||||||
await cache.set(cache_key, str(cache_value))
|
await cache.set_dict(cache_key, "output", str(cache_output))
|
||||||
|
await cache.set_dict(cache_key, "timestamp", timestamp)
|
||||||
await cache.expire(cache_key, seconds=cache_timeout)
|
await cache.expire(cache_key, seconds=cache_timeout)
|
||||||
|
|
||||||
log.debug(f"Added cache entry for query: {cache_key}")
|
log.debug(f"Added cache entry for query: {cache_key}")
|
||||||
@@ -106,7 +109,7 @@ async def query(query_data: Query, request: Request):
|
|||||||
runtime = int(round(elapsedtime, 0))
|
runtime = int(round(elapsedtime, 0))
|
||||||
|
|
||||||
# If it does, return the cached entry
|
# If it does, return the cached entry
|
||||||
cache_response = await cache.get(cache_key)
|
cache_response = await cache.get_dict(cache_key, "output")
|
||||||
|
|
||||||
log.debug(f"Cache match for {cache_key}:\n {cache_response}")
|
log.debug(f"Cache match for {cache_key}:\n {cache_response}")
|
||||||
log.success(f"Completed query execution for {query_data.summary}")
|
log.success(f"Completed query execution for {query_data.summary}")
|
||||||
@@ -116,6 +119,8 @@ async def query(query_data: Query, request: Request):
|
|||||||
"id": cache_key,
|
"id": cache_key,
|
||||||
"cached": cached,
|
"cached": cached,
|
||||||
"runtime": runtime,
|
"runtime": runtime,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"random": query_data.random(),
|
||||||
"level": "success",
|
"level": "success",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
}
|
}
|
||||||
|
@@ -71,10 +71,22 @@ class Cache:
|
|||||||
raw = await self.instance.mget(args)
|
raw = await self.instance.mget(args)
|
||||||
return await self._parse_types(raw)
|
return await self._parse_types(raw)
|
||||||
|
|
||||||
|
async def get_dict(self, key, field=None):
|
||||||
|
"""Get hash map (dict) item(s)."""
|
||||||
|
if field is None:
|
||||||
|
raw = await self.instance.hgetall(key)
|
||||||
|
else:
|
||||||
|
raw = await self.instance.hget(key, field)
|
||||||
|
return await self._parse_types(raw)
|
||||||
|
|
||||||
async def set(self, key, value):
|
async def set(self, key, value):
|
||||||
"""Set cache values."""
|
"""Set cache values."""
|
||||||
return await self.instance.set(key, value)
|
return await self.instance.set(key, value)
|
||||||
|
|
||||||
|
async def set_dict(self, key, field, value):
|
||||||
|
"""Set hash map (dict) values."""
|
||||||
|
return await self.instance.hset(key, field, value)
|
||||||
|
|
||||||
async def wait(self, pubsub, timeout=30, **kwargs):
|
async def wait(self, pubsub, timeout=30, **kwargs):
|
||||||
"""Wait for pub/sub messages & return posted message."""
|
"""Wait for pub/sub messages & return posted message."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
@@ -139,7 +139,7 @@ class Text(HyperglassModel):
|
|||||||
query_vrf: StrictStr = "Routing Table"
|
query_vrf: StrictStr = "Routing Table"
|
||||||
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
|
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
|
||||||
cache_prefix: StrictStr = "Results cached for "
|
cache_prefix: StrictStr = "Results cached for "
|
||||||
cache_icon: StrictStr = "Cached Response"
|
cache_icon: StrictStr = "Cached Response from {time}" # Formatted by Javascript
|
||||||
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
|
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
|
||||||
|
|
||||||
@validator("title_mode")
|
@validator("title_mode")
|
||||||
|
@@ -16,6 +16,7 @@ import styled from "@emotion/styled";
|
|||||||
import LightningBolt from "~/components/icons/LightningBolt";
|
import LightningBolt from "~/components/icons/LightningBolt";
|
||||||
import useAxios from "axios-hooks";
|
import useAxios from "axios-hooks";
|
||||||
import strReplace from "react-string-replace";
|
import strReplace from "react-string-replace";
|
||||||
|
import format from "string-format";
|
||||||
import { startCase } from "lodash";
|
import { startCase } from "lodash";
|
||||||
import useConfig from "~/components/HyperglassProvider";
|
import useConfig from "~/components/HyperglassProvider";
|
||||||
import useMedia from "~/components/MediaProvider";
|
import useMedia from "~/components/MediaProvider";
|
||||||
@@ -24,6 +25,8 @@ import RequeryButton from "~/components/RequeryButton";
|
|||||||
import ResultHeader from "~/components/ResultHeader";
|
import ResultHeader from "~/components/ResultHeader";
|
||||||
import CacheTimeout from "~/components/CacheTimeout";
|
import CacheTimeout from "~/components/CacheTimeout";
|
||||||
|
|
||||||
|
format.extend(String.prototype, {});
|
||||||
|
|
||||||
const FormattedError = ({ keywords, message }) => {
|
const FormattedError = ({ keywords, message }) => {
|
||||||
const patternStr = keywords.map((kw) => `(${kw})`).join("|");
|
const patternStr = keywords.map((kw) => `(${kw})`).join("|");
|
||||||
const pattern = new RegExp(patternStr, "gi");
|
const pattern = new RegExp(patternStr, "gi");
|
||||||
@@ -125,7 +128,11 @@ const Result = React.forwardRef(
|
|||||||
<>
|
<>
|
||||||
<CacheTimeout timeout={config.cache.timeout} text={config.web.text.cache_prefix} />
|
<CacheTimeout timeout={config.cache.timeout} text={config.web.text.cache_prefix} />
|
||||||
{data?.cached && (
|
{data?.cached && (
|
||||||
<Tooltip hasArrow label={config.web.text.cache_icon} placement="top">
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Box ml={1}>
|
<Box ml={1}>
|
||||||
<LightningBolt color={color[colorMode]} />
|
<LightningBolt color={color[colorMode]} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -136,7 +143,11 @@ const Result = React.forwardRef(
|
|||||||
const cacheSm = (
|
const cacheSm = (
|
||||||
<>
|
<>
|
||||||
{data?.cached && (
|
{data?.cached && (
|
||||||
<Tooltip hasArrow label={config.web.text.cache_icon} placement="top">
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Box mr={1}>
|
<Box mr={1}>
|
||||||
<LightningBolt color={color[colorMode]} />
|
<LightningBolt color={color[colorMode]} />
|
||||||
</Box>
|
</Box>
|
||||||
|
@@ -7,7 +7,7 @@ format.extend(String.prototype, {});
|
|||||||
|
|
||||||
const runtimeText = (runtime, text) => {
|
const runtimeText = (runtime, text) => {
|
||||||
let unit;
|
let unit;
|
||||||
if (runtime > 1) {
|
if (runtime === 1) {
|
||||||
unit = "seconds";
|
unit = "seconds";
|
||||||
} else {
|
} else {
|
||||||
unit = "second";
|
unit = "second";
|
||||||
|
Reference in New Issue
Block a user