├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docs ├── filter-syntax.md ├── images │ ├── icon-dark.svg │ ├── icon-light.svg │ ├── screenshot-demo.gif │ ├── screenshot-events.png │ ├── screenshot-spans.png │ └── screenshot-traces.png ├── interface.md └── licenses │ ├── Inter-License.txt │ └── NotoSansMono-License.txt ├── venator-app ├── .gitignore ├── README.md ├── app-icon.svg ├── index.html ├── package-lock.json ├── package.json ├── src-tauri │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── capabilities │ │ └── main.json │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── android │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ └── ios │ │ │ ├── AppIcon-20x20@1x.png │ │ │ ├── AppIcon-20x20@2x-1.png │ │ │ ├── AppIcon-20x20@2x.png │ │ │ ├── AppIcon-20x20@3x.png │ │ │ ├── AppIcon-29x29@1x.png │ │ │ ├── AppIcon-29x29@2x-1.png │ │ │ ├── AppIcon-29x29@2x.png │ │ │ ├── AppIcon-29x29@3x.png │ │ │ ├── AppIcon-40x40@1x.png │ │ │ ├── AppIcon-40x40@2x-1.png │ │ │ ├── AppIcon-40x40@2x.png │ │ │ ├── AppIcon-40x40@3x.png │ │ │ ├── AppIcon-512@2x.png │ │ │ ├── AppIcon-60x60@2x.png │ │ │ ├── AppIcon-60x60@3x.png │ │ │ ├── AppIcon-76x76@1x.png │ │ │ ├── AppIcon-76x76@2x.png │ │ │ └── AppIcon-83.5x83.5@2x.png │ ├── src │ │ ├── commands.rs │ │ ├── ingress │ │ │ ├── mod.rs │ │ │ ├── otel.rs │ │ │ └── tracing.rs │ │ ├── main.rs │ │ └── views.rs │ └── tauri.conf.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── Inter.ttf │ │ ├── NotoSansMono.ttf │ │ ├── collapse-all.svg │ │ ├── collapsed.svg │ │ ├── event-add.svg │ │ ├── event.svg │ │ ├── expand-all.svg │ │ ├── expanded.svg │ │ ├── live-pause.svg │ │ ├── live-play.svg │ │ ├── resource.svg │ │ ├── span-add.svg │ │ ├── span.svg │ │ └── trace.svg │ ├── components │ │ ├── detail-pane.css │ │ ├── detail-pane.tsx │ │ ├── event-count-graph.css │ │ ├── event-count-graph.tsx │ │ ├── filter-input.css │ │ ├── filter-input.tsx │ │ ├── graph-container.css │ │ ├── graph-container.tsx │ │ ├── screen-header.css │ │ ├── screen-header.tsx │ │ ├── span-graph.css │ │ ├── span-graph.tsx │ │ ├── tab-bar.css │ │ ├── tab-bar.tsx │ │ ├── table.css │ │ ├── table.tsx │ │ ├── time-controls.css │ │ ├── time-controls.tsx │ │ ├── trace-graph.css │ │ └── trace-graph.tsx │ ├── context │ │ ├── collapsable.ts │ │ └── navigation.ts │ ├── index.tsx │ ├── invoke.tsx │ ├── models.tsx │ ├── screens │ │ ├── events-screen.css │ │ ├── events-screen.tsx │ │ ├── spans-screen.css │ │ ├── spans-screen.tsx │ │ ├── trace-screen.css │ │ └── trace-screen.tsx │ ├── utils │ │ ├── datalayer.ts │ │ └── undo.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── venator-engine ├── .gitignore ├── Cargo.toml ├── README.md ├── benches │ └── my_benchmark.rs └── src │ ├── context │ ├── event_context.rs │ ├── mod.rs │ └── span_context.rs │ ├── engine │ ├── async_engine.rs │ ├── mod.rs │ └── sync_engine.rs │ ├── filter │ ├── event_filter.rs │ ├── input.rs │ ├── mod.rs │ ├── span_filter.rs │ ├── util.rs │ └── value.rs │ ├── index │ ├── event_indexes.rs │ ├── mod.rs │ ├── span_event_indexes.rs │ ├── span_indexes.rs │ ├── util.rs │ └── value.rs │ ├── lib.rs │ ├── models.rs │ ├── storage │ ├── cached.rs │ ├── file.rs │ ├── mod.rs │ └── transient.rs │ └── subscription.rs └── venator ├── Cargo.toml ├── README.md └── src ├── attributes.rs ├── ids.rs ├── lib.rs └── messaging.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "venator", 5 | "venator-app/src-tauri", 6 | "venator-engine", 7 | ] 8 | 9 | [profile.release] 10 | codegen-units = 1 11 | lto = true 12 | strip = true 13 | 14 | [patch.crates-io] 15 | venator-engine = { path = "./venator-engine" } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Trevor Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | venator logo 6 | 7 |

