diff --git a/README.md b/README.md index 7e9ab81..490f933 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,11 @@

-**SQLog** is a **Golang** library that leverages the power of **slog** to streamline log management in a simple and efficient way. +**SQLog** is a **Golang** library that simplifies log management using **slog**. **SQLog** offers a lightweight and reliable solution for logging, making it perfect for developers seeking a cost-effective, high-performance way to monitor their applications. -One of its key advantages is its integration with **SQLite**, an embedded database, meaning there's no need for an external database server setup. This reduces operational costs and drastically simplifies system maintenance, as everything runs **embedded**, without complex dependencies. +It integrates with **SQLite**, so no external database setup is required. This keeps operations simple and reduces costs, as everything runs **embedded**. -With **SQLog**, you get a lightweight and robust solution for securely logging and storing data. It's ideal for developers looking for a practical, cost-effective, and reliable way to monitor their applications, without sacrificing performance. -## Requirements - -The builtin SQLite storage uses the package `github.com/mattn/go-sqlite3`, which is a cgo package, so you need `gcc`. - -See the link for more details: [https://github.com/mattn/go-sqlite3?tab=readme-ov-file#installation](https://github.com/mattn/go-sqlite3?tab=readme-ov-file#installation) ## Usage @@ -24,8 +18,6 @@ Below is an example of using **SQLog** with the SQLite storage, with the interfa You can view the generated logs at `http://localhost:8080/logs/`. -> **Demo** You can see a [complete example in the demo directory](./demo/). - ```go import ( "log/slog" @@ -80,16 +72,19 @@ func main() { ## Demo -[SQLog-demo.webm](https://github.com/user-attachments/assets/046b65c9-dd36-4779-8b15-915be5f7e3f3) - -There is a test project in the [demo directory](./demo/). +You can view the current version of the SQLog demo at the links below: The demo project captures all mouse movements and sends them to the server, which logs the received parameters. -You can also view the current version of the SQLog demo at the links below: +- **Log generator**: https://sqlog-demo.onrender.com/ +- **SQLog UI** : https://sqlog-demo.onrender.com/logs/ + + > **IMPORTANT**! I am using the [free version of Render](https://docs.render.com/free), so there is no guarantee of service stability or availability. + +[SQLog-demo.webm](https://github.com/user-attachments/assets/046b65c9-dd36-4779-8b15-915be5f7e3f3) + +The source code is in the [demo directory](./demo/). -- Log generator (see console): https://sqlog-demo.onrender.com/ -- **SQLog** UI: https://sqlog-demo.onrender.com/logs/
@@ -97,10 +92,7 @@ You can also view the current version of the SQLog demo at the links below:
-> **IMPORTANT**! I am using the [free version of Render](https://docs.render.com/free), so there is no guarantee of service stability or availability. - - -## How it works +## How SQLog works?
@@ -134,6 +126,11 @@ You can also view the current version of the SQLog demo at the links below: The combination of these layers makes **SQLog** a robust and efficient solution for log management, optimizing performance through a non-blocking architecture and the use of atomic operations. This results in fast, real-time log capture capable of handling high workloads without compromising efficiency. +## Requirements + +The builtin SQLite storage uses the package `github.com/mattn/go-sqlite3`, which is a cgo package, so you need `gcc`. + +See the link for more details: [https://github.com/mattn/go-sqlite3?tab=readme-ov-file#installation](https://github.com/mattn/go-sqlite3?tab=readme-ov-file#installation) ## @TODO/IDEAS/ROADMAP diff --git a/demo/README.md b/demo/README.md index f3f36c6..f6f3580 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,9 +1,25 @@ # SQLog Demo + +You can view the current version of the SQLog demo at the links below: + +The demo project captures all mouse movements and sends them to the server, which logs the received parameters. + +- **Log generator**: https://sqlog-demo.onrender.com/ +- **SQLog UI** : https://sqlog-demo.onrender.com/logs/ + + > **IMPORTANT**! I am using the [free version of Render](https://docs.render.com/free), so there is no guarantee of service stability or availability. + [SQLog-demo.webm](https://github.com/user-attachments/assets/046b65c9-dd36-4779-8b15-915be5f7e3f3) +
+ +
+ + + ## Build Docker diff --git a/demo/go.mod b/demo/_go.mod similarity index 100% rename from demo/go.mod rename to demo/_go.mod diff --git a/demo/main.go b/demo/main.go index 9797cc8..4dc8212 100644 --- a/demo/main.go +++ b/demo/main.go @@ -9,12 +9,14 @@ import ( "path" "strings" + // "sqlog" + "github.com/nidorx/sqlog" "github.com/nidorx/sqlog/sqlite" ) var ( - dev = false + dev = true log sqlog.Log diff --git a/expr_parse_state.go b/expr_parse_state.go index 0566722..dad7ab8 100644 --- a/expr_parse_state.go +++ b/expr_parse_state.go @@ -21,7 +21,7 @@ type exprParseState[E any] struct { func (s *exprParseState[E]) addOperator() { if s.dirty { if s.operator == "" { - s.builder.Operator("OR") + s.builder.Operator("AND") } else { s.builder.Operator(s.operator) } diff --git a/expr_test.go b/expr_test.go index 4250987..20aea60 100644 --- a/expr_test.go +++ b/expr_test.go @@ -84,11 +84,11 @@ func Test_ExprBasic(t *testing.T) { }, { "hello world", - []any{"msg", "LIKE", "hello", "OR", "msg", "LIKE", "world"}, + []any{"msg", "LIKE", "hello", "AND", "msg", "LIKE", "world"}, }, { "hello* *world", - []any{"msg", "LIKE", "hello*", "OR", "msg", "LIKE", "*world"}, + []any{"msg", "LIKE", "hello*", "AND", "msg", "LIKE", "*world"}, }, { `"hello world"`, @@ -112,11 +112,11 @@ func Test_ExprBasic(t *testing.T) { }, { "field:hello world", - []any{"field", "LIKE", "hello", "OR", "msg", "LIKE", "world"}, + []any{"field", "LIKE", "hello", "AND", "msg", "LIKE", "world"}, }, { "field:hello* *world", - []any{"field", "LIKE", "hello*", "OR", "msg", "LIKE", "*world"}, + []any{"field", "LIKE", "hello*", "AND", "msg", "LIKE", "*world"}, }, { `field:"hello world"`, @@ -229,7 +229,7 @@ func Test_ExprBoolean(t *testing.T) { []any{"field", "LIKE", "hello", "OR", "msg", "LIKE", "world"}, }, { - "hello AND (beautiful world)", + "hello AND (beautiful OR world)", []any{"msg", "LIKE", "hello", "AND", "(", "msg", "LIKE", "beautiful", "OR", "msg", "LIKE", "world", ")"}, }, { diff --git a/sqlite/README.md b/sqlite/README.md index a6e7af4..b0ee0e3 100644 --- a/sqlite/README.md +++ b/sqlite/README.md @@ -1 +1,4 @@ -# SQLite Storage for SQLog \ No newline at end of file +# SQLite Storage for SQLog + + +@TODO: Docs \ No newline at end of file diff --git a/sqlite/expr_test.go b/sqlite/expr_test.go index fb11ef3..22b2b23 100644 --- a/sqlite/expr_test.go +++ b/sqlite/expr_test.go @@ -30,12 +30,12 @@ func Test_ExprBasic(t *testing.T) { }, { "hello world", - "json_extract(e.content, ?) GLOB ? OR json_extract(e.content, ?) GLOB ?", + "json_extract(e.content, ?) GLOB ? AND json_extract(e.content, ?) GLOB ?", []any{"$.msg", "*hello*", "$.msg", "*world*"}, }, { "hello* *world", - "json_extract(e.content, ?) GLOB ? OR json_extract(e.content, ?) GLOB ?", + "json_extract(e.content, ?) GLOB ? AND json_extract(e.content, ?) GLOB ?", []any{"$.msg", "hello*", "$.msg", "*world"}, }, { @@ -65,12 +65,12 @@ func Test_ExprBasic(t *testing.T) { }, { "field:hello world", - "json_extract(e.content, ?) GLOB ? OR json_extract(e.content, ?) GLOB ?", + "json_extract(e.content, ?) GLOB ? AND json_extract(e.content, ?) GLOB ?", []any{"$.field", "*hello*", "$.msg", "*world*"}, }, { "field:hello* *world", - "json_extract(e.content, ?) GLOB ? OR json_extract(e.content, ?) GLOB ?", + "json_extract(e.content, ?) GLOB ? AND json_extract(e.content, ?) GLOB ?", []any{"$.field", "hello*", "$.msg", "*world"}, }, { @@ -205,7 +205,7 @@ func Test_ExprBoolean(t *testing.T) { []any{"$.field", "*hello*", "$.msg", "*world*"}, }, { - "hello AND (beautiful world)", + "hello AND (beautiful OR world)", `json_extract(e.content, ?) GLOB ? AND (json_extract(e.content, ?) GLOB ? OR json_extract(e.content, ?) GLOB ?)`, []any{"$.msg", "*hello*", "$.msg", "*beautiful*", "$.msg", "*world*"}, }, diff --git a/web/index.html b/web/index.html index 09ff9eb..0bdf0bd 100644 --- a/web/index.html +++ b/web/index.html @@ -1,5 +1,5 @@ - + @@ -19,7 +19,7 @@
-
+
 
@@ -142,27 +142,220 @@ -
+
-
Search Syntax
+

Search Syntax

+
- @TODO: Documentar usando - https://docs.datadoghq.com/logs/explorer/search_syntax - https://raw.githubusercontent.com/DataDog/documentation/refs/heads/master/content/en/logs/explorer/search_syntax.md -
- + +

+ To combine multiple terms into a complex query, you can use any of the following case insensitive Boolean operators: +

+ + + + + + + + + + + + + + + + + + + + +
OperatorDescriptionExample
AND + Intersection: both terms are in the selected events (if nothing is added, AND is taken by default) + authentication AND failure
ORUnion: either term is contained in the selected eventsauthentication OR password
+ +

Search wildcard

+

+ You can combine text with wildcards to enhance your searches. +

+ + + + + + + + + + + + + + + + + +
WildcardDescription
? + Match a single special character or space. For example, to search + for an attribute my_attribute with the value hello world, my_attribute:hello?world. +
* + Perform a multi-character wildcard search. +
    +
  • + service:web* matches every log message that has a service starting with web. +
  • +
  • web* matches all log messages starting with web.
  • +
  • *web matches all log messages that end with web.
  • +
+
+

+ Note: Wildcards work as wildcards inside and outside of double quotes. For example, + "*test*" and *test* matches a log which has the string test in its message. + You cand escape the wildcard with with the \ character (example "\*test\*"). +

+ +

Single term example

+
+ + + + + + + + + + + + + + + + + + + + + +
Search syntaxDescription
helloSearches only the log message for the term hello.
hello*Searches all log attributes for strings that starts with hello. For example, hello_world.
*worldSearches all log attributes for strings that finishes with world. For example, hello_world.
+
+ +

Multiple terms with exact match example

+ + + + + + + + + + + + + +
Search syntaxDescription
"hello world"Searches only the log message for the exact term hello world.
+ +

Multiple terms without exact match example

+ + + + + + + + + + + + + + + + + +
Search syntaxDescription
hello world + Is equivalent to hello AND world. + It searches only the log message for the terms hello and world. +
"hello world" "i am here" + It searches all log attributes for the terms hello world and i am here. +
+ + + +

Attributes search

+

To search on a specific attribute, add : to specify you are searching on an attribute.

+

+ For instance, if your attribute name is url and you want to filter on the + url value https://github.com/nidorx/sqlog, enter: url:https://github.com/nidorx/sqlog +

+ + + + + + + + + + + + + + + + + + + + + + +
Search queryDescription
http.url_details.path:"/api/v1/test" + Searches all logs matching /api/v1/test in the attribute http.url_details.path. +
http.url:/api-v1/* + Searches all logs containing a value in http.url attribute that start with /api-v1/ +
http.status_code:[200 TO 299] http.url_details.path:/api-v1/* + Searches all logs containing a http.status_code value between 200 and 299, and + containing a value in http.url_details.path attribute that start with + /api-v1/ +
+ +

Numerical values

+

+ You can use numerical operators (<,>, <=, or >=) to + perform a search. + For instance, retrieve all logs that have a response time over 100ms with: http.response_time:>100 +

+

+ You can search for numerical attribute within a specific range. For instance, retrieve all your 4xx errors with: http.status_code:[400 TO 499] +

+
diff --git a/web/main.js b/web/main.js index 88f4fa3..70284fc 100644 --- a/web/main.js +++ b/web/main.js @@ -250,9 +250,9 @@ ENTRIES = []; $("> tbody tr", $content).remove(); } else { - // remove apenas os registros que não estão no range [EPOCH_START,EPOCH_END] - // usado pelo zoom e quando usuario seleciona outro periodo - // evita limpar todo o conteúdo, mantendo o scroll atual + // only remove records that are not within the range [EPOCH_START, EPOCH_END] + // used for zooming and when the user selects another period + // prevents clearing all content, keeping the current scroll ENTRIES = ENTRIES.filter(entry => { if (entry.Epoch < EPOCH_START || entry.Epoch > EPOCH_END) { entry.Element.remove(); @@ -311,10 +311,10 @@ } /** - * Obtém os marcadores para o filtro e o intervalo de tempo definido. - * - * Com essa informação é possível fazer a paginaçao do resultado (Keyset Pagination) - */ + * Retrieves the markers for the filter and the defined time range. + * + * With this information, it is possible to paginate the results (Keyset Pagination). + */ function requestTicks() { const $chart = document.querySelector("#chart"); @@ -438,7 +438,13 @@ d: 'YYYY-MM-DD HH:mm', })[INTERVAL_UNIT]; - [5, 15, 25, 35, 50, 65, 75, 85, 95].forEach(percent => { + + let pcts = [5, 15, 25, 35, 50, 65, 75, 85, 95]; + if (window.outerWidth < 768) { + pcts = [5, 50, 95]; + } + + pcts.forEach(percent => { let tickIndex = Math.floor((TICKS.length - 1) * percent / 100); let tick = TICKS[tickIndex]; @@ -469,10 +475,10 @@ } /** - * Faz o carregamento dos próximos registros - * - * @param {string} direction before|after - */ + * Loads the next set of records + * + * @param {string} direction before|after + */ function loadEntries(direction) { if (direction == 'before') { @@ -554,12 +560,6 @@ Overview: getTags(data) } - // @TODO: remover essa lógica - // let tickIndex = Math.floor((entry.Date.getTime() - EPOCH_START) / INTERVAL); - // let tick = ticks[tickIndex]; - // entry.Tick = tick; - // tick.Events.push(entry); - return entry }); @@ -648,7 +648,7 @@ tbody.append(tr); }); - // remove do inicio + // remove from the beginning if (!IS_LOADING_AFTER && ENTRIES.length > 60) { HAS_MORE_AFTER = true; let toRemove = ENTRIES.splice(0, ENTRIES.length - 60); @@ -677,7 +677,7 @@ let chunkHeight = $content.height() - heightBefore; $container.scrollTop(viewTop + chunkHeight); - // remove itens do final + // remove items from the end if (!IS_LOADING_BEFORE && ENTRIES.length > 60) { HAS_MORE_BEFORE = true let toRemove = ENTRIES.splice(60); diff --git a/web/styles.css b/web/styles.css index 2db212b..ae07d79 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1,4 +1,3 @@ - @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); html, @@ -31,22 +30,6 @@ strong { font-weight: bold; } -/* #csv { - font-family: monospace; - font-size: 10px; - width: 100%; - max-width: 100%; - min-width: 100%; - height: 400px; - max-height: 400px; - min-height: 400px; - border: 0; - border-top: 1px solid #d2d2d2; - background: #ccc; - margin-top: 50px; -} */ - - .clear { position: absolute; top: 2px; @@ -63,10 +46,14 @@ strong { position: relative; } -#date-range{ +#date-range { + min-width: 240px; max-width: 240px; + text-align: center; } + + #tab-content { padding: 0 10px; overflow: hidden; @@ -196,6 +183,7 @@ strong { display: table-cell; } + #chart { top: 0; position: sticky; @@ -210,14 +198,14 @@ strong { display: none; } -#chart .count { +#chart .count { position: absolute; left: 0; z-index: 5; top: -10px; } -#chart .zoom { +#chart .zoom { position: absolute; right: 0; z-index: 5; @@ -574,11 +562,55 @@ rgb(255 241 242) border-left: none; } -/* -RED 1: #e72727 -RED 2: #db999a -BLUE 1: #046eab -BLUE 2: #b3e5fc -GRAY: #d2d2d2 -*/ \ No newline at end of file +.offcanvas-body { + font-size: 14px; +} + + +#off-canvas-syntax { + --bs-offcanvas-width: 50% +} + +@media (max-width: 767.98px) { + #date-range { + width: 100%; + max-width: unset; + border-radius: 0; + margin-bottom: 3px; + } + + #expression { + width: 100%; + border-radius: 0; + margin-left: 0; + margin-bottom: 3px; + } + + #expression-help-button { + margin-left: 0; + border-radius: var(--bs-border-radius); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .inputs { + justify-content: center; + } + + #tab-content { + height: calc(100vh - 262px); + } + + #event-attributes { + width: 80%; + } + + #tab-content>table td:nth-child(4) { + display: none; + } + + #off-canvas-syntax { + --bs-offcanvas-width: 90% + } +} \ No newline at end of file