8 | 9 | Venator is a application for recording, viewing, and filtering logs and spans 10 | from programs instrumented with the Rust tracing crate or using OpenTelemetry. 11 | It is purpose-built for rapid local development. 12 | 13 | 14 | demo 15 | 16 | 17 | ## Installation 18 | 19 | ### With Pre-built Binaries: 20 | 21 | Binaries are pre-built and available in the [releases page](https://github.com/kmdreko/venator/releases) for: 22 | - Windows (x64) 23 | - MacOS (Intel, Apple silicon) 24 | 25 | ### With Cargo: 26 | 27 | Compiling and installing `venator` from source with Cargo (requires Rust 1.76 or 28 | newer): 29 | 30 | ``` 31 | cargo install venator-app 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Using OpenTelemetry: 37 | 38 | Configure your program's OpenTelemetry SDK to export logs and traces to 39 | `127.0.0.1:8362` (Venator's default listening port) and to use `grpc` or 40 | `http/protobuf`. 41 | 42 | ### Using Rust Tracing: 43 | 44 | In your instrumented program: 45 | 46 | ```toml 47 | [dependencies] 48 | venator = "1.1.0" 49 | ``` 50 | 51 | ```rust 52 | use venator::Venator; 53 | 54 | Venator::default().install(); 55 | ``` 56 | 57 | See the [documentation](https://docs.rs/venator/latest/venator/) for more. 58 | 59 | ## Features 60 | 61 | Events can be viewed narrowed by timespan and filtered by attributes, level, and 62 | other properties. The table of records can include columns of custom properties. 63 | The graph shows the counts by level at each bar. 64 | 65 | 66 | screenshots of events screen 67 | 68 | 69 | Spans can likewise be narrowed by timespan and filter. A detail pane can show 70 | all the properties of selected events and spans. The graph shows spans layered 71 | hierarchically. 72 | 73 | 74 | screenshots of spans screen 75 | 76 | 77 | Traces can be viewed that show both events and spans within a single execution. 78 | 79 | 80 | screenshots of trace screen 81 | 82 | -------------------------------------------------------------------------------- /docs/filter-syntax.md: -------------------------------------------------------------------------------- 1 | # Venator Filter Syntax 2 | 3 | The filter is composed of `property: value` predicates that an event or span 4 | must satisfy to be shown. 5 | 6 | Properties come in two kinds: 7 | 8 | - *inherent* properties start with `#` and correspond to values built-in to 9 | the instrumentation and reporting of events and spans. The available 10 | inherent properties are: 11 | - `#level`: 12 | - `#parent`: 13 | - `#trace`: 14 | - `#target`: 15 | - `#file`: 16 | 17 | - *attribute* properties start with `@` and are user-defined structured logging 18 | fields that can be provided on events and spans. Nested events and spans 19 | inheret the attributes of their parent span(s) and root unless overridden. 20 | 21 | Values can take a few different forms: 22 | 23 | - if the value matches `true` and `false` it will match boolean values as well 24 | as literal strings with those exact characters. 25 | - if the value can be parsed as an integer like `42` it will match integer and 26 | float values that equal the value as well as literal strings with those 27 | exact characters. 28 | - if the value can be parsed as a float like `6.09` or `-2.44e9` it will match 29 | float values that equal it as well as literal strings with those exact 30 | characters. 31 | - if the value starts and ends with `/` like `/[0-9a-f]{32}/` it will be parsed 32 | as a regex and will match string values satifying that regex. 33 | - if the value contains a `*` it will be interpretted as a wildcard (unless 34 | escaped like `\*`) and will match strings where `*` can satisfy any number 35 | of characters. 36 | - if the value starts and ends with `"` then it is interpretted literally 37 | (except `*`s still mean a wildcard) and will not try to parse other symbols 38 | 39 | Values can also have operators applied to them: 40 | 41 | - `!value` will match values that do __not__ satisfy that value (can have other 42 | operators as well) 43 | - `value` will match values greater than that value (lexicographical comparison 46 | for strings; numerical comparison for integers, floats, and booleans) 47 | - `<=value` will match values less than or equal to that value (lexicographical 48 | comparison for strings; numerical comparison for integers, floats, and 49 | booleans) 50 | - `>=value` will match values greater than or equal to that value (lexicographical 51 | comparison for strings; numerical comparison for integers, floats, and 52 | booleans) 53 | - `(value1 AND value2 ...)` will match values only if all are satisfied 54 | - `(value1 OR value2 ...)` will match values if any are satisfied 55 | 56 | 57 | ## FAQ 58 | 59 | 60 | ### How to filter for a value with spaces? 61 | 62 | You can surround a value with quotes `"` to include characters that would 63 | otherwise be misinterpretted - like spaces, `:`, `!`, and other operators: 64 | 65 | ``` 66 | @name: "John Titor" @category: "decor:lighting" 67 | ``` 68 | 69 | Note that quotes may be automatically removed when its not warranted and may 70 | be automatically added if a value was able to be parsed but includes special 71 | characters out of an abundance of clarity. 72 | 73 | This goes for attributes as well: 74 | 75 | ``` 76 | @"first name": John @"category:name": lighting 77 | ``` 78 | 79 | 80 | ### How to exclude a value? 81 | 82 | You can use `!` to negate a filter which will work on values: 83 | 84 | ``` 85 | @name: !John 86 | ``` 87 | 88 | It is worth noting however that the results will also include events or spans 89 | that do not have that property. To only get results that have the property 90 | set but are *not* a particular value, you can combine it with an "exists" 91 | filter: 92 | 93 | ``` 94 | @name: (* AND !John) 95 | ``` 96 | 97 | 98 | ### How to filter for a property that exists? 99 | 100 | A wildcard will typically only filter for values that are strings, however a 101 | bare `*` will include any value, so it can serve as an "exists" filter: 102 | 103 | ``` 104 | @name: * 105 | ``` 106 | 107 | 108 | ### How to filter for a range? 109 | 110 | The best way is to use an `AND` group with `>`/`>=` and `<`/`<=` comparison 111 | operators: 112 | 113 | ``` 114 | #duration: (>1s AND <10s) 115 | ``` 116 | 117 | 118 | ### How to filter for value that starts or ends with something? 119 | 120 | You can use wildcards at the end or beginning of a string value to get "starts 121 | with" or "ends with" behavior: 122 | 123 | ``` 124 | @name: "John *" @message: "* items were found" 125 | ``` 126 | 127 | 128 | ### How to filter for value in different properties? 129 | 130 | You can use an `(... OR ...)` grouping around property-value pairs to find 131 | entities that may satisfy one predicate or another: 132 | 133 | ``` 134 | (@name: John OR @legacy_name: John) 135 | ``` 136 | 137 | 138 | ### How to filter for a specific type of value? 139 | 140 | Not supported currently. 141 | -------------------------------------------------------------------------------- /docs/images/icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/screenshot-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/docs/images/screenshot-demo.gif -------------------------------------------------------------------------------- /docs/images/screenshot-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/docs/images/screenshot-events.png -------------------------------------------------------------------------------- /docs/images/screenshot-spans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/docs/images/screenshot-spans.png -------------------------------------------------------------------------------- /docs/images/screenshot-traces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/docs/images/screenshot-traces.png -------------------------------------------------------------------------------- /docs/interface.md: -------------------------------------------------------------------------------- 1 | # Venator Interface 2 | 3 | The user interface is split into three meta regions: 4 | - the tab bar 5 | - the main screen 6 | - the status bar 7 | 8 | 9 | ## Tab Bar 10 | 11 | The tabs should be self-explanatory but I'll explain regardless. The app can 12 | have multiple "screens" open simulaneouosly; the most lit one is what is being 13 | shown and the unlit ones are backgrounded. You can switch to a tab by clicking 14 | on it. You can open a new tab by clicking one of the buttons on the right (one 15 | for an events screen, one for a spans screen) or by using the shortcut `CTRL+T`. 16 | You can close a tab by clicking on `X` when its active, or by middle-clicking. 17 | Hovering over a tab will show the screen type and full filter. More options are 18 | available via `View` in the menu and right-clicking a tab. 19 | 20 | 21 | ## Status Bar 22 | 23 | The status bar at the bottom of the screen shows information of the application 24 | overall. 25 | 26 | The bottom left shows the database file that is open (or the "default dataset" 27 | if launched by default) as well as if it is listening for connections and if so 28 | what address and port. 29 | 30 | The bottom right shows the running metrics. The first metric is the bytes per 31 | second being received from established connections. The next metric shows the 32 | number of connections currently established. The last metric shows the load on 33 | the underlying engine that is handling incomming data and responding to queries. 34 | 35 | 36 | ## Main Screen 37 | 38 | The main screen is where you can view events, spans, and other entities based on 39 | timeframe and filter. It is composed of a few key components: 40 | - the time controls 41 | - the filter input 42 | - the graph 43 | - the table 44 | - the details panel (collapsable) 45 | 46 | 47 | ### Time Controls 48 | 49 | These controls affect the timeframe that affects the graph and table results of 50 | the screen. It is split between the starting point and the duration of the time 51 | frame with an additional button for listening to live events. 52 | 53 | The starting point controls shows the currently set starting point and has 54 | buttons for shifting the starting point before or after in time. The main field 55 | can also be edited manually to set a specific time. 56 | 57 | The duration controls shows the currently set duration and has buttons for 58 | reducing or expanding the duration. The main field can also be edited manually 59 | to set a specific duration. 60 | 61 | 62 | ### Filter Input 63 | 64 | The filter input is where you can specify `property: value` predicates for 65 | narrowing down the events, spans, etc that are being shown. See [filter syntax](./filter-syntax.md) 66 | for details. 67 | 68 | An empty event or span screen will include a permanent `#level` filter for only 69 | showing entities at or above the specified log level. You can also hover and 70 | scroll on this predicate to increase or decrease the level. 71 | 72 | When editing the filter, any non-permanent predicates are undecorated and shown 73 | as a single text input. When hitting `Enter` or clicking off, the filter will be 74 | parsed and applied to the graph and table results. The predicates will be 75 | decorated based on their type (white for attributes, gray for inherent fields). 76 | The predicates will be highlighted in red if the value was invalid or the whole 77 | input will be highlighted red if there was a syntax error that meant the filter 78 | couldn't be properly split into predicates. Individual predicates can be right- 79 | clicked to copy or remove, or middle-clicked to remove them. 80 | 81 | Options in the table or details panel can add predicates to the filter. 82 | 83 | 84 | ### Graph 85 | 86 | The graph can take a few different forms based on the type of main screen. For 87 | events it will show bars of aggregated counts by log level. For spans it will 88 | show them individually spread across their timeframe stacked on top of eachother 89 | (up to 10 high). For traces it will show all spans and events stacked on top of 90 | eachother (squished to accomodate them all). 91 | 92 | Hovering your cursor over the graph will show the hovered timestamp in the top. 93 | Clicking and dragging will highlight a timeframe and zoom to it when released. 94 | You can also scroll on the graph to zoom in and out. Middle-click dragging will 95 | allow you to pan the timeframe left and right. 96 | 97 | 98 | ### Table 99 | 100 | The table shows the actual events or spans being queried by the filter and 101 | timeframe. By default it will show columns for the level, when it occurred 102 | (`#timestamp` for events, `#created_at` for spans) and a default column 103 | (`@message` for events, `#name` for spans). The timing column also includes a 104 | toggle for controlling the sort order. 105 | 106 | Each of the non-fixed columns have a `+` button in the header to create a new 107 | column. The column header can be edited to change the property which uses the 108 | same `#` and `@` syntax as the filter. You can also right-click on the header to 109 | access various options or middle-click to remove it. 110 | 111 | The table cells contain the value for the entity and property of the row and 112 | column. If a value does not exist, it will show as `---`. Clicking on a row will 113 | show or hide the details panel for that entity. You can also right-click the 114 | cell to access various options for that value or property. 115 | 116 | 117 | ### Details Panel 118 | 119 | This panel opens alongside the table and shows all information about the entity 120 | selected. The top of the panel shows the inherent properties along with a 121 | `#stack` property that can be expanded or collapsed on-click to show parent 122 | spans. 123 | 124 | The highlighted section shows the primary data for the entity (`@message` for 125 | events and `#name` for spans). 126 | 127 | The bottom shows all the attributes and their values. The far left icon will 128 | indicate where that value came from (a missing icon means it is directly on the 129 | entity, a span icon means it came from a parent span, and a resource icon means 130 | it was provided by the root resource). You can hover over this icon for details 131 | or the right-click will include a `copy * id` option when available. The 132 | right-click menu shows many options for adding to the filter or even adding a 133 | column to the table. An attribute value that is too long will be cut off, but 134 | you can toggle the `-` after the attribute name to expand it. 135 | -------------------------------------------------------------------------------- /docs/licenses/Inter-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /docs/licenses/NotoSansMono-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /venator-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /venator-app/README.md: -------------------------------------------------------------------------------- 1 | The Venator GUI is made with Tauri and a SolidJS + Typescript frontend. 2 | 3 | Development instructions (Rust 1.76 or newer, npm 10.7 or newer): 4 | 5 | - navigate to `venator-app/` (this directory) 6 | - `npm i` 7 | - `npm run tauri dev` 8 | -------------------------------------------------------------------------------- /venator-app/app-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /venator-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Venator 9 | 10 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /venator-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "venator-app", 3 | "version": "1.0.3", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "dev": "vite", 9 | "build": "vite build", 10 | "serve": "vite preview", 11 | "tauri": "tauri" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@tanstack/solid-virtual": "^3.13.0", 16 | "@tauri-apps/api": "^2.0.1", 17 | "@tauri-apps/plugin-clipboard-manager": "^2.0.0", 18 | "@tauri-apps/plugin-dialog": "^2.0.0", 19 | "@tauri-apps/plugin-fs": "^2.0.0", 20 | "solid-js": "^1.7.8" 21 | }, 22 | "devDependencies": { 23 | "@tauri-apps/cli": "^2.0.1", 24 | "typescript": "^5.2.2", 25 | "vite": "^5.3.1", 26 | "vite-plugin-solid": "^2.8.0" 27 | } 28 | } -------------------------------------------------------------------------------- /venator-app/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | 9 | 10 | local.* 11 | -------------------------------------------------------------------------------- /venator-app/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "venator-app" 3 | version = "1.0.3" 4 | edition = "2021" 5 | description = "A log and trace viewer for Rust tracing and OpenTelemetry" 6 | readme = "README.md" 7 | repository = "https://github.com/kmdreko/venator" 8 | license = "MIT" 9 | keywords = ["logging", "tracing", "opentelemetry", "profiling"] 10 | include = ["/src", "/build.rs", "/tauri.conf.json", "/icons", "/gen", "/capabilities", "/dist"] 11 | 12 | [[bin]] 13 | name = "venator" 14 | path = "src/main.rs" 15 | 16 | [build-dependencies] 17 | tauri-build = { version = "2.0.1", features = [] } 18 | 19 | [dependencies] 20 | anyhow = "1.0.95" 21 | axum = { version = "0.7.9", default-features = false, features = ["http1", "http2", "tokio"] } 22 | bincode = { version = "1.3.3", default-features = false } 23 | clap = { version = "4.5.20", features = ["derive"] } 24 | directories = "5.0.1" 25 | futures = { version = "0.3.31", default-features = false } 26 | http-body = "1.0.1" 27 | open = "5.3.0" 28 | opentelemetry-proto = { version = "0.27.0", features = ["gen-tonic-messages", "logs", "metrics", "trace"] } 29 | prost = "0.13.3" 30 | tauri = { version = "2.0.1", features = [] } 31 | tauri-plugin-clipboard-manager = "2.0.1" 32 | tauri-plugin-dialog = "2.0.1" 33 | tauri-plugin-fs = "2.0.1" 34 | serde = { version = "1.0.159", default-features = false, features = ["std", "derive"] } 35 | serde_json = "1" 36 | tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros", "net"] } 37 | tokio-util = { version = "0.7.13", features = ["io"] } 38 | tonic = "0.12.3" 39 | tracing = "0.1.41" 40 | tracing-subscriber = { version = "0.3.19", features = ["json"] } 41 | 42 | venator-engine = { version = "0.4.1", features = ["persist"] } 43 | 44 | [features] 45 | default = ["custom-protocol"] 46 | # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! 47 | custom-protocol = ["tauri/custom-protocol"] 48 | -------------------------------------------------------------------------------- /venator-app/src-tauri/README.md: -------------------------------------------------------------------------------- 1 | venator logo 2 | 3 | The Venator application capable of recording, viewing, and filtering logs and 4 | spans from either Rust programs instrumented with the tracing crate or from 5 | programs that export OpenTelemetry data. 6 | 7 | ## Install with Cargo: 8 | 9 | ``` 10 | cargo install venator-app 11 | ``` 12 | 13 | Run: 14 | 15 | ``` 16 | venator 17 | ``` 18 | -------------------------------------------------------------------------------- /venator-app/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /venator-app/src-tauri/capabilities/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "main-capability", 3 | "description": "Capability for the main window", 4 | "windows": [ 5 | "main" 6 | ], 7 | "permissions": [ 8 | "core:path:default", 9 | "core:event:default", 10 | "core:window:default", 11 | "core:app:default", 12 | "core:resources:default", 13 | "core:menu:default", 14 | "core:tray:default", 15 | "core:window:allow-set-title", 16 | "clipboard-manager:allow-write-text", 17 | "dialog:default", 18 | "fs:allow-write-text-file" 19 | ] 20 | } -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /venator-app/src-tauri/src/ingress/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::pin::Pin; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use std::sync::{Arc, Mutex, OnceLock}; 5 | use std::task::{Context, Poll}; 6 | use std::time::Instant; 7 | 8 | use axum::body::{Body, Bytes, HttpBody}; 9 | use axum::extract::{Request, State}; 10 | use axum::middleware::{from_fn_with_state, Next}; 11 | use axum::response::Response; 12 | use axum::routing::post; 13 | use axum::BoxError; 14 | use http_body::Frame; 15 | use tokio::net::TcpListener; 16 | use tonic::service::Routes; 17 | 18 | use venator_engine::engine::AsyncEngine; 19 | 20 | mod otel; 21 | mod tracing; 22 | 23 | pub(crate) struct IngressState { 24 | bind: String, 25 | error: OnceLock, 26 | 27 | engine: AsyncEngine, 28 | 29 | last_check: Mutex, 30 | num_bytes: AtomicUsize, 31 | 32 | // Only one tracing instance for a given ID is allowed at a time, so this 33 | // keeps track of those that are connected. 34 | tracing_instances: Mutex>, 35 | } 36 | 37 | impl IngressState { 38 | fn new(engine: AsyncEngine, bind: String) -> IngressState { 39 | IngressState { 40 | bind, 41 | error: OnceLock::new(), 42 | engine, 43 | last_check: Mutex::new(Instant::now()), 44 | num_bytes: AtomicUsize::new(0), 45 | tracing_instances: Mutex::new(HashSet::new()), 46 | } 47 | } 48 | 49 | fn set_error(&self, error: String) { 50 | let _ = self.error.set(error); 51 | } 52 | 53 | pub(crate) fn get_status(&self) -> (String, Option) { 54 | if let Some(err) = self.error.get() { 55 | let msg = format!("not listening on {}", self.bind); 56 | let err = err.to_string(); 57 | 58 | (msg, Some(err)) 59 | } else { 60 | let msg = format!("listening on {}", self.bind); 61 | 62 | (msg, None) 63 | } 64 | } 65 | 66 | pub(crate) fn get_and_reset_metrics(&self) -> (usize, f64) { 67 | let now = Instant::now(); 68 | let last = std::mem::replace( 69 | &mut *self.last_check.lock().unwrap_or_else(|p| p.into_inner()), 70 | now, 71 | ); 72 | let elapsed = (now - last).as_secs_f64(); 73 | 74 | let num_bytes = self.num_bytes.swap(0, Ordering::Relaxed); 75 | 76 | (num_bytes, elapsed) 77 | } 78 | } 79 | 80 | struct IngressBody { 81 | state: Arc, 82 | inner: B, 83 | } 84 | 85 | impl IngressBody { 86 | fn wrap(state: Arc, inner: B) -> Body 87 | where 88 | B: HttpBody + Unpin + Send + 'static, 89 | B::Error: Into, 90 | { 91 | Body::new(IngressBody { state, inner }) 92 | } 93 | } 94 | 95 | impl HttpBody for IngressBody 96 | where 97 | B: HttpBody + Unpin, 98 | { 99 | type Data = B::Data; 100 | type Error = B::Error; 101 | 102 | fn poll_frame( 103 | mut self: Pin<&mut Self>, 104 | ctx: &mut Context<'_>, 105 | ) -> Poll, Self::Error>>> { 106 | let res = Pin::new(&mut self.inner).poll_frame(ctx); 107 | 108 | if let Poll::Ready(Some(Ok(ref frame))) = &res { 109 | if let Some(data) = frame.data_ref() { 110 | self.state 111 | .num_bytes 112 | .fetch_add(data.len(), Ordering::Relaxed); 113 | } 114 | } 115 | 116 | res 117 | } 118 | } 119 | 120 | async fn ingress_middleware( 121 | State(state): State>, 122 | request: Request, 123 | next: Next, 124 | ) -> Response { 125 | next.run(request.map(|b| IngressBody::wrap(state, b))).await 126 | } 127 | 128 | pub fn launch_ingress_thread(engine: AsyncEngine, bind: String) -> Arc { 129 | #[tokio::main(flavor = "current_thread")] 130 | async fn ingress_task(state: Arc) { 131 | let listener = match TcpListener::bind(&state.bind).await { 132 | Ok(listener) => listener, 133 | Err(err) => { 134 | state.set_error(format!("failed to listen on bind port: {err}")); 135 | return; 136 | } 137 | }; 138 | 139 | let routes = Routes::default() 140 | .add_service(otel::logs_service(state.engine.clone())) 141 | .add_service(otel::metrics_service(state.engine.clone())) 142 | .add_service(otel::trace_service(state.engine.clone())) 143 | .into_axum_router() 144 | .with_state(()) 145 | .route("/tracing/v1", post(self::tracing::post_tracing_handler)) 146 | .route("/v1/logs", post(self::otel::post_otel_logs_handler)) 147 | .route("/v1/metrics", post(self::otel::post_otel_metrics_handler)) 148 | .route("/v1/trace", post(self::otel::post_otel_trace_handler)) 149 | .layer(from_fn_with_state(state.clone(), ingress_middleware)) 150 | .with_state(state.clone()); 151 | 152 | match axum::serve(listener, routes).await { 153 | Ok(_) => { 154 | state.set_error("failed to serve: Exit".to_owned()); 155 | return; 156 | } 157 | Err(err) => { 158 | state.set_error(format!("failed to serve: {err}")); 159 | return; 160 | } 161 | }; 162 | } 163 | 164 | let state = Arc::new(IngressState::new(engine, bind)); 165 | 166 | std::thread::spawn({ 167 | let state = state.clone(); 168 | || ingress_task(state) 169 | }); 170 | 171 | state 172 | } 173 | -------------------------------------------------------------------------------- /venator-app/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.kmdreko.venator", 3 | "productName": "Venator", 4 | "version": "1.0.3", 5 | "build": { 6 | "beforeDevCommand": "npm run dev", 7 | "beforeBuildCommand": "npm run build", 8 | "devUrl": "http://localhost:1420", 9 | "frontendDist": "./dist" 10 | }, 11 | "app": { 12 | "windows": [ 13 | { 14 | "title": "Venator", 15 | "width": 1280, 16 | "height": 720, 17 | "theme": "Dark" 18 | } 19 | ], 20 | "security": { 21 | "csp": null 22 | } 23 | }, 24 | "bundle": { 25 | "publisher": "kmdreko", 26 | "shortDescription": "Venator", 27 | "longDescription": "A log and trace viewer for Rust tracing and OpenTelemetry", 28 | "active": true, 29 | "targets": "all", 30 | "icon": [ 31 | "icons/32x32.png", 32 | "icons/128x128.png", 33 | "icons/128x128@2x.png", 34 | "icons/icon.icns", 35 | "icons/icon.ico" 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /venator-app/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | 9 | overflow: clip; 10 | } 11 | 12 | :root { 13 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 14 | font-size: 12px; 15 | line-height: 20px; 16 | font-weight: 400; 17 | 18 | color: #0f0f0f; 19 | 20 | font-synthesis: none; 21 | text-rendering: optimizeLegibility; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-text-size-adjust: 100%; 25 | } 26 | 27 | ::selection { 28 | background-color: var(--text-selection-bg-color); 29 | color: var(--text-selection-color); 30 | } 31 | 32 | #root { 33 | height: 100%; 34 | display: flex; 35 | flex-direction: column; 36 | color: var(--text-normal); 37 | } 38 | 39 | #root[data-theme="light"] { 40 | color-scheme: light; 41 | 42 | --bg-tab-color: #2b2b2b; 43 | --bg-screen-color: #e8e8e8; 44 | --bg-secondary-color: #d2d2d2; 45 | --bg-highlight-color: white; 46 | 47 | --border-dark-color: gray; 48 | --border-normal-color: #a8a8a8; 49 | --border-light-color: #cccccc; 50 | --border-error: red; 51 | 52 | --text-normal: #0f0f0f; 53 | --text-light: #505050; 54 | --text-warn: #c16c00; 55 | --text-error: #d13200; 56 | --text-click-inactive-color: gray; 57 | --text-click-active-color: black; 58 | --text-selection-color: white; 59 | --text-selection-bg-color: #1e1e1e; 60 | 61 | --level-0-color: #0f0f0f; 62 | --level-0-bg-color: white; 63 | --level-0-bar-color: #1e1e1e; 64 | --level-1-color: #0f0f0f; 65 | --level-1-bg-color: #a8a8a8; 66 | --level-1-bar-color: #1e1e1e; 67 | --level-2-color: white; 68 | --level-2-bg-color: #1e1e1e; 69 | --level-2-bar-color: #1e1e1e; 70 | --level-3-color: #0f0f0f; 71 | --level-3-bg-color: #fb8c00; 72 | --level-3-bar-color: #fb8c00; 73 | --level-4-color: #0f0f0f; 74 | --level-4-bg-color: #e65100; 75 | --level-4-bar-color: #e65100; 76 | --level-5-color: #0f0f0f; 77 | --level-5-bg-color: #ad3100; 78 | --level-5-bar-color: #ad3100; 79 | } 80 | 81 | #root[data-theme="dark"] { 82 | color-scheme: dark; 83 | 84 | --bg-tab-color: #141414; 85 | --bg-screen-color: #262626; 86 | --bg-secondary-color: #333333; 87 | --bg-highlight-color: #141414; 88 | 89 | --border-dark-color: #636363; 90 | --border-normal-color: #5e5e5e; 91 | --border-light-color: #494949; 92 | --border-error: red; 93 | 94 | --text-normal: #b4b4b4; 95 | --text-light: #aaaaaa; 96 | --text-warn: #c16c00; 97 | --text-error: #d13200; 98 | --text-click-inactive-color: gray; 99 | --text-click-active-color: #d1d1d1; 100 | --text-selection-color: white; 101 | --text-selection-bg-color: #1e1e1e; 102 | 103 | --level-0-color: #0f0f0f; 104 | --level-0-bg-color: #e0e0e0; 105 | --level-0-bar-color: #727272; 106 | --level-1-color: #0f0f0f; 107 | --level-1-bg-color: #a8a8a8; 108 | --level-1-bar-color: #727272; 109 | --level-2-color: white; 110 | --level-2-bg-color: #1e1e1e; 111 | --level-2-bar-color: #727272; 112 | --level-3-color: #0f0f0f; 113 | --level-3-bg-color: #fb8c00; 114 | --level-3-bar-color: #fb8c00; 115 | --level-4-color: #0f0f0f; 116 | --level-4-bg-color: #e65100; 117 | --level-4-bar-color: #e65100; 118 | --level-5-color: #0f0f0f; 119 | --level-5-bg-color: #ad3100; 120 | --level-5-bar-color: #ad3100; 121 | } 122 | 123 | #menu-controls span { 124 | margin: 2px; 125 | } 126 | 127 | #screen { 128 | background-color: var(--bg-screen-color); 129 | flex: 1; 130 | height: 0; 131 | position: relative; 132 | } 133 | 134 | #statusbar { 135 | flex: 0; 136 | min-height: 19px; 137 | background-color: var(--bg-secondary-color); 138 | border-top: 1px solid var(--border-normal-color); 139 | font-size: 12px; 140 | line-height: 18px; 141 | color: var(--text-normal); 142 | 143 | display: flex; 144 | flex-direction: row; 145 | justify-content: space-between; 146 | } -------------------------------------------------------------------------------- /venator-app/src/assets/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src/assets/Inter.ttf -------------------------------------------------------------------------------- /venator-app/src/assets/NotoSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmdreko/venator/cfaee42487d3d53bfd762088761e68da37720f82/venator-app/src/assets/NotoSansMono.ttf -------------------------------------------------------------------------------- /venator-app/src/assets/collapse-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/collapsed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/event-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /venator-app/src/assets/event.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /venator-app/src/assets/expand-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/expanded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/live-pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/live-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /venator-app/src/assets/resource.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /venator-app/src/assets/span-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /venator-app/src/assets/span.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /venator-app/src/assets/trace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /venator-app/src/components/detail-pane.css: -------------------------------------------------------------------------------- 1 | #detail-pane { 2 | width: 500px; 3 | min-width: 500px; 4 | flex: 0; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | 9 | border: 1px solid var(--border-normal-color); 10 | border-top: none; 11 | } 12 | 13 | #detail-pane-grabber { 14 | width: 8px; 15 | cursor: ew-resize; 16 | } 17 | 18 | #detail-header { 19 | flex: 0; 20 | font-family: 'Noto Sans Mono', monospace; 21 | font-size: 12px; 22 | font-weight: 800; 23 | border-bottom: 1px solid var(--border-normal-color); 24 | padding: 0 8px; 25 | padding-bottom: 2px; 26 | padding-top: 1px; 27 | 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | } 32 | 33 | #detail-header button { 34 | border: none; 35 | background-color: transparent; 36 | color: var(--text-click-inactive-color); 37 | padding: 0; 38 | } 39 | 40 | #detail-header button:hover { 41 | color: var(--text-click-active-color); 42 | } 43 | 44 | #detail-info { 45 | flex: 1; 46 | background-color: var(--bg-secondary-color); 47 | 48 | display: flex; 49 | flex-direction: column; 50 | gap: 8px; 51 | padding: 8px; 52 | 53 | overflow: auto; 54 | } 55 | 56 | #detail-info-head { 57 | display: flex; 58 | flex-direction: row; 59 | justify-content: space-between; 60 | } 61 | 62 | #detail-info-head-data { 63 | display: flex; 64 | flex-direction: row; 65 | gap: 4px; 66 | } 67 | 68 | #detail-info-head-controls button { 69 | flex: 0; 70 | background: transparent; 71 | border: none; 72 | padding: 0 4px; 73 | opacity: 75%; 74 | height: 18px; 75 | } 76 | 77 | #detail-info-head-controls button:hover { 78 | opacity: 100%; 79 | } 80 | 81 | [data-theme="dark"] #detail-info-head-controls button { 82 | filter: invert(90%); 83 | } 84 | 85 | .detailed-level-0, 86 | .detailed-level-1, 87 | .detailed-level-2, 88 | .detailed-level-3, 89 | .detailed-level-4, 90 | .detailed-level-5 { 91 | height: 20px; 92 | border-radius: 4px; 93 | padding: 1px 6px; 94 | } 95 | 96 | .detailed-level-0 { 97 | margin-left: 1px; 98 | color: var(--level-0-color); 99 | background-color: var(--level-0-bg-color); 100 | outline: 1px solid var(--border-normal-color); 101 | } 102 | 103 | .detailed-level-1 { 104 | color: var(--level-1-color); 105 | background-color: var(--level-1-bg-color); 106 | } 107 | 108 | .detailed-level-2 { 109 | color: var(--level-2-color); 110 | background-color: var(--level-2-bg-color); 111 | } 112 | 113 | .detailed-level-3 { 114 | color: var(--level-3-color); 115 | background-color: var(--level-3-bg-color); 116 | } 117 | 118 | .detailed-level-4 { 119 | color: var(--level-4-color); 120 | background-color: var(--level-4-bg-color); 121 | } 122 | 123 | .detailed-level-5 { 124 | color: var(--level-4-color); 125 | background-color: var(--level-4-bg-color); 126 | } 127 | 128 | .detailed-timestamp { 129 | height: 20px; 130 | border-radius: 4px; 131 | padding: 0px 6px; 132 | border: 1px solid var(--border-normal-color); 133 | } 134 | 135 | .detailed-duration { 136 | padding-top: 4px; 137 | line-height: 16px; 138 | } 139 | 140 | .detailed-duration .total { 141 | height: 20px; 142 | border-radius: 4px; 143 | padding: 0px 6px; 144 | } 145 | 146 | .detailed-duration .bar { 147 | position: relative; 148 | display: inline-block; 149 | width: 100px; 150 | height: 10px; 151 | border-color: var(--border-dark-color); 152 | background-image: linear-gradient(transparent 0 44%, 153 | var(--border-dark-color) 50% 56%, 154 | transparent 56% 100%); 155 | border-right: 1px solid var(--border-dark-color); 156 | border-left: 1px solid var(--border-dark-color); 157 | } 158 | 159 | .detailed-duration .bar .busy-bar { 160 | position: absolute; 161 | top: 0%; 162 | bottom: 0%; 163 | 164 | background-color: var(--level-2-bar-color); 165 | } 166 | 167 | .detailed-duration .busy { 168 | font-weight: bold; 169 | height: 15px; 170 | border-radius: 4px; 171 | padding: 0px 6px; 172 | } 173 | 174 | /* .detailed-meta {} */ 175 | 176 | .detailed-meta:hover { 177 | background-color: color-mix(in lab, var(--bg-secondary-color) 90%, black 10%); 178 | } 179 | 180 | /* .detailed-meta-id {} */ 181 | 182 | .detailed-meta-id:hover { 183 | background-color: color-mix(in lab, var(--bg-secondary-color) 90%, black 10%); 184 | } 185 | 186 | .detailed-meta-parent { 187 | font-family: 'Noto Sans Mono', monospace; 188 | font-weight: 500; 189 | margin-left: 16px; 190 | } 191 | 192 | .detailed-meta-parent:hover { 193 | background-color: color-mix(in lab, var(--bg-secondary-color) 90%, black 10%); 194 | } 195 | 196 | .detail-info-primary { 197 | font-family: 'Noto Sans Mono', monospace; 198 | font-weight: 500; 199 | background-color: var(--bg-highlight-color); 200 | border-radius: 4px; 201 | padding: 2px 6px; 202 | border: 1px solid var(--border-normal-color); 203 | } 204 | 205 | #detail-info-attributes { 206 | display: grid; 207 | width: 100%; 208 | grid-template-columns: 16px max-content 16px auto; 209 | background-color: transparent; 210 | border-spacing: 0px; 211 | } 212 | 213 | .hovered { 214 | background-color: color-mix(in lab, var(--bg-secondary-color) 90%, black 10%); 215 | } 216 | 217 | .detail-info-attributes-source { 218 | user-select: none; 219 | } 220 | 221 | [data-theme="dark"] .detail-info-attributes-source img { 222 | filter: invert(90%); 223 | } 224 | 225 | .detail-info-attributes-name { 226 | font-weight: bold; 227 | } 228 | 229 | .detail-info-attributes-value { 230 | position: relative; 231 | } 232 | 233 | .detail-info-attributes-value.value-type-number { 234 | font-family: 'Noto Sans Mono', monospace; 235 | font-weight: 500; 236 | } 237 | 238 | .detail-info-attributes-value.value-type-boolean { 239 | font-family: 'Noto Sans Mono', monospace; 240 | font-weight: 500; 241 | font-weight: bold; 242 | } 243 | 244 | .detail-info-attributes-value.value-type-string { 245 | font-family: 'Noto Sans Mono', monospace; 246 | font-weight: 500; 247 | color: var(--text-light); 248 | } -------------------------------------------------------------------------------- /venator-app/src/components/event-count-graph.css: -------------------------------------------------------------------------------- 1 | .event-count-graph-bar { 2 | position: absolute; 3 | top: 0%; 4 | bottom: 0%; 5 | 6 | display: flex; 7 | flex-direction: column-reverse; 8 | } 9 | 10 | .event-count-graph-bar-level-0 { 11 | min-height: 1px; 12 | background-color: var(--level-0-bar-color); 13 | } 14 | 15 | .event-count-graph-bar-level-1 { 16 | min-height: 1px; 17 | background-color: var(--level-1-bar-color); 18 | } 19 | 20 | .event-count-graph-bar-level-2 { 21 | min-height: 1px; 22 | background-color: var(--level-2-bar-color); 23 | } 24 | 25 | .event-count-graph-bar-level-3 { 26 | min-height: 1px; 27 | background-color: var(--level-3-bar-color); 28 | } 29 | 30 | .event-count-graph-bar-level-4 { 31 | min-height: 1px; 32 | background-color: var(--level-4-bar-color); 33 | } 34 | 35 | .event-count-graph-bar-level-5 { 36 | min-height: 1px; 37 | background-color: var(--level-5-bar-color); 38 | } -------------------------------------------------------------------------------- /venator-app/src/components/filter-input.css: -------------------------------------------------------------------------------- 1 | .filter-input-container { 2 | font-family: 'Noto Sans Mono', monospace; 3 | font-size: 12px; 4 | font-weight: 500; 5 | background-color: var(--bg-highlight-color); 6 | border: 1px solid var(--border-dark-color); 7 | padding: 2px 4px; 8 | border-radius: 3px; 9 | white-space: pre-wrap; 10 | } 11 | 12 | .filter-input { 13 | line-height: 24px; 14 | white-space: pre-wrap; 15 | } 16 | 17 | .filter-input:focus { 18 | outline: 0px solid transparent; 19 | padding: 2px 5px; 20 | } 21 | 22 | .predicate { 23 | display: inline; 24 | border: 1px solid var(--border-dark-color); 25 | border-radius: 3px; 26 | margin: 2px 0px; 27 | padding: 0px 4px 1px 4px; 28 | } 29 | 30 | .predicate:hover { 31 | border: 1px solid var(--text-normal); 32 | } 33 | 34 | .filter-input:focus .predicate { 35 | margin: 3px 0px; 36 | padding: 0 0px; 37 | border: none; 38 | border-color: transparent; 39 | background-color: transparent; 40 | } 41 | 42 | .spacer { 43 | white-space: pre-wrap; 44 | display: inline-block; 45 | max-width: 4.4px; 46 | } 47 | 48 | .filter-input:focus .spacer { 49 | display: inline; 50 | max-width: fit-content; 51 | } 52 | 53 | .grouper { 54 | white-space: pre-wrap; 55 | font-weight: bold; 56 | margin: 2px 0px; 57 | /* padding: 2px 4px; */ 58 | padding: 2px 5px; 59 | } 60 | 61 | .filter-input:focus .grouper { 62 | display: inline; 63 | margin: 0; 64 | padding: 0; 65 | } 66 | 67 | .level-predicate-0 { 68 | color: var(--level-0-color); 69 | background-color: var(--level-0-bg-color); 70 | border: 1px solid var(--border-normal-color); 71 | } 72 | 73 | .level-predicate-1 { 74 | color: var(--level-1-color); 75 | background-color: var(--level-1-bg-color); 76 | border: 1px solid var(--level-1-bg-color); 77 | } 78 | 79 | .level-predicate-2 { 80 | color: var(--level-2-color); 81 | background-color: var(--level-2-bg-color); 82 | border: 1px solid var(--level-2-bg-color); 83 | } 84 | 85 | .level-predicate-3 { 86 | color: var(--level-3-color); 87 | background-color: var(--level-3-bg-color); 88 | border: 1px solid var(--level-3-bg-color); 89 | } 90 | 91 | .level-predicate-4 { 92 | color: var(--level-4-color); 93 | background-color: var(--level-4-bg-color); 94 | border: 1px solid var(--level-4-bg-color); 95 | } 96 | 97 | .level-predicate-5 { 98 | color: var(--level-5-color); 99 | background-color: var(--level-5-bg-color); 100 | border: 1px solid var(--level-5-bg-color); 101 | } 102 | 103 | .meta-predicate { 104 | background-color: var(--border-light-color); 105 | border-color: var(--border-normal-color); 106 | } 107 | 108 | .predicate.focused { 109 | border-color: transparent; 110 | background-color: transparent; 111 | } 112 | 113 | .predicate.error { 114 | border-color: var(--border-error); 115 | background-color: transparent; 116 | } 117 | 118 | .predicate.error:hover { 119 | border-color: color-mix(in lab, var(--border-error) 70%, black 30%); 120 | } -------------------------------------------------------------------------------- /venator-app/src/components/graph-container.css: -------------------------------------------------------------------------------- 1 | .graph-container { 2 | height: 100px; 3 | 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | } 8 | 9 | .graph-stats { 10 | flex: 0; 11 | color: var(--text-light); 12 | } 13 | 14 | .graph-stats .stat-name { 15 | margin-right: 4px; 16 | } 17 | 18 | .graph-stats .stat-value { 19 | font-weight: 500; 20 | color: var(--text-normal); 21 | margin-right: 8px; 22 | } 23 | 24 | .graph { 25 | flex: 1; 26 | 27 | position: relative; 28 | 29 | display: flex; 30 | flex-direction: row; 31 | align-items: flex-end; 32 | gap: 4px; 33 | overflow-y: clip; 34 | } 35 | 36 | .graph-x-axis { 37 | flex: 0; 38 | color: var(--text-light); 39 | position: relative; 40 | min-height: 20px; 41 | } 42 | 43 | .graph-x-axis-marker { 44 | flex: 0; 45 | color: var(--text-light); 46 | position: absolute; 47 | translate: -50% 0%; 48 | } 49 | 50 | .graph-y-lines { 51 | position: absolute; 52 | width: 100%; 53 | height: 100%; 54 | 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: space-between; 58 | } 59 | 60 | .graph-y-line { 61 | width: 100%; 62 | border-bottom: 1px solid var(--border-light-color); 63 | } 64 | 65 | .graph-cursor { 66 | position: absolute; 67 | min-width: 3px; 68 | height: 100%; 69 | 70 | background-color: gray; 71 | opacity: 50%; 72 | pointer-events: none; 73 | } 74 | 75 | .graph-selection { 76 | position: absolute; 77 | height: 100%; 78 | 79 | background-color: gray; 80 | opacity: 25%; 81 | } -------------------------------------------------------------------------------- /venator-app/src/components/screen-header.css: -------------------------------------------------------------------------------- 1 | .screen-header { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | align-items: flex-end; 6 | } 7 | 8 | .screen-header h1 { 9 | margin-top: 4px; 10 | margin-bottom: 0px; 11 | } 12 | 13 | [data-theme="dark"] .screen-header h1 img { 14 | filter: invert(90%); 15 | } 16 | 17 | .screen-header .sub-header { 18 | margin-left: 8px; 19 | font-size: 12px; 20 | font-weight: normal; 21 | color: var(--text-light); 22 | } 23 | 24 | .screen-header .sub-header.warn-text { 25 | color: var(--text-warn); 26 | } 27 | 28 | .screen-header .sub-header.error-text { 29 | color: var(--text-error); 30 | } -------------------------------------------------------------------------------- /venator-app/src/components/screen-header.tsx: -------------------------------------------------------------------------------- 1 | import { ScreenKind, Timespan } from "../models"; 2 | import { TimeControls } from "./time-controls"; 3 | import { Timestamp } from "../invoke"; 4 | 5 | import "./screen-header.css"; 6 | import eventIcon from '../assets/event.svg'; 7 | import spanIcon from '../assets/span.svg'; 8 | import traceIcon from '../assets/trace.svg'; 9 | 10 | export type ScreenHeaderProps = { 11 | screenKind: ScreenKind, 12 | timespan: Timespan | null, 13 | setTimespan: (timespan: Timespan) => void, 14 | timeControlsEnabled: boolean, 15 | count: [number, boolean], 16 | countThresholds: [number, number], 17 | live: boolean, 18 | setLive: (live: boolean) => void, 19 | getTimestampBefore: (timestamp: Timestamp) => Promise, 20 | getTimestampAfter: (timestamp: Timestamp) => Promise, 21 | }; 22 | 23 | export function ScreenHeader(props: ScreenHeaderProps) { 24 | function headerText() { 25 | if (props.screenKind == 'events') { 26 | return 'Events'; 27 | } else if (props.screenKind == 'spans') { 28 | return 'Spans'; 29 | } else { 30 | return 'Trace'; 31 | } 32 | } 33 | 34 | function headerIcon() { 35 | if (props.screenKind == 'events') { 36 | return eventIcon; 37 | } else if (props.screenKind == 'spans') { 38 | return spanIcon; 39 | } else { 40 | return traceIcon; 41 | } 42 | } 43 | 44 | let countText = () => { 45 | let [count, exact] = props.count; 46 | return `${count}${exact && !props.live ? '' : '+'}`; 47 | }; 48 | 49 | let countTextClasses = () => { 50 | let [count] = props.count; 51 | if (count >= props.countThresholds[1]) { 52 | return { "error-text": true } 53 | } 54 | if (count >= props.countThresholds[0]) { 55 | return { "warn-text": true } 56 | } 57 | return {}; 58 | } 59 | 60 | return (
61 |

62 | 63 |   64 | {headerText()} 65 | {countText()} in view 66 |

67 | props.setTimespan(t)} 70 | enabled={props.timeControlsEnabled} 71 | live={props.live} 72 | setLive={props.setLive} 73 | getTimestampBefore={props.getTimestampBefore} 74 | getTimestampAfter={props.getTimestampAfter} 75 | /> 76 |
); 77 | } 78 | -------------------------------------------------------------------------------- /venator-app/src/components/span-graph.css: -------------------------------------------------------------------------------- 1 | .span-graph-bar { 2 | min-width: 1px; 3 | height: 4px; 4 | position: absolute; 5 | } 6 | 7 | .span-graph-bar.level-0 { 8 | background-color: var(--level-0-bar-color); 9 | } 10 | 11 | .span-graph-bar.level-1 { 12 | background-color: var(--level-1-bar-color); 13 | } 14 | 15 | .span-graph-bar.level-2 { 16 | background-color: var(--level-2-bar-color); 17 | } 18 | 19 | .span-graph-bar.level-3 { 20 | background-color: var(--level-3-bar-color); 21 | } 22 | 23 | .span-graph-bar.level-4 { 24 | background-color: var(--level-4-bar-color); 25 | } 26 | 27 | .span-graph-bar.level-5 { 28 | background-color: var(--level-5-bar-color); 29 | } 30 | 31 | .span-count-graph-bar { 32 | position: absolute; 33 | top: 0%; 34 | bottom: 0%; 35 | 36 | display: flex; 37 | flex-direction: column-reverse; 38 | } 39 | 40 | .span-count-graph-bar-level-0 { 41 | min-height: 1px; 42 | background-color: var(--level-0-bar-color); 43 | } 44 | 45 | .span-count-graph-bar-level-1 { 46 | min-height: 1px; 47 | background-color: var(--level-1-bar-color); 48 | } 49 | 50 | .span-count-graph-bar-level-2 { 51 | min-height: 1px; 52 | background-color: var(--level-2-bar-color); 53 | } 54 | 55 | .span-count-graph-bar-level-3 { 56 | min-height: 1px; 57 | background-color: var(--level-3-bar-color); 58 | } 59 | 60 | .span-count-graph-bar-level-4 { 61 | min-height: 1px; 62 | background-color: var(--level-4-bar-color); 63 | } 64 | 65 | .span-count-graph-bar-level-5 { 66 | min-height: 1px; 67 | background-color: var(--level-5-bar-color); 68 | } -------------------------------------------------------------------------------- /venator-app/src/components/tab-bar.css: -------------------------------------------------------------------------------- 1 | .tabbar { 2 | height: 28px; 3 | background: var(--bg-tab-color); 4 | padding: 0 20px; 5 | user-select: none; 6 | 7 | display: flex; 8 | flex-direction: row; 9 | align-items: flex-end; 10 | } 11 | 12 | .tabs { 13 | flex: 1; 14 | gap: 1px; 15 | 16 | display: flex; 17 | flex-direction: row; 18 | align-items: flex-end; 19 | overflow-x: hidden; 20 | overflow-y: clip; 21 | } 22 | 23 | .tab { 24 | position: relative; 25 | flex: 0 1 220px; 26 | min-width: 10px; 27 | height: 21px; 28 | background-color: color-mix(in lab, var(--bg-screen-color) 90%, black 10%); 29 | border-bottom: 1px solid color-mix(in lab, var(--bg-screen-color) 80%, black 20%); 30 | padding: 0px 8px; 31 | 32 | text-wrap: nowrap; 33 | overflow: hidden; 34 | display: flex; 35 | align-items: center; 36 | } 37 | 38 | .tab:not(.active):hover { 39 | background-color: color-mix(in lab, var(--bg-screen-color) 85%, black 15%); 40 | } 41 | 42 | .tab.active { 43 | flex: 0 0 220px; 44 | height: 24px; 45 | background-color: var(--bg-screen-color); 46 | border-bottom: none; 47 | } 48 | 49 | .tab span { 50 | display: inline-block; 51 | flex: 1; 52 | min-width: 6px; 53 | text-overflow: ellipsis; 54 | overflow: clip; 55 | white-space: nowrap; 56 | } 57 | 58 | .tab button { 59 | border: none; 60 | background-color: transparent; 61 | padding: 0px; 62 | padding-left: 4px; 63 | font-size: 12px; 64 | color: var(--text-click-inactive-color); 65 | 66 | display: none; 67 | } 68 | 69 | .tab button:hover { 70 | color: var(--text-click-active-color); 71 | } 72 | 73 | .tab button:active { 74 | font-weight: bold; 75 | } 76 | 77 | .tab.active button { 78 | display: block; 79 | } 80 | 81 | .new-tab { 82 | flex: 0; 83 | background: transparent; 84 | border: none; 85 | opacity: 75%; 86 | font-size: 20px; 87 | margin-left: 2px; 88 | } 89 | 90 | .new-tab img { 91 | width: 16px; 92 | height: 16px; 93 | } 94 | 95 | .new-tab:hover { 96 | opacity: 100%; 97 | } -------------------------------------------------------------------------------- /venator-app/src/components/table.css: -------------------------------------------------------------------------------- 1 | #table { 2 | flex: 1; 3 | max-height: 100%; 4 | border: 1px solid var(--border-normal-color); 5 | border-top: none; 6 | overflow-y: auto; 7 | } 8 | 9 | #table-headers { 10 | top: 0; 11 | position: sticky; 12 | display: grid; 13 | /* grid-template-columns: set by column definitions */ 14 | grid-template-rows: 24px; 15 | z-index: 1; 16 | } 17 | 18 | #table-inner { 19 | position: relative; 20 | width: 100%; 21 | display: grid; 22 | /* grid-template-columns: set by column definitions */ 23 | grid-auto-rows: 21px; 24 | } 25 | 26 | #table .header { 27 | position: sticky; 28 | background-color: var(--bg-screen-color); 29 | border-bottom: 1px solid var(--border-light-color); 30 | border-right: 1px solid var(--border-light-color); 31 | font-size: 12px; 32 | font-weight: bold; 33 | text-align: left; 34 | 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: space-between; 38 | padding: 0 4px; 39 | padding-bottom: 2px; 40 | padding-top: 1px; 41 | } 42 | 43 | #table .header .header-text { 44 | width: fit-content; 45 | border: 1px solid transparent; 46 | border-radius: 3px; 47 | overflow: hidden; 48 | } 49 | 50 | #table .header .header-text:focus-within { 51 | font-weight: 600; 52 | background-color: var(--bg-highlight-color); 53 | padding: 0 8px; 54 | border-color: var(--border-dark-color); 55 | } 56 | 57 | #table .header .header-text div[contenteditable]:focus { 58 | outline: 0px solid transparent; 59 | } 60 | 61 | #table .header button { 62 | flex: 0; 63 | border: none; 64 | color: var(--text-click-inactive-color); 65 | background-color: transparent; 66 | } 67 | 68 | #table .header button:hover { 69 | color: var(--text-click-active-color); 70 | } 71 | 72 | #table .header .grabber { 73 | position: absolute; 74 | right: calc(0% - 3px); 75 | top: 0%; 76 | height: 100%; 77 | width: 5px; 78 | cursor: ew-resize; 79 | } 80 | 81 | #table .header.collapsable img { 82 | opacity: 67%; 83 | } 84 | 85 | #table .header.collapsable img:hover { 86 | opacity: 100%; 87 | } 88 | 89 | #table .data { 90 | position: relative; 91 | background-color: var(--bg-highlight-color); 92 | border-bottom: 1px solid var(--border-light-color); 93 | border-right: 1px solid var(--border-light-color); 94 | padding: 0 4px; 95 | font-family: 'Noto Sans Mono', monospace; 96 | font-size: 12px; 97 | font-weight: 500; 98 | font-optical-sizing: auto; 99 | white-space: nowrap; 100 | overflow: clip; 101 | } 102 | 103 | #table .data.selected { 104 | background-color: var(--bg-secondary-color); 105 | } 106 | 107 | #table .data:not(.selected).hovered { 108 | background-color: color-mix(in lab, var(--bg-highlight-color) 85%, gray 15%); 109 | } 110 | 111 | #table .data .level-0, 112 | #table .data .level-1, 113 | #table .data .level-2, 114 | #table .data .level-3, 115 | #table .data .level-4, 116 | #table .data .level-5 { 117 | margin-top: 3px; 118 | width: 7px; 119 | height: 14px; 120 | border-radius: 4px; 121 | } 122 | 123 | #table .data .level-0 { 124 | width: 5px; 125 | height: 12px; 126 | margin-left: 1px; 127 | background-color: var(--level-0-bg-color); 128 | outline: 1px solid var(--border-normal-color); 129 | } 130 | 131 | #table .data .level-1 { 132 | background-color: var(--level-1-bg-color); 133 | } 134 | 135 | #table .data .level-2 { 136 | background-color: var(--level-2-bg-color); 137 | } 138 | 139 | [data-theme="dark"] #table .data .level-2 { 140 | background-color: var(--level-2-bg-color); 141 | outline: 1px solid var(--border-normal-color); 142 | } 143 | 144 | #table .data .level-3 { 145 | background-color: var(--level-3-bg-color); 146 | } 147 | 148 | #table .data .level-4 { 149 | background-color: var(--level-4-bg-color); 150 | } 151 | 152 | #table .data .level-5 { 153 | background-color: var(--level-5-bg-color); 154 | } 155 | 156 | #table .data .time-bar { 157 | position: absolute; 158 | } 159 | 160 | #table .data .time-bar-span { 161 | top: 10%; 162 | height: 80%; 163 | min-width: 3px; 164 | border-left: 1px solid black; 165 | border-right: 1px solid black; 166 | } 167 | 168 | #table .data .time-bar-event { 169 | top: 30%; 170 | height: 40%; 171 | min-width: 5px; 172 | } 173 | 174 | #table .data .time-bar-span.time-bar-0 { 175 | border-color: var(--level-0-bar-color); 176 | background-image: linear-gradient(transparent 0 24%, 177 | var(--level-0-bar-color) 24% 30%, 178 | var(--level-0-bg-color) 30% 70%, 179 | var(--level-0-bar-color) 70% 76%, 180 | transparent 76% 100%); 181 | } 182 | 183 | #table .data .time-bar-span.time-bar-1 { 184 | border-color: var(--level-1-bar-color); 185 | background-image: linear-gradient(transparent 0 24%, 186 | var(--level-1-bar-color) 24% 30%, 187 | var(--level-1-bg-color) 30% 70%, 188 | var(--level-1-bar-color) 70% 76%, 189 | transparent 76% 100%); 190 | } 191 | 192 | #table .data .time-bar-span.time-bar-2 { 193 | border-color: var(--level-2-bar-color); 194 | background-image: linear-gradient(transparent 0 24%, 195 | var(--level-2-bar-color) 24% 30%, 196 | var(--level-2-bg-color) 30% 70%, 197 | var(--level-2-bar-color) 70% 76%, 198 | transparent 76% 100%); 199 | } 200 | 201 | #table .data .time-bar-span.time-bar-3 { 202 | border-color: var(--level-3-bar-color); 203 | background-image: linear-gradient(transparent 0 24%, 204 | var(--level-3-bar-color) 24% 30%, 205 | var(--level-3-bg-color) 30% 70%, 206 | var(--level-3-bar-color) 70% 76%, 207 | transparent 76% 100%); 208 | } 209 | 210 | #table .data .time-bar-span.time-bar-4 { 211 | border-color: var(--level-4-bar-color); 212 | background-image: linear-gradient(transparent 0 24%, 213 | var(--level-4-bar-color) 24% 30%, 214 | var(--level-4-bg-color) 30% 70%, 215 | var(--level-4-bar-color) 70% 76%, 216 | transparent 76% 100%); 217 | } 218 | 219 | #table .data .time-bar-span.time-bar-5 { 220 | border-color: var(--level-5-bar-color); 221 | background-image: linear-gradient(transparent 0 24%, 222 | var(--level-5-bar-color) 24% 30%, 223 | var(--level-5-bg-color) 30% 70%, 224 | var(--level-5-bar-color) 70% 76%, 225 | transparent 76% 100%); 226 | } 227 | 228 | #table .data .time-bar-event.time-bar-0 { 229 | background-color: var(--level-0-bg-color); 230 | outline: 1px solid var(--level-0-bar-color); 231 | } 232 | 233 | #table .data .time-bar-event.time-bar-1 { 234 | background-color: var(--level-1-bg-color); 235 | outline: 1px solid var(--level-1-bg-color); 236 | } 237 | 238 | #table .data .time-bar-event.time-bar-2 { 239 | background-color: var(--level-2-bg-color); 240 | outline: 1px solid var(--level-2-bar-color); 241 | } 242 | 243 | #table .data .time-bar-event.time-bar-3 { 244 | background-color: var(--level-3-bg-color); 245 | outline: 1px solid var(--level-3-bar-color); 246 | } 247 | 248 | #table .data .time-bar-event.time-bar-4 { 249 | background-color: var(--level-4-bg-color); 250 | outline: 1px solid var(--level-4-bar-color); 251 | } 252 | 253 | #table .data .time-bar-event.time-bar-5 { 254 | background-color: var(--level-5-bg-color); 255 | outline: 1px solid var(--level-5-bar-color); 256 | } 257 | 258 | #table .data.collapser { 259 | font-size: 16px; 260 | color: var(--text-click-inactive-color); 261 | user-select: none; 262 | padding: 0 2px; 263 | } 264 | 265 | #table .data.collapser:hover { 266 | color: var(--text-click-active-color); 267 | } 268 | 269 | #table .data.collapser img { 270 | opacity: 67%; 271 | } 272 | 273 | #table .data.collapser img:hover { 274 | opacity: 100%; 275 | } 276 | 277 | #table .data:hover { 278 | outline: 1px solid var(--border-dark-color); 279 | border-right: 1px solid var(--border-dark-color); 280 | border-bottom: 1px solid var(--border-dark-color); 281 | } -------------------------------------------------------------------------------- /venator-app/src/components/time-controls.css: -------------------------------------------------------------------------------- 1 | .time-controls { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 4px; 5 | } 6 | 7 | .time-control { 8 | display: flex; 9 | flex-direction: row; 10 | } 11 | 12 | .time-controls button { 13 | border: 1px solid var(--border-dark-color); 14 | border-radius: 3px; 15 | background-color: var(--bg-highlight-color); 16 | } 17 | 18 | .time-controls:not(.enabled) button { 19 | background-color: var(--bg-secondary-color); 20 | } 21 | 22 | .time-controls.enabled button:hover { 23 | background-color: color-mix(in lab, var(--bg-highlight-color) 85%, gray 15%); 24 | } 25 | 26 | .time-controls.enabled button:active { 27 | background-color: color-mix(in lab, var(--bg-highlight-color) 75%, gray 25%); 28 | } 29 | 30 | .time-controls .time-control .left { 31 | border-radius: 3px 0px 0px 3px; 32 | } 33 | 34 | .time-controls .time-control .main { 35 | border-top: 1px solid var(--border-dark-color); 36 | border-bottom: 1px solid var(--border-dark-color); 37 | padding: 0 8px; 38 | background-color: var(--bg-highlight-color); 39 | text-align: center; 40 | overflow: hidden; 41 | white-space: nowrap; 42 | } 43 | 44 | .time-controls .time-control .main.error { 45 | border-top: 1px solid var(--border-error); 46 | border-bottom: 1px solid var(--border-error); 47 | } 48 | 49 | .time-controls .time-control .main[contenteditable]:focus { 50 | outline: 0px solid transparent; 51 | } 52 | 53 | .time-controls:not(.enabled) .time-control .main { 54 | background-color: var(--bg-secondary-color); 55 | } 56 | 57 | .time-controls .time-control .right { 58 | border-radius: 0px 3px 3px 0px; 59 | } 60 | 61 | .time-controls button.live { 62 | padding-top: 2px; 63 | padding-bottom: 0px; 64 | } 65 | 66 | [data-theme="dark"] .time-controls button.live img { 67 | filter: invert(90%); 68 | } 69 | 70 | .time-controls button.live.active { 71 | animation: pulse 1s infinite; 72 | } 73 | 74 | @keyframes pulse { 75 | 76 | 0%, 77 | 49%, 78 | 100% { 79 | border-color: var(--level-3-bg-color); 80 | } 81 | 82 | 50%, 83 | 99% { 84 | border-color: var(--border-dark-color); 85 | } 86 | } -------------------------------------------------------------------------------- /venator-app/src/components/trace-graph.css: -------------------------------------------------------------------------------- 1 | .trace-graph-bar { 2 | min-width: 1px; 3 | height: 4px; 4 | position: absolute; 5 | } 6 | 7 | .trace-graph-bar.level-0 { 8 | background-color: var(--level-0-bar-color); 9 | } 10 | 11 | .trace-graph-bar.level-1 { 12 | background-color: var(--level-1-bar-color); 13 | } 14 | 15 | .trace-graph-bar.level-2 { 16 | background-color: var(--level-2-bar-color); 17 | } 18 | 19 | .trace-graph-bar.level-3 { 20 | background-color: var(--level-3-bar-color); 21 | } 22 | 23 | .trace-graph-bar.level-4 { 24 | background-color: var(--level-4-bar-color); 25 | } 26 | 27 | .trace-graph-bar.level-5 { 28 | background-color: var(--level-5-bar-color); 29 | } -------------------------------------------------------------------------------- /venator-app/src/components/trace-graph.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect, createSignal, For } from "solid-js"; 2 | import { Span, Event } from "../invoke"; 3 | import { PaginationFilter, Timespan } from "../models"; 4 | 5 | import "./trace-graph.css"; 6 | import { GraphContainer } from "./graph-container"; 7 | 8 | export type TraceGraphProps = { 9 | timespan: Timespan | null, 10 | hoveredRow: Event | Span | null, 11 | 12 | setCount: (count: [number, boolean]) => void, 13 | 14 | getEntries: (filter: PaginationFilter) => Promise<(Event | Span)[]>, 15 | }; 16 | 17 | export function TraceGraph(props: TraceGraphProps) { 18 | const [entries, setEntries] = createSignal<(Event | Span)[]>([]); 19 | 20 | createEffect(async () => { 21 | let current_timespan = props.timespan; 22 | if (current_timespan == null) { 23 | return; 24 | } 25 | 26 | let entries = await props.getEntries({ order: 'asc' }); 27 | setEntries(entries); 28 | }); 29 | 30 | function barHeightMax() { 31 | return Math.max(entries().length, 10); 32 | } 33 | 34 | function position(entry: Event | Span, i: number): { top?: string, left: string, right?: string, height: string } { 35 | let current_timespan = props.timespan; 36 | if (current_timespan == null) { 37 | return {} as any; 38 | } 39 | 40 | let [start, end] = current_timespan; 41 | let duration = end - start; 42 | 43 | let height = Math.min(Math.max(60 / barHeightMax(), 2), 4); 44 | let top = i / barHeightMax() * 56 / 60; 45 | 46 | if ((entry as any).timestamp != undefined) { 47 | let event = entry as Event; 48 | let left = (event.timestamp - start) / duration; 49 | 50 | return { 51 | top: `${top * 100}%`, 52 | left: `${left * 100}%`, 53 | height: `${height}px`, 54 | }; 55 | } else { 56 | let span = entry as Span; 57 | let left = (span.created_at - start) / duration; 58 | let right = (span.closed_at == null) ? 0.0 : (end - span.closed_at) / duration; 59 | 60 | return { 61 | top: `${top * 100}%`, 62 | left: `${left * 100}%`, 63 | right: `${right * 100}%`, 64 | height: `${height}px`, 65 | }; 66 | } 67 | } 68 | 69 | return { }} height={barHeightMax()}> 70 | 71 | {(entry, i) => ()} 72 | 73 | ; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /venator-app/src/context/collapsable.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "solid-js"; 2 | 3 | import { FullSpanId } from "../invoke"; 4 | 5 | export const CollapsableContext = createContext(); 6 | 7 | export type Collapsable = { 8 | isCollapsed: (id: FullSpanId) => boolean, 9 | collapse: (id: FullSpanId, collapse: boolean) => void, 10 | areAnyCollapsed: () => boolean, 11 | expandAll: () => void, 12 | collapseAll: () => void, 13 | } 14 | -------------------------------------------------------------------------------- /venator-app/src/context/navigation.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "solid-js"; 2 | 3 | import { ColumnData, ScreenData } from "../App"; 4 | 5 | export const NavigationContext = createContext(); 6 | 7 | export type Navigation = { 8 | createTab: (data: ScreenData, columns: ColumnData, navigate: boolean) => void, 9 | removeTab: (idx: number) => void, 10 | removeAllOtherTabs: (idx: number) => void, 11 | moveTab: (fromIdx: number, toIdx: number) => void, 12 | activateTab: (idx: number) => void, 13 | } 14 | -------------------------------------------------------------------------------- /venator-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from "solid-js/web"; 3 | import App from "./App"; 4 | 5 | render(() => (), document.getElementById("root")!); 6 | -------------------------------------------------------------------------------- /venator-app/src/invoke.tsx: -------------------------------------------------------------------------------- 1 | import { Channel, invoke } from "@tauri-apps/api/core"; 2 | 3 | export type Timestamp = number; 4 | export type InstanceId = string; 5 | export type FullSpanId = string; 6 | export type TraceRoot = string; 7 | export type Level = 0 | 1 | 2 | 3 | 4 | 5; 8 | export type SourceKind = 'tracing' | 'opentelemetry'; 9 | 10 | export type Stats = { 11 | start?: Timestamp; 12 | end?: Timestamp; 13 | total_spans: number; 14 | total_events: number; 15 | }; 16 | 17 | export type Comparator = 'Gt' | 'Gte' | 'Eq' | 'Lt' | 'Lte'; 18 | 19 | export type ValuePredicate = 20 | { value_kind: 'not', value: ValuePredicate } | 21 | { value_kind: 'comparison', value: [Comparator, string] } | 22 | { value_kind: 'and', value: ValuePredicate[] } | 23 | { value_kind: 'or', value: ValuePredicate[] }; 24 | 25 | export type Input = (ValidFilterPredicate | InvalidFilterPredicate) & { editable?: false }; 26 | 27 | export type ValidFilterPredicate = { input: 'valid' } & FilterPredicate; 28 | 29 | export type InvalidFilterPredicate = { input: 'invalid', text: string, error: string }; 30 | 31 | export type FilterPredicate = 32 | { predicate_kind: 'single', predicate: FilterPredicateSingle } | 33 | { predicate_kind: 'and', predicate: Input[] } | 34 | { predicate_kind: 'or', predicate: Input[] }; 35 | 36 | export type FilterPredicateSingle = { 37 | text: string, 38 | property_kind?: string, 39 | property: string, 40 | } & ValuePredicate; 41 | 42 | export type EventFilter = { 43 | filter: FilterPredicate[]; 44 | order: 'asc' | 'desc'; 45 | limit?: number; 46 | start: Timestamp | null; 47 | end: Timestamp | null; 48 | previous?: Timestamp; 49 | }; 50 | 51 | export type CountFilter = { 52 | filter: FilterPredicate[]; 53 | start: Timestamp; 54 | end: Timestamp; 55 | }; 56 | 57 | export type Event = { 58 | kind: SourceKind, 59 | ancestors: Ancestor[]; 60 | timestamp: Timestamp; 61 | content: string; 62 | namespace: string | null; 63 | function: string | null; 64 | level: Level; 65 | file?: string; 66 | attributes: Attribute[]; 67 | }; 68 | 69 | export type SpanFilter = { 70 | filter: FilterPredicate[]; 71 | order: 'asc' | 'desc'; 72 | limit?: number; 73 | start: Timestamp | null; 74 | end: Timestamp | null; 75 | previous?: Timestamp; 76 | }; 77 | 78 | export type Span = { 79 | kind: SourceKind, 80 | id: FullSpanId, 81 | ancestors: Ancestor[]; 82 | created_at: Timestamp; 83 | closed_at: Timestamp | null; 84 | busy: number | null; 85 | name: string; 86 | namespace: string | null; 87 | function: string | null; 88 | level: Level; 89 | file?: string; 90 | attributes: Attribute[]; 91 | }; 92 | 93 | export type Ancestor = { 94 | id: FullSpanId, 95 | name: string, 96 | }; 97 | 98 | export type Attribute = { 99 | name: string; 100 | value: string; 101 | type: 'f64' | 'i64' | 'u64' | 'i128' | 'u128' | 'bool' | 'string'; 102 | } & ({ source: 'resource' } 103 | | { source: 'span', span_id: FullSpanId } 104 | | { source: 'inherent' }); 105 | 106 | export type AppStatus = { 107 | ingress_message: string; 108 | ingress_error: string; 109 | ingress_bytes_per_second: number; 110 | dataset_name: string; 111 | engine_load: number; 112 | }; 113 | 114 | export type DeleteMetrics = { 115 | spans: number; 116 | span_events: number; 117 | events: number; 118 | }; 119 | 120 | export type Session = { 121 | tabs: SessionTab[]; 122 | }; 123 | 124 | export type SessionTab = { 125 | kind: 'events' | 'spans' | 'trace'; 126 | start: Timestamp; 127 | end: Timestamp; 128 | filter: string; 129 | columns: string[]; 130 | }; 131 | 132 | export type SubscriptionResponse = { 133 | kind: 'add'; 134 | entity: T; 135 | } | { 136 | kind: 'remove'; 137 | entity: Timestamp; 138 | }; 139 | 140 | export async function getStats(): Promise { 141 | console.debug("invoking 'get_stats'"); 142 | return await invoke("get_stats", {}); 143 | } 144 | 145 | export async function getEvents(filter: EventFilter): Promise { 146 | console.debug("invoking 'get_events'"); 147 | return await invoke("get_events", filter); 148 | } 149 | 150 | export async function getEventCount(filter: CountFilter): Promise { 151 | console.debug("invoking 'get_event_count'"); 152 | return await invoke("get_event_count", filter); 153 | } 154 | 155 | export async function parseEventFilter(filter: string): Promise { 156 | console.debug("invoking 'parse_event_filter'"); 157 | return await invoke("parse_event_filter", { filter }); 158 | } 159 | 160 | export async function getSpans(filter: SpanFilter): Promise { 161 | console.debug("invoking 'get_spans'"); 162 | return await invoke("get_spans", filter); 163 | } 164 | 165 | export async function getSpanCount(filter: CountFilter): Promise { 166 | console.debug("invoking 'get_span_count'"); 167 | return await invoke("get_span_count", filter); 168 | } 169 | 170 | export async function parseSpanFilter(filter: string): Promise { 171 | console.debug("invoking 'parse_span_filter'"); 172 | return await invoke("parse_span_filter", { filter }); 173 | } 174 | 175 | export async function deleteEntities(start: Timestamp | null, end: Timestamp | null, inside: boolean, dryRun: boolean): Promise { 176 | console.debug("invoking 'delete_entities'"); 177 | return await invoke("delete_entities", { start, end, inside, dryRun }); 178 | } 179 | 180 | export async function subscribeToSpans(filter: FilterPredicate[], channel: Channel>): Promise { 181 | console.debug("invoking 'subscribe_to_spans'"); 182 | return await invoke("subscribe_to_spans", { filter, channel }); 183 | } 184 | 185 | export async function unsubscribeFromSpans(id: number): Promise { 186 | console.debug("invoking 'unsubscribe_from_spans'"); 187 | return await invoke("unsubscribe_from_spans", { id }); 188 | } 189 | 190 | export async function subscribeToEvents(filter: FilterPredicate[], channel: Channel>): Promise { 191 | console.debug("invoking 'subscribe_to_events'"); 192 | return await invoke("subscribe_to_events", { filter, channel }); 193 | } 194 | 195 | export async function unsubscribeFromEvents(id: number): Promise { 196 | console.debug("invoking 'unsubscribe_from_events'"); 197 | return await invoke("unsubscribe_from_events", { id }); 198 | } 199 | 200 | export async function createAttributeIndex(name: string): Promise { 201 | console.debug("invoking 'create_attribute_index'"); 202 | return await invoke("create_attribute_index", { name }); 203 | } 204 | 205 | export async function removeAttributeIndex(name: string): Promise { 206 | console.debug("invoking 'remove_attribute_index'"); 207 | return await invoke("remove_attribute_index", { name }); 208 | } 209 | 210 | export async function getStatus(): Promise { 211 | console.debug("invoking 'get_status'"); 212 | return await invoke("get_status"); 213 | } 214 | 215 | export async function loadSession(): Promise { 216 | console.debug("invoking 'load_session'"); 217 | return await invoke("load_session"); 218 | } 219 | 220 | export async function saveSession(session: Session): Promise { 221 | console.debug("invoking 'save_session'"); 222 | return await invoke("save_session", { session }); 223 | } 224 | -------------------------------------------------------------------------------- /venator-app/src/models.tsx: -------------------------------------------------------------------------------- 1 | import { Span, Timestamp, Event, FullSpanId, Level } from "./invoke"; 2 | 3 | export type ScreenKind = "events" | "spans" | "trace"; 4 | 5 | export type Counts = [number, number, number, number, number, number]; 6 | export type Timespan = [Timestamp, Timestamp]; 7 | 8 | export type PartialFilter = { 9 | order: 'asc' | 'desc'; 10 | limit?: number; 11 | start: Timestamp | null; 12 | end: Timestamp | null; 13 | previous?: Timestamp; 14 | }; 15 | 16 | export type PartialCountFilter = { 17 | start: Timestamp; 18 | end: Timestamp; 19 | }; 20 | 21 | export type PaginationFilter = { 22 | order: 'asc' | 'desc'; 23 | limit?: number; 24 | previous?: Timestamp; 25 | }; 26 | 27 | export type Entry = Event | Span; 28 | 29 | export type PositionedSpan = { 30 | id: FullSpanId, 31 | created_at: Timestamp, 32 | closed_at: Timestamp | null, 33 | level: Level, 34 | slot: number, 35 | }; 36 | 37 | export type PositionedConnection = { 38 | id: FullSpanId, 39 | connected_at: Timestamp, 40 | disconnected_at: Timestamp | null, 41 | slot: number, 42 | }; 43 | -------------------------------------------------------------------------------- /venator-app/src/screens/events-screen.css: -------------------------------------------------------------------------------- 1 | .events-screen { 2 | height: 100%; 3 | 4 | box-sizing: border-box; 5 | padding: 8px; 6 | margin: 0px; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 4px; 10 | } 11 | 12 | .events-screen-content { 13 | height: 0px; 14 | flex: 1; 15 | 16 | display: flex; 17 | flex-direction: row; 18 | } -------------------------------------------------------------------------------- /venator-app/src/screens/events-screen.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, Show } from "solid-js"; 2 | 3 | import { EventDetailPane } from "../components/detail-pane"; 4 | import { EventCountGraph } from "../components/event-count-graph"; 5 | import { FilterInput } from "../components/filter-input"; 6 | import { ScreenHeader } from "../components/screen-header"; 7 | import { Event, Input, parseEventFilter, Timestamp } from '../invoke'; 8 | import { Counts, PartialCountFilter, PartialFilter, Timespan } from "../models"; 9 | import { ColumnDef, CONTENT, parseEventColumn, Table } from "../components/table"; 10 | 11 | import './events-screen.css'; 12 | 13 | export type EventsScreenProps = { 14 | raw_filter: Input[], 15 | filter: Input[], 16 | setFilter: (filter: Input[]) => void, 17 | addToFilter: (filter: Input[]) => void, 18 | timespan: Timespan, 19 | setTimespan: (timespan: Timespan) => void, 20 | 21 | columns: ColumnDef[], 22 | columnWidths: string[], 23 | columnUpdate: (i: number, def: ColumnDef) => void, 24 | columnUpdateWidth: (i: number, width: string) => void, 25 | columnMove: (i: number, to: number) => void, 26 | columnInsert: (i: number, def: ColumnDef) => void, 27 | columnRemove: (i: number) => void, 28 | 29 | getEvents: (filter: PartialFilter, wait?: boolean) => Promise, 30 | getEventCounts: (filter: PartialCountFilter, wait?: boolean, cache?: boolean) => Promise, 31 | 32 | live: boolean, 33 | setLive: (live: boolean) => void, 34 | 35 | selected: Event | null, 36 | setSelected: (e: Event | null) => void, 37 | }; 38 | 39 | export function EventsScreen(props: EventsScreenProps) { 40 | const [hoveredRow, setHoveredRow] = createSignal(null); 41 | const [count, setCount] = createSignal<[number, boolean]>([0, false]); 42 | 43 | async function getTimestampBefore(timestamp: Timestamp) { 44 | let events = (await props.getEvents({ 45 | order: 'desc', 46 | start: null, 47 | end: timestamp, 48 | limit: 1, 49 | }))!; 50 | 51 | if (events.length == 0) { 52 | return null; 53 | } 54 | 55 | return events[0].timestamp; 56 | } 57 | 58 | async function getTimestampAfter(timestamp: Timestamp) { 59 | let events = (await props.getEvents({ 60 | order: 'asc', 61 | start: timestamp, 62 | end: null, 63 | limit: 1, 64 | }))!; 65 | 66 | if (events.length == 0) { 67 | return null; 68 | } 69 | 70 | return events[0].timestamp; 71 | } 72 | 73 | return (
74 | props.setTimespan(t)} 78 | count={count()} 79 | countThresholds={[1000, 10000]} 80 | timeControlsEnabled={true} 81 | live={props.live} 82 | setLive={props.setLive} 83 | getTimestampBefore={getTimestampBefore} 84 | getTimestampAfter={getTimestampAfter} 85 | /> 86 | 87 | 88 | 89 | props.setTimespan(t)} 93 | getEventCounts={props.getEventCounts} 94 | setCount={setCount} 95 | hoveredRow={hoveredRow()} 96 | /> 97 | 98 |
99 | 100 | timespan={props.timespan} 101 | columns={props.columns} 102 | columnWidths={props.columnWidths} 103 | columnUpdate={props.columnUpdate} 104 | columnUpdateWidth={props.columnUpdateWidth} 105 | columnMove={props.columnMove} 106 | columnInsert={props.columnInsert} 107 | columnRemove={props.columnRemove} 108 | columnDefault={CONTENT} 109 | columnMin={3} 110 | selectedRow={props.selected} 111 | setSelectedRow={props.setSelected} 112 | hoveredRow={hoveredRow()} 113 | setHoveredRow={setHoveredRow} 114 | getEntries={props.getEvents} 115 | addToFilter={async f => props.addToFilter(await parseEventFilter(f))} 116 | columnParser={parseEventColumn} 117 | /> 118 | 119 | 120 | {row => props.addToFilter(await parseEventFilter(f))} 126 | addColumn={c => props.columnInsert(-1, parseEventColumn(c))} 127 | />} 128 | 129 |
130 |
); 131 | } 132 | -------------------------------------------------------------------------------- /venator-app/src/screens/spans-screen.css: -------------------------------------------------------------------------------- 1 | .spans-screen { 2 | height: 100%; 3 | 4 | box-sizing: border-box; 5 | padding: 8px; 6 | margin: 0px; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 4px; 10 | } 11 | 12 | .spans-screen-content { 13 | height: 0px; 14 | flex: 1; 15 | 16 | display: flex; 17 | flex-direction: row; 18 | } -------------------------------------------------------------------------------- /venator-app/src/screens/spans-screen.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, Show } from "solid-js"; 2 | 3 | import { SpanDetailPane } from "../components/detail-pane"; 4 | import { FilterInput } from "../components/filter-input"; 5 | import { ScreenHeader } from "../components/screen-header"; 6 | import { Input, parseSpanFilter, Span, Timestamp } from '../invoke'; 7 | import { Counts, PartialCountFilter, PartialFilter, PositionedSpan, Timespan } from "../models"; 8 | import { ColumnDef, INHERENT, parseSpanColumn, Table } from "../components/table"; 9 | import { SpanGraph } from "../components/span-graph"; 10 | 11 | import './spans-screen.css'; 12 | 13 | export type SpansScreenProps = { 14 | raw_filter: Input[], 15 | filter: Input[], 16 | setFilter: (filter: Input[]) => void, 17 | addToFilter: (filter: Input[]) => void, 18 | timespan: Timespan, 19 | setTimespan: (timespan: Timespan) => void, 20 | 21 | columns: ColumnDef[], 22 | columnWidths: string[], 23 | columnUpdate: (i: number, def: ColumnDef) => void, 24 | columnUpdateWidth: (i: number, width: string) => void, 25 | columnMove: (i: number, to: number) => void, 26 | columnInsert: (i: number, def: ColumnDef) => void, 27 | columnRemove: (i: number) => void, 28 | 29 | getSpans: (filter: PartialFilter, wait?: boolean) => Promise, 30 | getPositionedSpans: (filter: PartialFilter, wait?: boolean) => Promise, 31 | getSpanCounts: (filter: PartialCountFilter, wait?: boolean, cache?: boolean) => Promise, 32 | 33 | live: boolean, 34 | setLive: (live: boolean) => void, 35 | 36 | selected: Span | null, 37 | setSelected: (e: Span | null) => void, 38 | }; 39 | 40 | export function SpansScreen(props: SpansScreenProps) { 41 | const [hoveredRow, setHoveredRow] = createSignal(null); 42 | const [count, setCount] = createSignal<[number, boolean]>([0, false]); 43 | 44 | async function getTimestampBefore(timestamp: Timestamp) { 45 | // TODO: this gets the most recent "created_at" and not the most recent 46 | // "closed_at" that a user is probably expecting, however at the moment 47 | // that is more complicated to get and unclear what to return if the 48 | // timestamp is intersecting a span 49 | 50 | let spans = await props.getSpans({ 51 | order: 'desc', 52 | start: null, 53 | end: timestamp, 54 | limit: 1, 55 | }); 56 | 57 | if (spans == null || spans.length == 0) { 58 | return null; 59 | } 60 | 61 | return spans[0].created_at; 62 | } 63 | 64 | async function getTimestampAfter(timestamp: Timestamp) { 65 | // TODO: this will return a timestamp "before" the one provided if it 66 | // intersects a span 67 | 68 | let spans = await props.getSpans({ 69 | order: 'asc', 70 | start: timestamp, 71 | end: null, 72 | limit: 1, 73 | }); 74 | 75 | if (spans == null || spans.length == 0) { 76 | return null; 77 | } 78 | 79 | return spans[0].created_at; 80 | } 81 | 82 | return (
83 | 94 | 95 | 96 | 97 | 106 | 107 |
108 | 109 | timespan={props.timespan} 110 | columns={props.columns} 111 | columnWidths={props.columnWidths} 112 | columnUpdate={props.columnUpdate} 113 | columnUpdateWidth={props.columnUpdateWidth} 114 | columnMove={props.columnMove} 115 | columnInsert={props.columnInsert} 116 | columnRemove={props.columnRemove} 117 | columnDefault={INHERENT('name')} 118 | columnMin={3} 119 | selectedRow={props.selected} 120 | setSelectedRow={props.setSelected} 121 | hoveredRow={hoveredRow()} 122 | setHoveredRow={setHoveredRow} 123 | getEntries={props.getSpans} 124 | addToFilter={async f => props.addToFilter(await parseSpanFilter(f))} 125 | columnParser={parseSpanColumn} 126 | /> 127 | 128 | 129 | {row => props.addToFilter(await parseSpanFilter(f))} 135 | addColumn={c => props.columnInsert(-1, parseSpanColumn(c))} 136 | />} 137 | 138 |
139 |
); 140 | } 141 | -------------------------------------------------------------------------------- /venator-app/src/screens/trace-screen.css: -------------------------------------------------------------------------------- 1 | .trace-screen { 2 | height: 100%; 3 | 4 | box-sizing: border-box; 5 | padding: 8px; 6 | margin: 0px; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 4px; 10 | } 11 | 12 | .trace-screen-content { 13 | height: 0px; 14 | flex: 1; 15 | 16 | display: flex; 17 | flex-direction: row; 18 | } -------------------------------------------------------------------------------- /venator-app/src/screens/trace-screen.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect, createSignal, Show } from "solid-js"; 2 | 3 | import { EventDetailPane, SpanDetailPane } from "../components/detail-pane"; 4 | import { FilterInput } from "../components/filter-input"; 5 | import { ScreenHeader } from "../components/screen-header"; 6 | import { parseSpanFilter, Span, Event, Input, parseEventFilter, FilterPredicateSingle } from '../invoke'; 7 | import { PaginationFilter, Timespan } from "../models"; 8 | import { ColumnDef, COMBINED, CONTENT, INHERENT, parseTraceColumn, Table } from "../components/table"; 9 | import { TraceGraph } from "../components/trace-graph"; 10 | 11 | import './trace-screen.css'; 12 | import { CollapsableContext } from "../context/collapsable"; 13 | 14 | export type TraceScreenProps = { 15 | raw_filter: Input[], 16 | filter: Input[], 17 | setFilter: (filter: Input[]) => void, 18 | addToFilter: (filter: Input[]) => void, 19 | timespan: Timespan | null, 20 | setTimespan: (timespan: Timespan) => void, 21 | 22 | columns: ColumnDef[], 23 | columnWidths: string[], 24 | columnUpdate: (i: number, def: ColumnDef) => void, 25 | columnUpdateWidth: (i: number, width: string) => void, 26 | columnMove: (i: number, to: number) => void, 27 | columnInsert: (i: number, def: ColumnDef) => void, 28 | columnRemove: (i: number) => void, 29 | 30 | getEntries: (filter: PaginationFilter) => Promise<(Event | Span)[]>, 31 | 32 | collapsed: { [id: string]: true }, 33 | setCollapsed: (id: string, collapsed: boolean) => void, 34 | areAnyCollapsed: () => boolean, 35 | expandAll: () => void, 36 | collapseAll: () => void, 37 | 38 | selected: Event | Span | null, 39 | setSelected: (e: Event | Span | null) => void, 40 | }; 41 | 42 | export function TraceScreen(props: TraceScreenProps) { 43 | const [hoveredRow, setHoveredRow] = createSignal(null); 44 | const [count, setCount] = createSignal<[number, boolean]>([0, false]); 45 | 46 | async function getUncollapsedEntries(filter: PaginationFilter): Promise<(Event | Span)[]> { 47 | // this relies on the fact that props.getEntries mostly ignores the 48 | // filter and returns either all or none entries 49 | let entries = await props.getEntries(filter); 50 | 51 | return entries.filter(e => e.ancestors.every(a => props.collapsed[a.id] != true)); 52 | } 53 | 54 | createEffect(async () => { 55 | props.filter; 56 | let entries = props.getEntries({ order: 'asc' }); 57 | 58 | setCount([(await entries).length, true]); 59 | }); 60 | 61 | return (
62 | { }} 70 | getTimestampBefore={async () => null} 71 | getTimestampAfter={async () => null} 72 | /> 73 | 74 | 75 | 76 | 82 | 83 |
84 | 85 | props.collapsed[id] == true, 87 | collapse: (id, c) => props.setCollapsed(id, c), 88 | areAnyCollapsed: props.areAnyCollapsed, 89 | expandAll: props.expandAll, 90 | collapseAll: props.collapseAll, 91 | }}> 92 | props.addToFilter(await parseTraceFilter(f))} 109 | columnParser={parseTraceColumn} 110 | /> 111 | 112 | 113 | 114 | props.addToFilter(await parseTraceFilter(f))} 120 | addColumn={c => props.columnInsert(-1, parseTraceColumn(c))} 121 | /> 122 | 123 | 124 | props.addToFilter(await parseTraceFilter(f))} 130 | addColumn={c => props.columnInsert(-1, parseTraceColumn(c))} 131 | /> 132 | 133 | 134 | ); 135 | } 136 | 137 | export async function parseTraceFilter(f: string): Promise { 138 | let eventFilter = await parseEventFilter(f); 139 | let spanFilter = await parseSpanFilter(f); 140 | 141 | function mergeFilters(a: Input[], b: Input[]): Input[] { 142 | // - prioritize errors 143 | // - if they have different property types (attribute vs inherent) then 144 | // convert to error 145 | // - if they are completely different for some reason, 146 | // - recurse as needed 147 | 148 | let merged: Input[] = []; 149 | for (let e of a.map((a, i) => [a, b[i]])) { 150 | let [a, b] = e; 151 | 152 | if (a.input == 'invalid') { 153 | merged.push(a); 154 | continue; 155 | } 156 | if (b.input == 'invalid') { 157 | merged.push(b); 158 | continue; 159 | } 160 | 161 | if (a.predicate_kind != b.predicate_kind) { 162 | // this should never happen under normal circumstances 163 | merged.push({ 164 | input: 'invalid', 165 | error: 'Conflict', 166 | text: 'conflict', 167 | }); 168 | continue; 169 | } 170 | 171 | switch (a.predicate_kind) { 172 | case "single": 173 | let a_single = a.predicate; 174 | let b_single = b.predicate as FilterPredicateSingle; 175 | 176 | if (a_single.property_kind != b_single.property_kind) { 177 | merged.push({ 178 | input: 'invalid', 179 | error: 'Conflict', 180 | text: a_single.text.slice(1), 181 | }); 182 | } else { 183 | merged.push(a); 184 | } 185 | 186 | continue; 187 | case "or": 188 | merged.push({ 189 | ...a, 190 | predicate: mergeFilters(a.predicate, b.predicate as Input[]), 191 | }); 192 | continue; 193 | case "and": 194 | merged.push({ 195 | ...a, 196 | predicate: mergeFilters(a.predicate, b.predicate as Input[]), 197 | }); 198 | continue; 199 | } 200 | } 201 | 202 | return merged; 203 | } 204 | 205 | return mergeFilters(eventFilter, spanFilter); 206 | } 207 | -------------------------------------------------------------------------------- /venator-app/src/utils/undo.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDef } from "../components/table"; 2 | import { Input, Event, Span } from "../invoke"; 3 | import { Timespan } from "../models"; 4 | 5 | const UNDO_DEBOUNCE_PERIOD_MS = 2000; 6 | 7 | export type UndoDataKind = 'root' | 'timespan' | 'filter' | 'columns'; 8 | 9 | export type UndoData = { 10 | timespan: Timespan, 11 | raw_filter: Input[], 12 | columns: ColumnDef[], 13 | columnWidths: string[], 14 | }; 15 | 16 | export type UndoRecord = UndoData & { 17 | at: number, 18 | kind: UndoDataKind, 19 | }; 20 | 21 | export class UndoHistory { 22 | data: UndoRecord[]; 23 | current: number; 24 | 25 | constructor(data: UndoData) { 26 | this.data = [{ 27 | ...data, 28 | at: Date.now() - UNDO_DEBOUNCE_PERIOD_MS, 29 | kind: 'root', 30 | }]; 31 | this.current = 0; 32 | } 33 | 34 | 35 | undo(): UndoRecord | null { 36 | if (this.current == 0) { 37 | return null; 38 | } 39 | 40 | this.current -= 1; 41 | return this.data[this.current]; 42 | } 43 | 44 | redo(): UndoRecord | null { 45 | if (this.current == this.data.length - 1) { 46 | return null; 47 | } 48 | 49 | this.current += 1; 50 | return this.data[this.current]; 51 | } 52 | 53 | updateWithTimespan(timespan: Timespan) { 54 | let now = Date.now(); 55 | let current = this.data[this.current]; 56 | 57 | if (current.kind == 'timespan' && now - current.at < UNDO_DEBOUNCE_PERIOD_MS) { 58 | this.data.splice(this.current + 1); 59 | 60 | current.timespan = timespan; 61 | current.at = now; 62 | } else { 63 | this.data.splice(this.current + 1); 64 | this.data.push({ 65 | ...current, 66 | timespan, 67 | at: now, 68 | kind: 'timespan', 69 | }); 70 | this.current += 1; 71 | } 72 | } 73 | 74 | updateWithFilter(raw_filter: Input[]) { 75 | let now = Date.now(); 76 | let current = this.data[this.current]; 77 | 78 | // don't debounce the filter 79 | 80 | this.data.splice(this.current + 1); 81 | this.data.push({ 82 | ...current, 83 | raw_filter: [...raw_filter], 84 | at: now, 85 | kind: 'filter', 86 | }); 87 | this.current += 1; 88 | } 89 | 90 | updateWithColumnData(columns: ColumnDef[], columnWidths: string[]) { 91 | let now = Date.now(); 92 | let current = this.data[this.current]; 93 | 94 | if (current.kind == 'columns' && now - current.at < UNDO_DEBOUNCE_PERIOD_MS) { 95 | this.data.splice(this.current + 1); 96 | 97 | current.columns = [...columns]; 98 | current.columnWidths = [...columnWidths]; 99 | current.at = now; 100 | } else { 101 | this.data.splice(this.current + 1); 102 | this.data.push({ 103 | ...current, 104 | columns: [...columns], 105 | columnWidths: [...columnWidths], 106 | at: now, 107 | kind: 'columns', 108 | }); 109 | this.current += 1; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /venator-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /venator-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /venator-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /venator-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solid from "vite-plugin-solid"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(async () => ({ 6 | plugins: [solid()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // 10 | // 1. prevent vite from obscuring rust errors 11 | clearScreen: false, 12 | // 2. tauri expects a fixed port, fail if that port is not available 13 | server: { 14 | port: 1420, 15 | strictPort: true, 16 | watch: { 17 | // 3. tell vite to ignore watching `src-tauri` 18 | ignored: ["**/src-tauri/**"], 19 | }, 20 | }, 21 | build: { 22 | outDir: "src-tauri/dist", 23 | }, 24 | })); 25 | -------------------------------------------------------------------------------- /venator-engine/.gitignore: -------------------------------------------------------------------------------- 1 | benches/*.db -------------------------------------------------------------------------------- /venator-engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "venator-engine" 3 | version = "0.4.1" 4 | edition = "2021" 5 | description = "The core functionality that drives the Venator app" 6 | readme = "README.md" 7 | repository = "https://github.com/kmdreko/venator" 8 | license = "MIT" 9 | keywords = ["logging", "tracing", "opentelemetry", "profiling"] 10 | 11 | [[bench]] 12 | name = "my_benchmark" 13 | harness = false 14 | 15 | [features] 16 | default = ["persist"] 17 | persist = ["dep:rusqlite", "dep:serde_json"] 18 | 19 | [dependencies] 20 | anyhow = "1.0.95" 21 | bincode = { version = "1.3.3", default-features = false } 22 | futures = { version = "0.3.30", default-features = false, features = ["executor"] } 23 | lru = "0.12.5" 24 | nom = "7.1.3" 25 | regex = "1.10.6" 26 | rusqlite = { version = "0.31.0", features = ["bundled"], optional = true} 27 | serde = { version = "1.0.159", default-features = false, features = ["std", "derive"] } 28 | serde_json = { version = "1.0.120", optional = true } 29 | serde_repr = "0.1.19" 30 | tokio = { version = "1.38.0", features = ["rt", "sync", "macros"] } 31 | tracing = "0.1.41" 32 | wildcard = "0.2.0" 33 | 34 | [dev-dependencies] 35 | criterion = { version = "0.5.1", default-features = false} 36 | -------------------------------------------------------------------------------- /venator-engine/README.md: -------------------------------------------------------------------------------- 1 | The Venator "engine" provides the functionality that powers the Venator app. It 2 | is able to store, index, and query the spans, events, and related entities. Do 3 | note that this crate does not have support for listening for events and spans. 4 | 5 | Although the Venator application and tracing library are considered stable, this 6 | crate isn't. It is still built and somewhat documented so that someone could use 7 | it on its own. 8 | -------------------------------------------------------------------------------- /venator-engine/benches/my_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use std::hint::black_box; 3 | use std::path::Path; 4 | use venator_engine::engine::SyncEngine; 5 | use venator_engine::filter::{FilterPredicate, Order, Query}; 6 | use venator_engine::storage::{FileStorage, TransientStorage}; 7 | use venator_engine::Timestamp; 8 | 9 | fn event_counts_benchmark(c: &mut Criterion) { 10 | let file_storage = FileStorage::new(Path::new("./benches/test.vena.db")); 11 | let file_engine = SyncEngine::new(file_storage).unwrap(); 12 | 13 | let mut mem_storage = TransientStorage::new(); 14 | file_engine.copy_dataset(&mut mem_storage).unwrap(); 15 | let mem_engine = SyncEngine::new(mem_storage).unwrap(); 16 | 17 | drop(file_engine); 18 | 19 | let query = Query { 20 | filter: FilterPredicate::parse("").unwrap(), 21 | order: Order::Asc, 22 | limit: 0, 23 | start: Timestamp::MIN, 24 | end: Timestamp::MAX, 25 | previous: None, 26 | }; 27 | 28 | c.bench_function("count all events", |b| { 29 | b.iter(|| { 30 | assert_eq!( 31 | black_box(mem_engine.query_event_count(query.clone())), 32 | 16537 33 | ) 34 | }) 35 | }); 36 | 37 | let query = Query { 38 | filter: FilterPredicate::parse("#level: >=INFO").unwrap(), 39 | order: Order::Asc, 40 | limit: 0, 41 | start: Timestamp::MIN, 42 | end: Timestamp::MAX, 43 | previous: None, 44 | }; 45 | 46 | c.bench_function("count all events with level", |b| { 47 | b.iter(|| assert_eq!(black_box(mem_engine.query_event_count(query.clone())), 1511)) 48 | }); 49 | 50 | let query = Query { 51 | filter: FilterPredicate::parse("#level: >=INFO #content: /^logging in/").unwrap(), 52 | order: Order::Asc, 53 | limit: 0, 54 | start: Timestamp::MIN, 55 | end: Timestamp::MAX, 56 | previous: None, 57 | }; 58 | 59 | c.bench_function("count all events with level and regex", |b| { 60 | b.iter(|| assert_eq!(black_box(mem_engine.query_event_count(query.clone())), 874)) 61 | }); 62 | } 63 | 64 | fn span_counts_benchmark(c: &mut Criterion) { 65 | let file_storage = FileStorage::new(Path::new("./benches/test.vena.db")); 66 | let file_engine = SyncEngine::new(file_storage).unwrap(); 67 | 68 | let mut mem_storage = TransientStorage::new(); 69 | file_engine.copy_dataset(&mut mem_storage).unwrap(); 70 | let mem_engine = SyncEngine::new(mem_storage).unwrap(); 71 | 72 | drop(file_engine); 73 | 74 | let query = Query { 75 | filter: FilterPredicate::parse("").unwrap(), 76 | order: Order::Asc, 77 | limit: 0, 78 | start: Timestamp::MIN, 79 | end: Timestamp::MAX, 80 | previous: None, 81 | }; 82 | 83 | c.bench_function("count all spans", |b| { 84 | b.iter(|| assert_eq!(black_box(mem_engine.query_span_count(query.clone())), 98197)) 85 | }); 86 | 87 | let query = Query { 88 | filter: FilterPredicate::parse("#level: >=INFO").unwrap(), 89 | order: Order::Asc, 90 | limit: 0, 91 | start: Timestamp::MIN, 92 | end: Timestamp::MAX, 93 | previous: None, 94 | }; 95 | 96 | c.bench_function("count all spans with level", |b| { 97 | b.iter(|| assert_eq!(black_box(mem_engine.query_span_count(query.clone())), 98197)) 98 | }); 99 | 100 | let query = Query { 101 | filter: FilterPredicate::parse("#level: >=INFO @service.name: /^a/").unwrap(), 102 | order: Order::Asc, 103 | limit: 0, 104 | start: Timestamp::MIN, 105 | end: Timestamp::MAX, 106 | previous: None, 107 | }; 108 | 109 | c.bench_function("count all spans with level and regex", |b| { 110 | b.iter(|| assert_eq!(black_box(mem_engine.query_span_count(query.clone())), 97704)) 111 | }); 112 | } 113 | 114 | criterion_group!(benches, event_counts_benchmark, span_counts_benchmark); 115 | criterion_main!(benches); 116 | -------------------------------------------------------------------------------- /venator-engine/src/context/event_context.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | use std::collections::BTreeMap; 3 | use std::sync::Arc; 4 | 5 | use crate::storage::Storage; 6 | use crate::{ 7 | Ancestor, Attribute, AttributeSource, ComposedEvent, Event, EventKey, FullSpanId, Resource, 8 | Span, Timestamp, TraceRoot, Value, 9 | }; 10 | 11 | use super::RefOrDeferredArc; 12 | 13 | pub(crate) struct EventContext<'a, S> { 14 | event_key: EventKey, 15 | storage: &'a S, 16 | event: RefOrDeferredArc<'a, Event>, 17 | parents: OnceCell>>, 18 | resource: OnceCell>, 19 | } 20 | 21 | impl<'a, S> EventContext<'a, S> 22 | where 23 | S: Storage, 24 | { 25 | pub(crate) fn new(event_key: EventKey, storage: &'a S) -> EventContext<'a, S> { 26 | EventContext { 27 | event_key, 28 | storage, 29 | event: RefOrDeferredArc::Deferred(OnceCell::new()), 30 | parents: OnceCell::new(), 31 | resource: OnceCell::new(), 32 | } 33 | } 34 | 35 | pub(crate) fn with_event(event: &'a Event, storage: &'a S) -> EventContext<'a, S> { 36 | EventContext { 37 | event_key: event.key(), 38 | storage, 39 | event: RefOrDeferredArc::Ref(event), 40 | parents: OnceCell::new(), 41 | resource: OnceCell::new(), 42 | } 43 | } 44 | 45 | pub(crate) fn key(&self) -> EventKey { 46 | self.event_key 47 | } 48 | 49 | pub(crate) fn trace_root(&self) -> Option { 50 | let parent_id = self.event().parent_id; 51 | 52 | match parent_id { 53 | Some(FullSpanId::Tracing(_, _)) => { 54 | let root_parent_id = self.parents().last().map(|p| p.id); 55 | if let Some(FullSpanId::Tracing(instance_id, span_id)) = root_parent_id { 56 | Some(TraceRoot::Tracing(instance_id, span_id)) 57 | } else { 58 | panic!("tracing event's root span doesnt have tracing id or is missing"); 59 | } 60 | } 61 | Some(FullSpanId::Opentelemetry(trace_id, _)) => { 62 | Some(TraceRoot::Opentelemetry(trace_id)) 63 | } 64 | None => None, 65 | } 66 | } 67 | 68 | pub(crate) fn event(&self) -> &Event { 69 | match &self.event { 70 | RefOrDeferredArc::Ref(event) => event, 71 | RefOrDeferredArc::Deferred(deferred) => { 72 | deferred.get_or_init(|| self.storage.get_event(self.event_key).unwrap()) 73 | } 74 | } 75 | } 76 | 77 | pub(crate) fn parents(&self) -> impl Iterator { 78 | let event = self.event(); 79 | 80 | self.parents 81 | .get_or_init(|| { 82 | let mut parents = vec![]; 83 | let mut parent_key_next = event.parent_key; 84 | 85 | while let Some(parent_key) = parent_key_next { 86 | let Ok(parent) = self.storage.get_span(parent_key) else { 87 | tracing::error!("event/span has parent key but parent doesn't exist"); 88 | break; 89 | }; 90 | 91 | parent_key_next = parent.parent_key; 92 | parents.push(parent); 93 | } 94 | 95 | parents 96 | }) 97 | .iter() 98 | .map(|p| &**p) 99 | } 100 | 101 | pub(crate) fn resource(&self) -> &Resource { 102 | let event = self.event(); 103 | 104 | self.resource 105 | .get_or_init(|| self.storage.get_resource(event.resource_key).unwrap()) 106 | .as_ref() 107 | } 108 | 109 | pub(crate) fn attribute(&self, attr: &str) -> Option<&Value> { 110 | let event = self.event(); 111 | if let Some(v) = event.attributes.get(attr) { 112 | return Some(v); 113 | } 114 | 115 | let parents = self.parents(); 116 | for parent in parents { 117 | if let Some(v) = parent.attributes.get(attr) { 118 | return Some(v); 119 | } 120 | } 121 | 122 | let resource = self.resource(); 123 | if let Some(v) = resource.attributes.get(attr) { 124 | return Some(v); 125 | } 126 | 127 | None 128 | } 129 | 130 | pub(crate) fn attribute_with_key(&self, attr: &str) -> Option<(&Value, Timestamp)> { 131 | let event = self.event(); 132 | if let Some(v) = event.attributes.get(attr) { 133 | return Some((v, event.key())); 134 | } 135 | 136 | let parents = self.parents(); 137 | for parent in parents { 138 | if let Some(v) = parent.attributes.get(attr) { 139 | return Some((v, parent.key())); 140 | } 141 | } 142 | 143 | let resource = self.resource(); 144 | if let Some(v) = resource.attributes.get(attr) { 145 | return Some((v, resource.key())); 146 | } 147 | 148 | None 149 | } 150 | 151 | pub(crate) fn attributes(&self) -> impl Iterator { 152 | let mut attributes = BTreeMap::new(); 153 | 154 | let event = self.event(); 155 | for (attr, value) in &event.attributes { 156 | attributes.entry(&**attr).or_insert(value); 157 | } 158 | 159 | let parents = self.parents(); 160 | for parent in parents { 161 | for (attr, value) in &parent.attributes { 162 | attributes.entry(&**attr).or_insert(value); 163 | } 164 | } 165 | 166 | let resource = self.resource(); 167 | for (attr, value) in &resource.attributes { 168 | attributes.entry(&**attr).or_insert(value); 169 | } 170 | 171 | attributes.into_iter() 172 | } 173 | 174 | pub(crate) fn render(&self) -> ComposedEvent { 175 | let event = self.event(); 176 | 177 | let mut attributes = BTreeMap::::new(); 178 | for (attribute, value) in &self.event().attributes { 179 | attributes.insert( 180 | attribute.to_owned(), 181 | (AttributeSource::Inherent, value.clone()), 182 | ); 183 | } 184 | for parent in self.parents() { 185 | for (attribute, value) in &parent.attributes { 186 | if !attributes.contains_key(attribute) { 187 | attributes.insert( 188 | attribute.to_owned(), 189 | (AttributeSource::Span { span_id: parent.id }, value.clone()), 190 | ); 191 | } 192 | } 193 | } 194 | for (attribute, value) in &self.resource().attributes { 195 | if !attributes.contains_key(attribute) { 196 | attributes.insert( 197 | attribute.to_owned(), 198 | (AttributeSource::Resource, value.clone()), 199 | ); 200 | } 201 | } 202 | 203 | ComposedEvent { 204 | kind: event.kind, 205 | ancestors: { 206 | let mut ancestors = self 207 | .parents() 208 | .map(|parent| Ancestor { 209 | id: parent.id, 210 | name: parent.name.clone(), 211 | }) 212 | .collect::>(); 213 | 214 | ancestors.reverse(); 215 | ancestors 216 | }, 217 | timestamp: event.timestamp, 218 | level: event.level.into_simple_level(), 219 | content: event.content.clone(), 220 | namespace: event.namespace.clone(), 221 | function: event.function.clone(), 222 | file: match (&event.file_name, event.file_line) { 223 | (None, _) => None, 224 | (Some(name), None) => Some(name.clone()), 225 | (Some(name), Some(line)) => Some(format!("{name}:{line}")), 226 | }, 227 | attributes: attributes 228 | .into_iter() 229 | .map(|(name, (kind, value))| Attribute { 230 | name, 231 | value, 232 | source: kind, 233 | }) 234 | .collect(), 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /venator-engine/src/context/mod.rs: -------------------------------------------------------------------------------- 1 | //! A "context" provides an entity methods to get its related content (e.g. the 2 | //! `EventContext` can access the event's parent spans and root resource). It is 3 | //! necessary to deduce an event/span's full attribute set among other things. 4 | 5 | use std::cell::OnceCell; 6 | use std::sync::Arc; 7 | 8 | mod event_context; 9 | mod span_context; 10 | 11 | pub(crate) use event_context::EventContext; 12 | pub(crate) use span_context::SpanContext; 13 | 14 | enum RefOrDeferredArc<'a, T> { 15 | Ref(&'a T), 16 | Deferred(OnceCell>), 17 | } 18 | -------------------------------------------------------------------------------- /venator-engine/src/context/span_context.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | use std::collections::BTreeMap; 3 | use std::sync::Arc; 4 | 5 | use crate::storage::Storage; 6 | use crate::{ 7 | Ancestor, Attribute, AttributeSource, ComposedSpan, FullSpanId, Resource, Span, SpanKey, 8 | Timestamp, TraceRoot, Value, 9 | }; 10 | 11 | use super::RefOrDeferredArc; 12 | 13 | pub(crate) struct SpanContext<'a, S> { 14 | span_key: SpanKey, 15 | storage: &'a S, 16 | span: RefOrDeferredArc<'a, Span>, 17 | parents: OnceCell>>, 18 | resource: OnceCell>, 19 | } 20 | 21 | impl<'a, S> SpanContext<'a, S> 22 | where 23 | S: Storage, 24 | { 25 | pub(crate) fn new(span_key: SpanKey, storage: &'a S) -> SpanContext<'a, S> { 26 | SpanContext { 27 | span_key, 28 | storage, 29 | span: RefOrDeferredArc::Deferred(OnceCell::new()), 30 | parents: OnceCell::new(), 31 | resource: OnceCell::new(), 32 | } 33 | } 34 | 35 | pub(crate) fn with_span(span: &'a Span, storage: &'a S) -> SpanContext<'a, S> { 36 | SpanContext { 37 | span_key: span.key(), 38 | storage, 39 | span: RefOrDeferredArc::Ref(span), 40 | parents: OnceCell::new(), 41 | resource: OnceCell::new(), 42 | } 43 | } 44 | 45 | pub(crate) fn key(&self) -> SpanKey { 46 | self.span_key 47 | } 48 | 49 | pub(crate) fn trace_root(&self) -> TraceRoot { 50 | let id = self.span().id; 51 | 52 | match id { 53 | FullSpanId::Tracing(_, _) => { 54 | let root_parent_id = self.parents().last().map(|p| p.id).unwrap_or(id); 55 | if let FullSpanId::Tracing(instance_id, span_id) = root_parent_id { 56 | TraceRoot::Tracing(instance_id, span_id) 57 | } else { 58 | panic!("tracing span's root span doesnt have tracing id"); 59 | } 60 | } 61 | FullSpanId::Opentelemetry(trace_id, _) => TraceRoot::Opentelemetry(trace_id), 62 | } 63 | } 64 | 65 | pub(crate) fn span(&self) -> &Span { 66 | match &self.span { 67 | RefOrDeferredArc::Ref(span) => span, 68 | RefOrDeferredArc::Deferred(deferred) => { 69 | deferred.get_or_init(|| self.storage.get_span(self.span_key).unwrap()) 70 | } 71 | } 72 | } 73 | 74 | pub(crate) fn parents(&self) -> impl Iterator { 75 | let span = self.span(); 76 | 77 | self.parents 78 | .get_or_init(|| { 79 | let mut parents = vec![]; 80 | let mut parent_key_next = span.parent_key; 81 | 82 | while let Some(parent_key) = parent_key_next { 83 | let Ok(parent) = self.storage.get_span(parent_key) else { 84 | tracing::error!("event/span has parent key but parent doesn't exist"); 85 | break; 86 | }; 87 | 88 | parent_key_next = parent.parent_key; 89 | parents.push(parent); 90 | } 91 | 92 | parents 93 | }) 94 | .iter() 95 | .map(|p| &**p) 96 | } 97 | 98 | pub(crate) fn resource(&self) -> &Resource { 99 | let span = self.span(); 100 | 101 | self.resource 102 | .get_or_init(|| self.storage.get_resource(span.resource_key).unwrap()) 103 | .as_ref() 104 | } 105 | 106 | pub(crate) fn attribute(&self, attr: &str) -> Option<&Value> { 107 | let span = self.span(); 108 | if let Some(v) = span.attributes.get(attr) { 109 | return Some(v); 110 | } 111 | 112 | if let Some(v) = span.instrumentation_attributes.get(attr) { 113 | return Some(v); 114 | } 115 | 116 | let parents = self.parents(); 117 | for parent in parents { 118 | if let Some(v) = parent.attributes.get(attr) { 119 | return Some(v); 120 | } 121 | } 122 | 123 | let resource = self.resource(); 124 | if let Some(v) = resource.attributes.get(attr) { 125 | return Some(v); 126 | } 127 | 128 | None 129 | } 130 | 131 | pub(crate) fn attribute_with_key(&self, attr: &str) -> Option<(&Value, Timestamp)> { 132 | let span = self.span(); 133 | if let Some(v) = span.attributes.get(attr) { 134 | return Some((v, span.key())); 135 | } 136 | 137 | if let Some(v) = span.instrumentation_attributes.get(attr) { 138 | return Some((v, span.key())); 139 | } 140 | 141 | let parents = self.parents(); 142 | for parent in parents { 143 | if let Some(v) = parent.attributes.get(attr) { 144 | return Some((v, parent.key())); 145 | } 146 | } 147 | 148 | let resource = self.resource(); 149 | if let Some(v) = resource.attributes.get(attr) { 150 | return Some((v, resource.key())); 151 | } 152 | 153 | None 154 | } 155 | 156 | pub(crate) fn attributes(&self) -> impl Iterator { 157 | let mut attributes = BTreeMap::new(); 158 | 159 | let span = self.span(); 160 | for (attr, value) in &span.attributes { 161 | attributes.entry(&**attr).or_insert(value); 162 | } 163 | 164 | for (attr, value) in &span.instrumentation_attributes { 165 | attributes.entry(&**attr).or_insert(value); 166 | } 167 | 168 | let parents = self.parents(); 169 | for parent in parents { 170 | for (attr, value) in &parent.attributes { 171 | attributes.entry(&**attr).or_insert(value); 172 | } 173 | } 174 | 175 | let resource = self.resource(); 176 | for (attr, value) in &resource.attributes { 177 | attributes.entry(&**attr).or_insert(value); 178 | } 179 | 180 | attributes.into_iter() 181 | } 182 | 183 | pub(crate) fn render(&self) -> ComposedSpan { 184 | let span = self.span(); 185 | 186 | let mut attributes = BTreeMap::::new(); 187 | for (attribute, value) in &self.span().attributes { 188 | attributes.insert( 189 | attribute.to_owned(), 190 | (AttributeSource::Inherent, value.clone()), 191 | ); 192 | } 193 | for parent in self.parents() { 194 | for (attribute, value) in &parent.attributes { 195 | if !attributes.contains_key(attribute) { 196 | attributes.insert( 197 | attribute.to_owned(), 198 | (AttributeSource::Span { span_id: parent.id }, value.clone()), 199 | ); 200 | } 201 | } 202 | } 203 | for (attribute, value) in &self.resource().attributes { 204 | if !attributes.contains_key(attribute) { 205 | attributes.insert( 206 | attribute.to_owned(), 207 | (AttributeSource::Resource, value.clone()), 208 | ); 209 | } 210 | } 211 | 212 | ComposedSpan { 213 | kind: span.kind, 214 | id: span.id, 215 | ancestors: { 216 | let mut ancestors = self 217 | .parents() 218 | .map(|parent| Ancestor { 219 | id: parent.id, 220 | name: parent.name.clone(), 221 | }) 222 | .collect::>(); 223 | 224 | ancestors.reverse(); 225 | ancestors 226 | }, 227 | created_at: span.created_at, 228 | closed_at: span.closed_at, 229 | busy: span.busy, 230 | level: span.level.into_simple_level(), 231 | name: span.name.clone(), 232 | namespace: span.namespace.clone(), 233 | function: span.function.clone(), 234 | file: match (&span.file_name, span.file_line) { 235 | (None, _) => None, 236 | (Some(name), None) => Some(name.clone()), 237 | (Some(name), Some(line)) => Some(format!("{name}:{line}")), 238 | }, 239 | links: span.links.clone(), 240 | attributes: attributes 241 | .into_iter() 242 | .map(|(name, (kind, value))| Attribute { 243 | name, 244 | value, 245 | source: kind, 246 | }) 247 | .collect(), 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /venator-engine/src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | //! The actual engine that handles inserts and queries. 2 | //! 3 | //! There are two variants: sync and async. `SyncEngine` is the core and the 4 | //! `AsyncEngine` variant wraps an `SyncEngine` in a thread and coordinates via 5 | //! message passing. 6 | 7 | mod async_engine; 8 | mod sync_engine; 9 | 10 | pub use async_engine::AsyncEngine; 11 | pub use sync_engine::SyncEngine; 12 | -------------------------------------------------------------------------------- /venator-engine/src/filter/util.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | #[allow(dead_code)] 4 | pub(crate) trait BoundSearch { 5 | // This finds the first index of an item that is not less than the provided 6 | // item. This works via a binary-search algorithm. 7 | // 8 | // NOTE: The result is only meaningful if the input is sorted. 9 | fn lower_bound(&self, item: &T) -> usize; 10 | 11 | // This finds the first index of an item that is greater than the provided 12 | // item. This works via a binary-search algorithm. 13 | // 14 | // NOTE: The result is only meaningful if the input is sorted. 15 | fn upper_bound(&self, item: &T) -> usize; 16 | 17 | // This finds the first index of an item that is not less than the provided 18 | // item. This works via a binary-expansion-search algorithm, i.e. it checks 19 | // indexes geometrically starting from the beginning and then uses binary 20 | // -search within those bounds. This method is good if the item is expected 21 | // near the beginning. 22 | // 23 | // NOTE: The result is only meaningful if the input is sorted. 24 | fn lower_bound_via_expansion(&self, item: &T) -> usize; 25 | 26 | // This finds the first index of an item that is greater than the provided 27 | // item. This works via a binary-expansion-search algorithm, i.e. it checks 28 | // indexes geometrically starting from the end and then uses binary-search 29 | // within those bounds. This method is good if the item is expected near the 30 | // end. 31 | // 32 | // NOTE: The result is only meaningful if the input is sorted. 33 | fn upper_bound_via_expansion(&self, item: &T) -> usize; 34 | } 35 | 36 | impl BoundSearch for [T] { 37 | fn lower_bound(&self, item: &T) -> usize { 38 | self.binary_search_by(|current_item| match current_item.cmp(item) { 39 | Ordering::Greater => Ordering::Greater, 40 | Ordering::Equal => Ordering::Greater, 41 | Ordering::Less => Ordering::Less, 42 | }) 43 | .unwrap_or_else(|idx| idx) 44 | } 45 | 46 | fn upper_bound(&self, item: &T) -> usize { 47 | self.binary_search_by(|current_item| match current_item.cmp(item) { 48 | Ordering::Greater => Ordering::Greater, 49 | Ordering::Equal => Ordering::Less, 50 | Ordering::Less => Ordering::Less, 51 | }) 52 | .unwrap_or_else(|idx| idx) 53 | } 54 | 55 | fn lower_bound_via_expansion(&self, item: &T) -> usize { 56 | let len = self.len(); 57 | for (start, mut end) in std::iter::successors(Some((0, 1)), |&(_, j)| Some((j, j * 2))) { 58 | if end >= len { 59 | end = len 60 | } else if &self[end] < item { 61 | continue; 62 | } 63 | 64 | return self[start..end].lower_bound(item) + start; 65 | } 66 | 67 | unreachable!() 68 | } 69 | 70 | fn upper_bound_via_expansion(&self, item: &T) -> usize { 71 | let len = self.len(); 72 | for (start, mut end) in std::iter::successors(Some((0, 1)), |&(_, j)| Some((j, j * 2))) { 73 | if end >= len { 74 | end = len 75 | } else if &self[len - end] > item { 76 | continue; 77 | } 78 | 79 | return self[len - end..len - start].upper_bound(item) + (len - end); 80 | } 81 | 82 | unreachable!() 83 | } 84 | } 85 | 86 | pub(crate) fn merge(a: Option, b: Option, f: impl FnOnce(T, T) -> T) -> Option { 87 | // I wish this was in the standard library 88 | 89 | match (a, b) { 90 | (Some(a), Some(b)) => Some(f(a, b)), 91 | (Some(a), None) => Some(a), 92 | (None, Some(b)) => Some(b), 93 | (None, None) => None, 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | 101 | #[test] 102 | fn bounds_on_empty_slice() { 103 | assert_eq!([].lower_bound(&0), 0); 104 | assert_eq!([].upper_bound(&0), 0); 105 | assert_eq!([].lower_bound_via_expansion(&0), 0); 106 | assert_eq!([].upper_bound_via_expansion(&0), 0); 107 | } 108 | 109 | #[test] 110 | fn bounds_on_single_slice() { 111 | assert_eq!([1].lower_bound(&0), 0); 112 | assert_eq!([1].upper_bound(&0), 0); 113 | assert_eq!([1].lower_bound_via_expansion(&0), 0); 114 | assert_eq!([1].upper_bound_via_expansion(&0), 0); 115 | 116 | assert_eq!([1].lower_bound(&1), 0); 117 | assert_eq!([1].upper_bound(&1), 1); 118 | assert_eq!([1].lower_bound_via_expansion(&1), 0); 119 | assert_eq!([1].upper_bound_via_expansion(&1), 1); 120 | 121 | assert_eq!([1].lower_bound(&2), 1); 122 | assert_eq!([1].upper_bound(&2), 1); 123 | assert_eq!([1].lower_bound_via_expansion(&2), 1); 124 | assert_eq!([1].upper_bound_via_expansion(&2), 1); 125 | } 126 | 127 | #[test] 128 | fn bounds_for_duplicate_item() { 129 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound(&-1), 0); 130 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound(&-1), 0); 131 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound_via_expansion(&-1), 0); 132 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound_via_expansion(&-1), 0); 133 | 134 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound(&0), 0); 135 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound(&0), 2); 136 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound_via_expansion(&0), 0); 137 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound_via_expansion(&0), 2); 138 | 139 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound(&1), 2); 140 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound(&1), 4); 141 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound_via_expansion(&1), 2); 142 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound_via_expansion(&1), 4); 143 | 144 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound(&2), 4); 145 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound(&2), 6); 146 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound_via_expansion(&2), 4); 147 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound_via_expansion(&2), 6); 148 | 149 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound(&3), 6); 150 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound(&3), 6); 151 | assert_eq!([0, 0, 1, 1, 2, 2].lower_bound_via_expansion(&3), 6); 152 | assert_eq!([0, 0, 1, 1, 2, 2].upper_bound_via_expansion(&3), 6); 153 | } 154 | 155 | #[test] 156 | fn bounds_for_missing_item() { 157 | assert_eq!([0, 0, 2, 2].lower_bound(&1), 2); 158 | assert_eq!([0, 0, 2, 2].upper_bound(&1), 2); 159 | assert_eq!([0, 0, 2, 2].lower_bound_via_expansion(&1), 2); 160 | assert_eq!([0, 0, 2, 2].upper_bound_via_expansion(&1), 2); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /venator-engine/src/index/event_indexes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::context::{EventContext, SpanContext}; 6 | use crate::filter::BoundSearch; 7 | use crate::models::{EventKey, FullSpanId, Timestamp, TraceRoot, Value}; 8 | use crate::{ResourceKey, SpanKey, Storage}; 9 | 10 | use super::{IndexExt, ValueIndex}; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub(crate) struct EventIndexes { 14 | pub all: Vec, 15 | pub levels: [Vec; 6], 16 | pub resources: BTreeMap>, 17 | pub namespaces: BTreeMap>, 18 | pub filenames: BTreeMap>, 19 | pub roots: Vec, 20 | pub traces: HashMap>, 21 | pub contents: ValueIndex, 22 | pub attributes: BTreeMap, 23 | 24 | // events whose `parent_id` has not been seen yet 25 | pub orphanage: HashMap>, 26 | } 27 | 28 | impl EventIndexes { 29 | pub fn new() -> EventIndexes { 30 | EventIndexes { 31 | all: vec![], 32 | levels: [ 33 | Vec::new(), 34 | Vec::new(), 35 | Vec::new(), 36 | Vec::new(), 37 | Vec::new(), 38 | Vec::new(), 39 | ], 40 | resources: BTreeMap::new(), 41 | namespaces: BTreeMap::new(), 42 | filenames: BTreeMap::new(), 43 | roots: Vec::new(), 44 | traces: HashMap::new(), 45 | contents: ValueIndex::new(), 46 | attributes: BTreeMap::new(), 47 | 48 | orphanage: HashMap::new(), 49 | } 50 | } 51 | 52 | pub fn update_with_new_event(&mut self, context: &EventContext<'_, S>) { 53 | let event = context.event(); 54 | let event_key = event.timestamp; 55 | 56 | let idx = self.all.upper_bound_via_expansion(&event_key); 57 | self.all.insert(idx, event_key); 58 | 59 | let level_index = &mut self.levels[event.level.into_simple_level() as usize]; 60 | let idx = level_index.upper_bound_via_expansion(&event_key); 61 | level_index.insert(idx, event_key); 62 | 63 | // TODO: do I need a per-resource index? 64 | let resource_index = self.resources.entry(event.resource_key).or_default(); 65 | let idx = resource_index.upper_bound_via_expansion(&event_key); 66 | resource_index.insert(idx, event_key); 67 | 68 | if let Some(namespace) = event.namespace.clone() { 69 | let namespace_index = self.namespaces.entry(namespace).or_default(); 70 | let idx = namespace_index.upper_bound_via_expansion(&event_key); 71 | namespace_index.insert(idx, event_key); 72 | } 73 | 74 | if let Some(filename) = &event.file_name { 75 | let filename_index = self.filenames.entry(filename.clone()).or_default(); 76 | let idx = filename_index.upper_bound_via_expansion(&event_key); 77 | filename_index.insert(idx, event_key); 78 | } 79 | 80 | if let Some(trace) = context.trace_root() { 81 | let trace_index = self.traces.entry(trace).or_default(); 82 | let idx = trace_index.upper_bound_via_expansion(&event_key); 83 | trace_index.insert(idx, event_key); 84 | } 85 | 86 | if event.parent_id.is_none() { 87 | let idx = self.roots.upper_bound_via_expansion(&event_key); 88 | self.roots.insert(idx, event_key); 89 | } 90 | 91 | if let (Some(parent_id), None) = (event.parent_id, event.parent_key) { 92 | let orphan_index = self.orphanage.entry(parent_id).or_default(); 93 | let idx = orphan_index.upper_bound_via_expansion(&event_key); 94 | orphan_index.insert(idx, event_key); 95 | } 96 | 97 | self.contents.add_entry(event_key, &event.content); 98 | 99 | for (attribute, value) in context.attributes() { 100 | let index = self 101 | .attributes 102 | .entry(attribute.to_owned()) 103 | .or_insert_with(ValueIndex::new); 104 | 105 | index.add_entry(event_key, value); 106 | } 107 | } 108 | 109 | pub fn update_with_new_span( 110 | &mut self, 111 | context: &SpanContext<'_, S>, 112 | ) -> Vec { 113 | self.orphanage 114 | .remove(&context.span().id) 115 | .unwrap_or_default() 116 | } 117 | 118 | pub fn update_with_new_field_on_parent( 119 | &mut self, 120 | context: &EventContext<'_, S>, 121 | parent_key: Timestamp, 122 | parent_attributes: &BTreeMap, 123 | ) { 124 | for (attribute, new_value) in parent_attributes { 125 | let attribute_index = self 126 | .attributes 127 | .entry(attribute.to_owned()) 128 | .or_insert_with(ValueIndex::new); 129 | 130 | if let Some((old_value, key)) = context.attribute_with_key(attribute) { 131 | if key <= parent_key && new_value != old_value { 132 | attribute_index.remove_entry(context.key(), old_value); 133 | attribute_index.add_entry(context.key(), new_value); 134 | } 135 | } else { 136 | // there was no old value, just insert 137 | attribute_index.add_entry(context.key(), new_value); 138 | } 139 | } 140 | } 141 | 142 | pub fn remove_events(&mut self, events: &[EventKey]) { 143 | self.all.remove_list_sorted(events); 144 | 145 | for level_index in &mut self.levels { 146 | level_index.remove_list_sorted(events); 147 | } 148 | 149 | for resource_index in self.resources.values_mut() { 150 | resource_index.remove_list_sorted(events); 151 | } 152 | 153 | for target_index in self.namespaces.values_mut() { 154 | target_index.remove_list_sorted(events); 155 | } 156 | 157 | for filename_index in self.filenames.values_mut() { 158 | filename_index.remove_list_sorted(events); 159 | } 160 | self.roots.remove_list_sorted(events); 161 | 162 | for attribute_index in self.attributes.values_mut() { 163 | attribute_index.remove_entries(events); 164 | } 165 | } 166 | 167 | pub fn remove_spans(&mut self, _spans: &[SpanKey]) {} 168 | } 169 | -------------------------------------------------------------------------------- /venator-engine/src/index/mod.rs: -------------------------------------------------------------------------------- 1 | mod event_indexes; 2 | mod span_event_indexes; 3 | mod span_indexes; 4 | mod util; 5 | mod value; 6 | 7 | pub(crate) use event_indexes::EventIndexes; 8 | pub(crate) use span_event_indexes::SpanEventIndexes; 9 | pub(crate) use span_indexes::{SpanDurationIndex, SpanIndexes}; 10 | pub(crate) use value::ValueIndex; 11 | 12 | use util::IndexExt; 13 | -------------------------------------------------------------------------------- /venator-engine/src/index/span_event_indexes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::filter::BoundSearch; 6 | use crate::models::Timestamp; 7 | use crate::{SpanEvent, SpanEventKey, SpanKey}; 8 | 9 | use super::IndexExt; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub(crate) struct SpanEventIndexes { 13 | pub all: Vec, 14 | pub spans: HashMap>, 15 | } 16 | 17 | impl SpanEventIndexes { 18 | pub fn new() -> SpanEventIndexes { 19 | SpanEventIndexes { 20 | all: Vec::new(), 21 | spans: HashMap::new(), 22 | } 23 | } 24 | 25 | pub fn update_with_new_span_event(&mut self, span_event: &SpanEvent) { 26 | let timestamp = span_event.timestamp; 27 | 28 | let idx = self.all.upper_bound_via_expansion(×tamp); 29 | self.all.insert(idx, timestamp); 30 | 31 | let span_index = self.spans.entry(span_event.span_key).or_default(); 32 | let idx = span_index.upper_bound_via_expansion(×tamp); 33 | span_index.insert(idx, timestamp); 34 | } 35 | 36 | pub fn remove_span_events(&mut self, span_events: &[SpanEventKey]) { 37 | self.all.remove_list_sorted(span_events); 38 | 39 | for span_index in self.spans.values_mut() { 40 | span_index.remove_list_sorted(span_events); 41 | } 42 | } 43 | 44 | pub fn remove_spans(&mut self, spans: &[SpanKey]) { 45 | for span_key in spans { 46 | self.spans.remove(span_key); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /venator-engine/src/index/util.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use crate::filter::BoundSearch; 4 | 5 | pub trait IndexExt { 6 | /// This is intended to remove elements in an efficient way for sorted 7 | /// `self` and `list`. 8 | fn remove_list_sorted(&mut self, list: &[T]); 9 | } 10 | 11 | impl IndexExt for Vec { 12 | fn remove_list_sorted(&mut self, list: &[T]) { 13 | let mut i = 0; 14 | let mut j = 0; 15 | 16 | while let Some((ii, jj)) = find_next_match(&self[i..], &list[j..]) { 17 | // TODO: this can be done more efficiently with unsafe shenanigans - 18 | // as it is, this is O(n^2) when it could be O(n) 19 | self.remove(i + ii); 20 | 21 | i += ii; 22 | j += jj + 1; 23 | } 24 | } 25 | } 26 | 27 | // Returns the indexes from the respective lists of the first element that is 28 | // found in both. This assumes both lists are sorted. 29 | fn find_next_match(a: &[T], b: &[T]) -> Option<(usize, usize)> { 30 | if a.is_empty() || b.is_empty() { 31 | return None; 32 | } 33 | 34 | let mut i = 0; 35 | let mut j = 0; 36 | 37 | loop { 38 | match Ord::cmp(&a[i], &b[j]) { 39 | Ordering::Equal => return Some((i, j)), 40 | Ordering::Less => { 41 | i = a[i..].lower_bound_via_expansion(&b[j]); 42 | if i == a.len() { 43 | return None; 44 | } 45 | } 46 | Ordering::Greater => { 47 | j = b[j..].lower_bound_via_expansion(&a[i]); 48 | if j == b.len() { 49 | return None; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /venator-engine/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod context; 4 | pub mod engine; 5 | pub mod filter; 6 | mod index; 7 | mod models; 8 | pub mod storage; 9 | mod subscription; 10 | 11 | use storage::Storage; 12 | 13 | pub use models::{ 14 | Ancestor, Attribute, AttributeSource, ComposedEvent, ComposedSpan, CreateSpanEvent, 15 | DatasetStats, DeleteFilter, DeleteMetrics, EngineStatus, Event, EventKey, FullSpanId, 16 | InstanceId, Level, LevelConvertError, NewCloseSpanEvent, NewCreateSpanEvent, NewEnterSpanEvent, 17 | NewEvent, NewFollowsSpanEvent, NewResource, NewSpanEvent, NewSpanEventKind, NewUpdateSpanEvent, 18 | Resource, ResourceKey, SourceKind, Span, SpanEvent, SpanEventKey, SpanEventKind, SpanId, 19 | SpanKey, Timestamp, TraceId, TraceRoot, UpdateSpanEvent, Value, ValueOperator, 20 | }; 21 | pub use subscription::{SubscriptionId, SubscriptionResponse}; 22 | -------------------------------------------------------------------------------- /venator-engine/src/storage/cached.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::BTreeMap; 3 | use std::num::NonZeroUsize; 4 | use std::sync::Arc; 5 | 6 | use lru::LruCache; 7 | 8 | use crate::{Event, EventKey, FullSpanId, Resource, Span, SpanEvent, SpanKey, Timestamp, Value}; 9 | 10 | use super::{IndexStorage, Storage, StorageError}; 11 | 12 | /// This storage wraps another storage implementation to keep some in memory. 13 | pub struct CachedStorage { 14 | resources: RefCell>>, 15 | spans: RefCell>>, 16 | // span_events: RefCell>>, 17 | events: RefCell>>, 18 | inner: S, 19 | } 20 | 21 | impl CachedStorage { 22 | pub fn new(capacity: usize, storage: S) -> CachedStorage { 23 | let capacity = NonZeroUsize::new(capacity).unwrap(); 24 | 25 | CachedStorage { 26 | resources: RefCell::new(LruCache::new(capacity)), 27 | spans: RefCell::new(LruCache::new(capacity)), 28 | events: RefCell::new(LruCache::new(capacity)), 29 | inner: storage, 30 | } 31 | } 32 | } 33 | 34 | impl Storage for CachedStorage 35 | where 36 | S: Storage, 37 | { 38 | fn get_resource(&self, at: Timestamp) -> Result, StorageError> { 39 | if let Some(resource) = self.resources.borrow_mut().get(&at) { 40 | return Ok(resource.clone()); 41 | } 42 | 43 | let resource = self.inner.get_resource(at)?; 44 | self.resources.borrow_mut().put(at, resource.clone()); 45 | 46 | Ok(resource) 47 | } 48 | 49 | fn get_span(&self, at: Timestamp) -> Result, StorageError> { 50 | if let Some(span) = self.spans.borrow_mut().get(&at) { 51 | return Ok(span.clone()); 52 | } 53 | 54 | let span = self.inner.get_span(at)?; 55 | self.spans.borrow_mut().put(at, span.clone()); 56 | 57 | Ok(span) 58 | } 59 | 60 | fn get_span_event(&self, at: Timestamp) -> Result, StorageError> { 61 | self.inner.get_span_event(at) 62 | } 63 | 64 | fn get_event(&self, at: Timestamp) -> Result, StorageError> { 65 | if let Some(event) = self.events.borrow_mut().get(&at) { 66 | return Ok(event.clone()); 67 | } 68 | 69 | let event = self.inner.get_event(at)?; 70 | self.events.borrow_mut().put(at, event.clone()); 71 | 72 | Ok(event) 73 | } 74 | 75 | fn get_all_resources( 76 | &self, 77 | ) -> Result, StorageError>> + '_>, StorageError> 78 | { 79 | self.inner.get_all_resources() 80 | } 81 | 82 | fn get_all_spans( 83 | &self, 84 | ) -> Result, StorageError>> + '_>, StorageError> { 85 | self.inner.get_all_spans() 86 | } 87 | 88 | fn get_all_span_events( 89 | &self, 90 | ) -> Result, StorageError>> + '_>, StorageError> 91 | { 92 | self.inner.get_all_span_events() 93 | } 94 | 95 | fn get_all_events( 96 | &self, 97 | ) -> Result, StorageError>> + '_>, StorageError> { 98 | self.inner.get_all_events() 99 | } 100 | 101 | fn insert_resource(&mut self, resource: Resource) -> Result<(), StorageError> { 102 | self.inner.insert_resource(resource) 103 | } 104 | 105 | fn insert_span(&mut self, span: Span) -> Result<(), StorageError> { 106 | self.inner.insert_span(span) 107 | } 108 | 109 | fn insert_span_event(&mut self, span_event: SpanEvent) -> Result<(), StorageError> { 110 | self.inner.insert_span_event(span_event) 111 | } 112 | 113 | fn insert_event(&mut self, event: Event) -> Result<(), StorageError> { 114 | self.inner.insert_event(event) 115 | } 116 | 117 | fn update_span_closed( 118 | &mut self, 119 | at: Timestamp, 120 | closed: Timestamp, 121 | busy: Option, 122 | ) -> Result<(), StorageError> { 123 | self.spans.borrow_mut().pop(&at); 124 | self.inner.update_span_closed(at, closed, busy) 125 | } 126 | 127 | fn update_span_attributes( 128 | &mut self, 129 | at: Timestamp, 130 | attributes: BTreeMap, 131 | ) -> Result<(), StorageError> { 132 | self.spans.borrow_mut().pop(&at); 133 | self.inner.update_span_attributes(at, attributes) 134 | } 135 | 136 | fn update_span_link( 137 | &mut self, 138 | at: Timestamp, 139 | link: FullSpanId, 140 | attributes: BTreeMap, 141 | ) -> Result<(), StorageError> { 142 | self.spans.borrow_mut().pop(&at); 143 | self.inner.update_span_link(at, link, attributes) 144 | } 145 | 146 | fn update_span_parents( 147 | &mut self, 148 | parent_key: SpanKey, 149 | spans: &[SpanKey], 150 | ) -> Result<(), StorageError> { 151 | let mut cached_spans = self.spans.borrow_mut(); 152 | for span in spans { 153 | cached_spans.pop(span); 154 | } 155 | drop(cached_spans); 156 | self.inner.update_span_parents(parent_key, spans) 157 | } 158 | 159 | fn update_event_parents( 160 | &mut self, 161 | parent_key: SpanKey, 162 | events: &[EventKey], 163 | ) -> Result<(), StorageError> { 164 | let mut cached_events = self.events.borrow_mut(); 165 | for event in events { 166 | cached_events.pop(event); 167 | } 168 | drop(cached_events); 169 | self.inner.update_event_parents(parent_key, events) 170 | } 171 | 172 | fn drop_resources(&mut self, resources: &[Timestamp]) -> Result<(), StorageError> { 173 | for c in resources { 174 | self.resources.borrow_mut().pop(c); 175 | } 176 | 177 | self.inner.drop_resources(resources) 178 | } 179 | 180 | fn drop_spans(&mut self, spans: &[Timestamp]) -> Result<(), StorageError> { 181 | for s in spans { 182 | self.spans.borrow_mut().pop(s); 183 | } 184 | 185 | self.inner.drop_spans(spans) 186 | } 187 | 188 | fn drop_span_events(&mut self, span_events: &[Timestamp]) -> Result<(), StorageError> { 189 | self.inner.drop_span_events(span_events) 190 | } 191 | 192 | fn drop_events(&mut self, events: &[Timestamp]) -> Result<(), StorageError> { 193 | for s in events { 194 | self.events.borrow_mut().pop(s); 195 | } 196 | 197 | self.inner.drop_events(events) 198 | } 199 | 200 | #[allow(private_interfaces)] 201 | fn as_index_storage(&self) -> Option<&dyn IndexStorage> { 202 | self.inner.as_index_storage() 203 | } 204 | 205 | #[allow(private_interfaces)] 206 | fn as_index_storage_mut(&mut self) -> Option<&mut dyn IndexStorage> { 207 | self.inner.as_index_storage_mut() 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /venator-engine/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Traits and implementations for storing data in the engine. 2 | //! 3 | //! There are two primary storage implementations: `TransientStorage` that holds 4 | //! all data in memory and `FileStorage` which persists the data in an SQLite 5 | //! database file. Though when using file storage, it is best to wrap it in a 6 | //! `CachedStorage` layer since the engine does not cache data itself and new 7 | //! lookups are often temporally related. 8 | //! 9 | //! Custom implemenations can be created and used with the engine with the only 10 | //! caveat that index persistence is only supported by `FileStorage` - any other 11 | //! implementation must rebuild indexes based on `get_all_*` calls on startup. 12 | 13 | use std::collections::BTreeMap; 14 | use std::error::Error as StdError; 15 | use std::fmt::{Display, Error as FmtError, Formatter}; 16 | use std::sync::Arc; 17 | 18 | mod cached; 19 | #[cfg(feature = "persist")] 20 | mod file; 21 | mod transient; 22 | 23 | use crate::index::{EventIndexes, SpanEventIndexes, SpanIndexes}; 24 | use crate::models::{Event, EventKey, Resource, Span, SpanEvent, SpanKey, Timestamp, Value}; 25 | use crate::FullSpanId; 26 | 27 | pub use cached::CachedStorage; 28 | #[cfg(feature = "persist")] 29 | pub use file::FileStorage; 30 | pub use transient::TransientStorage; 31 | 32 | #[derive(Debug, Clone)] 33 | pub enum StorageError { 34 | NotFound, 35 | Internal(String), 36 | } 37 | 38 | impl Display for StorageError { 39 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { 40 | match self { 41 | StorageError::NotFound => write!(f, "not found"), 42 | StorageError::Internal(s) => write!(f, "internal: {s}"), 43 | } 44 | } 45 | } 46 | 47 | impl StdError for StorageError {} 48 | 49 | pub type StorageIter<'a, T> = Box, StorageError>> + 'a>; 50 | 51 | /// This serves as the backing storage of resources, spans, events, and span 52 | /// events. 53 | /// 54 | /// An implementation must provide fast lookups for each respective entity based 55 | /// on its "timestamp" (`timestamp` for events and span events, `created_at` for 56 | /// resources and spans). 57 | pub trait Storage { 58 | fn get_resource(&self, at: Timestamp) -> Result, StorageError>; 59 | fn get_span(&self, at: Timestamp) -> Result, StorageError>; 60 | fn get_span_event(&self, at: Timestamp) -> Result, StorageError>; 61 | fn get_event(&self, at: Timestamp) -> Result, StorageError>; 62 | 63 | fn get_all_resources(&self) -> Result, StorageError>; 64 | fn get_all_spans(&self) -> Result, StorageError>; 65 | fn get_all_span_events(&self) -> Result, StorageError>; 66 | fn get_all_events(&self) -> Result, StorageError>; 67 | 68 | fn insert_resource(&mut self, resource: Resource) -> Result<(), StorageError>; 69 | fn insert_span(&mut self, span: Span) -> Result<(), StorageError>; 70 | fn insert_span_event(&mut self, span_event: SpanEvent) -> Result<(), StorageError>; 71 | fn insert_event(&mut self, event: Event) -> Result<(), StorageError>; 72 | 73 | fn update_span_closed( 74 | &mut self, 75 | at: Timestamp, 76 | closed: Timestamp, 77 | busy: Option, 78 | ) -> Result<(), StorageError>; 79 | fn update_span_attributes( 80 | &mut self, 81 | at: Timestamp, 82 | attributes: BTreeMap, 83 | ) -> Result<(), StorageError>; 84 | fn update_span_link( 85 | &mut self, 86 | at: Timestamp, 87 | link: FullSpanId, 88 | attributes: BTreeMap, 89 | ) -> Result<(), StorageError>; 90 | fn update_span_parents( 91 | &mut self, 92 | parent_key: SpanKey, 93 | spans: &[SpanKey], 94 | ) -> Result<(), StorageError>; 95 | fn update_event_parents( 96 | &mut self, 97 | parent_key: SpanKey, 98 | events: &[EventKey], 99 | ) -> Result<(), StorageError>; 100 | 101 | fn drop_resources(&mut self, resources: &[Timestamp]) -> Result<(), StorageError>; 102 | fn drop_spans(&mut self, spans: &[Timestamp]) -> Result<(), StorageError>; 103 | fn drop_span_events(&mut self, span_events: &[Timestamp]) -> Result<(), StorageError>; 104 | fn drop_events(&mut self, events: &[Timestamp]) -> Result<(), StorageError>; 105 | 106 | #[doc(hidden)] 107 | #[allow(private_interfaces)] 108 | fn as_index_storage(&self) -> Option<&dyn IndexStorage> { 109 | None 110 | } 111 | 112 | #[doc(hidden)] 113 | #[allow(private_interfaces)] 114 | fn as_index_storage_mut(&mut self) -> Option<&mut dyn IndexStorage> { 115 | None 116 | } 117 | } 118 | 119 | pub(crate) trait IndexStorage { 120 | fn get_indexes( 121 | &self, 122 | ) -> Result, StorageError>; 123 | fn update_indexes( 124 | &mut self, 125 | span_indexes: &SpanIndexes, 126 | span_event_indexes: &SpanEventIndexes, 127 | event_indexes: &EventIndexes, 128 | ) -> Result<(), StorageError>; 129 | } 130 | -------------------------------------------------------------------------------- /venator-engine/src/storage/transient.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::sync::Arc; 3 | 4 | use super::{Storage, StorageError}; 5 | use crate::models::{EventKey, Value}; 6 | use crate::{Event, FullSpanId, Resource, Span, SpanEvent, SpanKey, Timestamp}; 7 | 8 | /// This storage just holds all entities in memory. 9 | pub struct TransientStorage { 10 | resources: BTreeMap>, 11 | spans: BTreeMap>, 12 | span_events: BTreeMap>, 13 | events: BTreeMap>, 14 | } 15 | 16 | impl TransientStorage { 17 | pub fn new() -> TransientStorage { 18 | TransientStorage { 19 | resources: BTreeMap::new(), 20 | spans: BTreeMap::new(), 21 | span_events: BTreeMap::new(), 22 | events: BTreeMap::new(), 23 | } 24 | } 25 | } 26 | 27 | impl Default for TransientStorage { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | impl Storage for TransientStorage { 34 | fn get_resource(&self, at: Timestamp) -> Result, StorageError> { 35 | self.resources 36 | .get(&at) 37 | .cloned() 38 | .ok_or(StorageError::NotFound) 39 | } 40 | 41 | fn get_span(&self, at: Timestamp) -> Result, StorageError> { 42 | self.spans.get(&at).cloned().ok_or(StorageError::NotFound) 43 | } 44 | 45 | fn get_span_event(&self, at: Timestamp) -> Result, StorageError> { 46 | self.span_events 47 | .get(&at) 48 | .cloned() 49 | .ok_or(StorageError::NotFound) 50 | } 51 | 52 | fn get_event(&self, at: Timestamp) -> Result, StorageError> { 53 | self.events.get(&at).cloned().ok_or(StorageError::NotFound) 54 | } 55 | 56 | fn get_all_resources( 57 | &self, 58 | ) -> Result, StorageError>> + '_>, StorageError> 59 | { 60 | Ok(Box::new(self.resources.values().cloned().map(Ok))) 61 | } 62 | 63 | fn get_all_spans( 64 | &self, 65 | ) -> Result, StorageError>> + '_>, StorageError> { 66 | Ok(Box::new(self.spans.values().cloned().map(Ok))) 67 | } 68 | 69 | fn get_all_span_events( 70 | &self, 71 | ) -> Result, StorageError>> + '_>, StorageError> 72 | { 73 | Ok(Box::new(self.span_events.values().cloned().map(Ok))) 74 | } 75 | 76 | fn get_all_events( 77 | &self, 78 | ) -> Result, StorageError>> + '_>, StorageError> { 79 | Ok(Box::new(self.events.values().cloned().map(Ok))) 80 | } 81 | 82 | fn insert_resource(&mut self, resource: Resource) -> Result<(), StorageError> { 83 | let at = resource.key(); 84 | self.resources.insert(at, Arc::new(resource)); 85 | Ok(()) 86 | } 87 | 88 | fn insert_span(&mut self, span: Span) -> Result<(), StorageError> { 89 | let at = span.created_at; 90 | self.spans.insert(at, Arc::new(span)); 91 | Ok(()) 92 | } 93 | 94 | fn insert_span_event(&mut self, span_event: SpanEvent) -> Result<(), StorageError> { 95 | let at = span_event.timestamp; 96 | self.span_events.insert(at, Arc::new(span_event)); 97 | Ok(()) 98 | } 99 | 100 | fn insert_event(&mut self, event: Event) -> Result<(), StorageError> { 101 | let at = event.timestamp; 102 | self.events.insert(at, Arc::new(event)); 103 | Ok(()) 104 | } 105 | 106 | fn update_span_closed( 107 | &mut self, 108 | at: Timestamp, 109 | closed_at: Timestamp, 110 | busy: Option, 111 | ) -> Result<(), StorageError> { 112 | if let Some(span) = self.spans.get(&at) { 113 | let mut span = (**span).clone(); 114 | span.closed_at = Some(closed_at); 115 | span.busy = busy; 116 | self.spans.insert(at, Arc::new(span)); 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | fn update_span_attributes( 123 | &mut self, 124 | at: Timestamp, 125 | attributes: BTreeMap, 126 | ) -> Result<(), StorageError> { 127 | if let Some(span) = self.spans.get(&at) { 128 | let mut span = (**span).clone(); 129 | span.attributes.extend(attributes); 130 | self.spans.insert(at, Arc::new(span)); 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | fn update_span_link( 137 | &mut self, 138 | at: Timestamp, 139 | link: FullSpanId, 140 | attributes: BTreeMap, 141 | ) -> Result<(), StorageError> { 142 | if let Some(span) = self.spans.get(&at) { 143 | let mut span = (**span).clone(); 144 | span.links.push((link, attributes)); 145 | self.spans.insert(at, Arc::new(span)); 146 | } 147 | 148 | Ok(()) 149 | } 150 | 151 | fn update_span_parents( 152 | &mut self, 153 | parent_key: SpanKey, 154 | spans: &[SpanKey], 155 | ) -> Result<(), StorageError> { 156 | for span in spans { 157 | if let Some(span) = self.spans.get_mut(span) { 158 | let mut span = (**span).clone(); 159 | span.parent_key = Some(parent_key); 160 | self.spans.insert(span.key(), Arc::new(span)); 161 | } 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | fn update_event_parents( 168 | &mut self, 169 | parent_key: SpanKey, 170 | events: &[EventKey], 171 | ) -> Result<(), StorageError> { 172 | for event in events { 173 | if let Some(event) = self.events.get_mut(event) { 174 | let mut event = (**event).clone(); 175 | event.parent_key = Some(parent_key); 176 | self.events.insert(event.key(), Arc::new(event)); 177 | } 178 | } 179 | 180 | Ok(()) 181 | } 182 | 183 | fn drop_resources(&mut self, resources: &[Timestamp]) -> Result<(), StorageError> { 184 | for at in resources { 185 | self.resources.remove(at); 186 | } 187 | 188 | Ok(()) 189 | } 190 | 191 | fn drop_spans(&mut self, spans: &[Timestamp]) -> Result<(), StorageError> { 192 | for at in spans { 193 | self.spans.remove(at); 194 | } 195 | 196 | Ok(()) 197 | } 198 | 199 | fn drop_span_events(&mut self, span_events: &[Timestamp]) -> Result<(), StorageError> { 200 | for at in span_events { 201 | self.span_events.remove(at); 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | fn drop_events(&mut self, events: &[Timestamp]) -> Result<(), StorageError> { 208 | for at in events { 209 | self.events.remove(at); 210 | } 211 | 212 | Ok(()) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /venator-engine/src/subscription.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 2 | 3 | use crate::context::{EventContext, SpanContext}; 4 | use crate::filter::{BasicEventFilter, BasicSpanFilter, BoundSearch}; 5 | use crate::models::EventKey; 6 | use crate::{ComposedEvent, SpanKey, ComposedSpan, Storage, Timestamp}; 7 | 8 | pub type SubscriptionId = usize; 9 | pub type Subscriber = (SubscriptionId, UnboundedReceiver>); 10 | 11 | pub enum SubscriptionResponse { 12 | Add(T), 13 | Remove(Timestamp), 14 | } 15 | 16 | pub(crate) struct EventSubscription { 17 | filter: BasicEventFilter, 18 | sender: UnboundedSender>, 19 | cache: Vec, 20 | } 21 | 22 | impl EventSubscription { 23 | pub(crate) fn new( 24 | filter: BasicEventFilter, 25 | sender: UnboundedSender>, 26 | ) -> EventSubscription { 27 | EventSubscription { 28 | filter, 29 | sender, 30 | cache: Vec::new(), 31 | } 32 | } 33 | 34 | pub(crate) fn connected(&self) -> bool { 35 | !self.sender.is_closed() 36 | } 37 | 38 | /// This should be called when an event is created or was impacted by a 39 | /// change in a parent span. 40 | pub(crate) fn on_event(&mut self, event: &EventContext<'_, S>) { 41 | if self.filter.matches(event) { 42 | let idx = self.cache.upper_bound_via_expansion(&event.key()); 43 | if idx == 0 || self.cache[idx - 1] != event.key() { 44 | // the event was not visible by this subscription before 45 | self.cache.insert(idx, event.key()); 46 | } 47 | 48 | // we emit an event regardless since we want the subscriber to have 49 | // fresh data 50 | let _ = self.sender.send(SubscriptionResponse::Add(event.render())); 51 | } else { 52 | let idx = self.cache.upper_bound_via_expansion(&event.key()); 53 | if idx != 0 && self.cache[idx - 1] == event.key() { 54 | // the event was visible by this subscription before 55 | self.cache.remove(idx - 1); 56 | let _ = self.sender.send(SubscriptionResponse::Remove(event.key())); 57 | } 58 | 59 | // NOTE: There is wiggle room for error here if the subscriber pre- 60 | // loads an event before subscribing but after subscribing a parent 61 | // span update means the event shouldn't be shown. This code would 62 | // not emit a "remove" event. 63 | // 64 | // However, the likleyhood of that happening is low since only some 65 | // filters are even susceptible to the possibility (have negation) 66 | // and the window for opportunity is often short-lived. 67 | } 68 | } 69 | } 70 | 71 | pub(crate) struct SpanSubscription { 72 | filter: BasicSpanFilter, 73 | sender: UnboundedSender>, 74 | cache: Vec, 75 | } 76 | 77 | impl SpanSubscription { 78 | pub(crate) fn new( 79 | filter: BasicSpanFilter, 80 | sender: UnboundedSender>, 81 | ) -> SpanSubscription { 82 | SpanSubscription { 83 | filter, 84 | sender, 85 | cache: Vec::new(), 86 | } 87 | } 88 | 89 | pub(crate) fn connected(&self) -> bool { 90 | !self.sender.is_closed() 91 | } 92 | 93 | /// This should be called when a span is created or was impacted by a change 94 | /// in a parent span. 95 | pub(crate) fn on_span(&mut self, span: &SpanContext<'_, S>) { 96 | if self.filter.matches(span) { 97 | let idx = self.cache.upper_bound_via_expansion(&span.key()); 98 | if idx == 0 || self.cache[idx - 1] != span.key() { 99 | // the span was not visible by this subscription before 100 | self.cache.insert(idx, span.key()); 101 | } 102 | 103 | // we emit an span regardless since we want the subscriber to have 104 | // fresh data 105 | let _ = self.sender.send(SubscriptionResponse::Add(span.render())); 106 | } else { 107 | let idx = self.cache.upper_bound_via_expansion(&span.key()); 108 | if idx != 0 && self.cache[idx - 1] == span.key() { 109 | // the span was visible by this subscription before 110 | self.cache.remove(idx - 1); 111 | let _ = self.sender.send(SubscriptionResponse::Remove(span.key())); 112 | } 113 | 114 | // NOTE: There is wiggle room for error here if the subscriber pre- 115 | // loads an span before subscribing but after subscribing a parent 116 | // span update means the span shouldn't be shown. This code would 117 | // not emit a "remove" event. 118 | // 119 | // However, the likleyhood of that happening is low since only some 120 | // filters are even susceptible to the possibility (have negation) 121 | // and the window for opportunity is often short-lived. 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /venator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "venator" 3 | version = "1.1.0" 4 | edition = "2021" 5 | description = "A tracing layer for exporting logs and spans to the Venator app" 6 | readme = "README.md" 7 | repository = "https://github.com/kmdreko/venator" 8 | license = "MIT" 9 | keywords = ["logging", "tracing", "profiling"] 10 | 11 | [features] 12 | default = ["record-128s"] 13 | record-128s = [] # requires tracing v0.1.36+ 14 | 15 | [dependencies] 16 | bincode = { version = "1.3.3", default-features = false } 17 | serde = { version = "1.0.159", default-features = false, features = ["std", "derive"] } 18 | thread-id = "5.0.0" 19 | tracing = { version = "0.1.0", default-features = false } 20 | tracing-core = { version = "0.1.20", default-features = false } 21 | tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "registry"] } 22 | 23 | [dev-dependencies] 24 | tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "registry", "fmt", "env-filter"] } 25 | -------------------------------------------------------------------------------- /venator/README.md: -------------------------------------------------------------------------------- 1 | The Venator Rust library provides a tracing layer that will export logs and 2 | spans to the [Venator app](https://crates.io/crates/venator-app). 3 | 4 | ## Usage 5 | 6 | ```toml 7 | [dependencies] 8 | venator = "1.1.0" 9 | ``` 10 | 11 | Install it as the global subscriber: 12 | 13 | ```rust 14 | use venator::Venator; 15 | 16 | // minimal 17 | Venator::default().install(); 18 | ``` 19 | 20 | ```rust 21 | use venator::Venator; 22 | 23 | // configured 24 | Venator::builder() 25 | .with_host("localhost:8362") 26 | .with_attribute("service", "my_app") 27 | .with_attribute("environment.name", "internal") 28 | .with_attribute("environment.dev", true) 29 | .build() 30 | .install(); 31 | ``` 32 | 33 | Or use it as a [`Layer`](https://docs.rs/tracing-subscriber/0.3.19/tracing_subscriber/layer/trait.Layer.html): 34 | 35 | ```rust 36 | use tracing_subscriber::layer::SubscriberExt; 37 | use tracing_subscriber::util::SubscriberInitExt; 38 | use venator::Venator; 39 | 40 | tracing_subscriber::registry() 41 | .with(Venator::default()) 42 | .with(tracing_subscriber::fmt::Layer::default()) 43 | .with(tracing_subscriber::EnvFilter::from_default_env()) 44 | .init() 45 | ``` 46 | -------------------------------------------------------------------------------- /venator/src/ids.rs: -------------------------------------------------------------------------------- 1 | //! ID generation 2 | //! 3 | //! This module is for generating unique IDs. It keeps thread-local blocks of 4 | //! IDs to hand out and only coordinate with global state if that block is 5 | //! exhausted. 6 | //! 7 | //! This is needed since the tracing-subscriber `Registry` says that it will 8 | //! re-use IDs from closed spans. 9 | 10 | use std::cell::Cell; 11 | use std::num::NonZeroU64; 12 | use std::ops::Range; 13 | use std::sync::atomic::{AtomicU64, Ordering}; 14 | 15 | static ID_COUNTER: AtomicU64 = AtomicU64::new(1); 16 | 17 | const LOCAL_ID_COUNTER_BLOCK_SIZE: u64 = 1024 * 1024; 18 | 19 | fn get_local_block() -> Range { 20 | let block = ID_COUNTER.fetch_add(1, Ordering::Relaxed); 21 | let start = block * LOCAL_ID_COUNTER_BLOCK_SIZE; 22 | let end = start + LOCAL_ID_COUNTER_BLOCK_SIZE; 23 | 24 | Range { start, end } 25 | } 26 | 27 | thread_local! { 28 | static LOCAL_ID_COUNTER: Cell> = Cell::new(get_local_block()); 29 | } 30 | 31 | #[derive(Copy, Clone)] 32 | pub(crate) struct VenatorId(pub(crate) NonZeroU64); 33 | 34 | pub(crate) fn generate() -> VenatorId { 35 | let mut local_counter = LOCAL_ID_COUNTER.take(); 36 | 37 | let id = match local_counter.next() { 38 | Some(id) => id, 39 | None => { 40 | local_counter = get_local_block(); 41 | local_counter.next().unwrap() 42 | } 43 | }; 44 | 45 | LOCAL_ID_COUNTER.set(local_counter); 46 | 47 | VenatorId(NonZeroU64::new(id).unwrap()) 48 | } 49 | -------------------------------------------------------------------------------- /venator/src/messaging.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::io::Error as IoError; 3 | use std::num::NonZeroU64; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | 6 | use bincode::{DefaultOptions, Error as BincodeError, Options}; 7 | use serde::Serialize; 8 | use tracing::span::{Attributes, Record}; 9 | use tracing::{Event, Level, Subscriber}; 10 | use tracing_subscriber::layer::Context; 11 | use tracing_subscriber::registry::LookupSpan; 12 | 13 | use crate::attributes::OwnedValue; 14 | use crate::ids::VenatorId; 15 | 16 | fn now() -> NonZeroU64 { 17 | let microseconds = SystemTime::now() 18 | .duration_since(UNIX_EPOCH) 19 | .expect("now should not be before the UNIX epoch") 20 | .as_micros(); 21 | 22 | let microseconds = u64::try_from(microseconds) 23 | .expect("microseconds shouldn't exceed a u64 until the year 586,912 AD"); 24 | 25 | NonZeroU64::new(microseconds).expect("now should not be at the UNIX epoch") 26 | } 27 | 28 | pub(crate) fn encode_message( 29 | buffer: &mut Vec, 30 | payload: &T, 31 | ) -> Result<(), BincodeError> { 32 | // this uses a two-byte length prefix followed by the bincode-ed payload 33 | 34 | buffer.resize(2, 0); 35 | 36 | DefaultOptions::new() 37 | .with_varint_encoding() 38 | .with_big_endian() 39 | .with_limit(u16::MAX as u64) 40 | .serialize_into(&mut *buffer, payload)?; 41 | 42 | let payload_size = buffer.len() - 2; 43 | let payload_size_bytes = (payload_size as u16).to_be_bytes(); 44 | 45 | buffer[0..2].copy_from_slice(&payload_size_bytes); 46 | 47 | Ok(()) 48 | } 49 | 50 | pub(crate) fn encode_chunk(buffer: &mut Vec, payload: &[u8]) -> Result<(), IoError> { 51 | use std::io::Write; 52 | 53 | buffer.clear(); 54 | write!(buffer, "{:x}\r\n", payload.len())?; 55 | buffer.extend_from_slice(payload); 56 | write!(buffer, "\r\n")?; 57 | 58 | Ok(()) 59 | } 60 | 61 | #[derive(Serialize)] 62 | pub struct Handshake { 63 | pub attributes: BTreeMap, 64 | } 65 | 66 | #[derive(Serialize)] 67 | pub struct Message<'a, 'callsite> { 68 | timestamp: NonZeroU64, 69 | span_id: Option, 70 | data: MessageData<'a, 'callsite>, 71 | } 72 | 73 | #[derive(Serialize)] 74 | enum MessageData<'a, 'callsite> { 75 | Create(CreateData<'a, 'callsite>), 76 | Update(UpdateData<'a, 'callsite>), 77 | Follows(FollowsData), 78 | Enter(EnterData), 79 | Exit, 80 | Close, 81 | Event(EventData<'a, 'callsite>), 82 | } 83 | 84 | impl Message<'_, '_> { 85 | pub(crate) fn from_new_span<'a, 'callsite, S: Subscriber + for<'lookup> LookupSpan<'lookup>>( 86 | attrs: &'a Attributes<'callsite>, 87 | id: &VenatorId, 88 | ctx: &Context<'_, S>, 89 | ) -> Message<'a, 'callsite> { 90 | let timestamp = now(); 91 | let metadata = attrs.metadata(); 92 | let parent_id = ctx.current_span().id().cloned(); 93 | 94 | let parent_id = parent_id 95 | .and_then(|id| ctx.span(&id)) 96 | .and_then(|span| span.extensions().get::().copied()); 97 | 98 | Message { 99 | timestamp, 100 | span_id: Some(id.0), 101 | data: MessageData::Create(CreateData { 102 | parent_id: parent_id.map(|id| id.0), 103 | target: metadata.target(), 104 | name: metadata.name(), 105 | level: level_to_number(*metadata.level()), 106 | file_name: metadata.file(), 107 | file_line: metadata.line(), 108 | attributes: attrs, 109 | }), 110 | } 111 | } 112 | 113 | pub(crate) fn from_record<'a, 'callsite>( 114 | id: &VenatorId, 115 | values: &'a Record<'callsite>, 116 | ) -> Message<'a, 'callsite> { 117 | let timestamp = now(); 118 | 119 | Message { 120 | timestamp, 121 | span_id: Some(id.0), 122 | data: MessageData::Update(UpdateData { attributes: values }), 123 | } 124 | } 125 | 126 | pub(crate) fn from_follows( 127 | id: &VenatorId, 128 | follows_id: &VenatorId, 129 | ) -> Message<'static, 'static> { 130 | let timestamp = now(); 131 | 132 | Message { 133 | timestamp, 134 | span_id: Some(id.0), 135 | data: MessageData::Follows(FollowsData { 136 | follows: follows_id.0, 137 | }), 138 | } 139 | } 140 | 141 | pub(crate) fn from_enter(id: &VenatorId) -> Message<'static, 'static> { 142 | let timestamp = now(); 143 | 144 | Message { 145 | timestamp, 146 | span_id: Some(id.0), 147 | data: MessageData::Enter(EnterData { 148 | thread_id: thread_id::get() as u64, 149 | }), 150 | } 151 | } 152 | 153 | pub(crate) fn from_exit(id: &VenatorId) -> Message<'static, 'static> { 154 | let timestamp = now(); 155 | 156 | Message { 157 | timestamp, 158 | span_id: Some(id.0), 159 | data: MessageData::Exit, 160 | } 161 | } 162 | 163 | pub(crate) fn from_close(id: &VenatorId) -> Message<'static, 'static> { 164 | let timestamp = now(); 165 | 166 | Message { 167 | timestamp, 168 | span_id: Some(id.0), 169 | data: MessageData::Close, 170 | } 171 | } 172 | 173 | pub(crate) fn from_event<'a, 'callsite, S: Subscriber + for<'lookup> LookupSpan<'lookup>>( 174 | event: &'a Event<'callsite>, 175 | ctx: &Context<'_, S>, 176 | ) -> Message<'a, 'callsite> { 177 | let timestamp = now(); 178 | let metadata = event.metadata(); 179 | 180 | let parent_id = ctx 181 | .event_span(event) 182 | .and_then(|span| span.extensions().get::().copied()); 183 | 184 | Message { 185 | timestamp, 186 | span_id: parent_id.map(|id| id.0), 187 | data: MessageData::Event(EventData { 188 | name: metadata.name(), 189 | target: metadata.target(), 190 | level: level_to_number(*metadata.level()), 191 | file_name: metadata.file(), 192 | file_line: metadata.line(), 193 | attributes: event, 194 | }), 195 | } 196 | } 197 | } 198 | 199 | #[derive(Serialize)] 200 | struct CreateData<'a, 'callsite> { 201 | parent_id: Option, 202 | target: &'static str, 203 | name: &'static str, 204 | level: i32, 205 | file_name: Option<&'static str>, 206 | file_line: Option, 207 | #[serde(serialize_with = "crate::attributes::from_attributes")] 208 | attributes: &'a Attributes<'callsite>, 209 | } 210 | 211 | #[derive(Serialize)] 212 | struct UpdateData<'a, 'callsite> { 213 | #[serde(serialize_with = "crate::attributes::from_record")] 214 | attributes: &'a Record<'callsite>, 215 | } 216 | 217 | #[derive(Serialize)] 218 | struct FollowsData { 219 | follows: NonZeroU64, 220 | } 221 | 222 | #[derive(Serialize)] 223 | struct EnterData { 224 | thread_id: u64, 225 | } 226 | 227 | #[derive(Serialize)] 228 | struct EventData<'a, 'callsite> { 229 | target: &'static str, 230 | name: &'static str, 231 | level: i32, 232 | file_name: Option<&'static str>, 233 | file_line: Option, 234 | #[serde(serialize_with = "crate::attributes::from_event")] 235 | attributes: &'a Event<'callsite>, 236 | } 237 | 238 | fn level_to_number(level: Level) -> i32 { 239 | match level { 240 | Level::TRACE => 0, 241 | Level::DEBUG => 1, 242 | Level::INFO => 2, 243 | Level::WARN => 3, 244 | Level::ERROR => 4, 245 | } 246 | } 247 | --------------------------------------------------------------------------------