fastapi-radar 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 0 0% 9%;--card: 0 0% 100%;--card-foreground: 0 0% 9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 9%;--primary: 0 0% 9%;--primary-foreground: 0 0% 100%;--secondary: 0 0% 96%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96%;--muted-foreground: 0 0% 45%;--accent: 0 0% 96%;--accent-foreground: 0 0% 9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 100%;--border: 0 0% 92%;--input: 0 0% 92%;--ring: 0 0% 9%;--radius: .375rem;--chart-1: 0 0% 15%;--chart-2: 0 0% 30%;--chart-3: 0 0% 45%;--chart-4: 0 0% 60%;--chart-5: 0 0% 75%}.dark{--background: 0 0% 3%;--foreground: 0 0% 98%;--card: 0 0% 5%;--card-foreground: 0 0% 98%;--popover: 0 0% 3%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 3%;--secondary: 0 0% 10%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 10%;--muted-foreground: 0 0% 60%;--accent: 0 0% 10%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14%;--input: 0 0% 14%;--ring: 0 0% 98%;--chart-1: 0 0% 85%;--chart-2: 0 0% 70%;--chart-3: 0 0% 55%;--chart-4: 0 0% 40%;--chart-5: 0 0% 25%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.-bottom-5{bottom:-1.25rem}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.left-1\/4{left:25%}.left-2{left:.5rem}.left-3{left:.75rem}.left-3\/4{left:75%}.left-\[50\%\]{left:50%}.right-0{right:0}.right-2{right:.5rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100px\]{height:100px}.h-\[1px\]{height:1px}.h-\[200px\]{height:200px}.h-\[300px\]{height:300px}.h-\[400px\]{height:400px}.h-\[80vh\]{height:80vh}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-\[--radix-select-content-available-height\]{max-height:var(--radix-select-content-available-height)}.max-h-\[var\(--radix-dropdown-menu-content-available-height\)\]{max-height:var(--radix-dropdown-menu-content-available-height)}.w-1\/3{width:33.333333%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[150px\]{width:150px}.w-\[1px\]{width:1px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[200px\]{min-width:200px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.origin-\[--radix-dropdown-menu-content-transform-origin\]{transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\[--radix-select-content-transform-origin\]{transform-origin:var(--radix-select-content-transform-origin)}.origin-\[--radix-tooltip-content-transform-origin\]{transform-origin:var(--radix-tooltip-content-transform-origin)}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-destructive{border-color:hsl(var(--destructive))}.border-destructive\/20{border-color:hsl(var(--destructive) / .2)}.border-destructive\/50{border-color:hsl(var(--destructive) / .5)}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-input{border-color:hsl(var(--input))}.border-transparent{border-color:transparent}.border-l-border{border-left-color:hsl(var(--border))}.border-l-destructive{border-left-color:hsl(var(--destructive))}.border-l-foreground{border-left-color:hsl(var(--foreground))}.border-l-muted-foreground{border-left-color:hsl(var(--muted-foreground))}.border-l-transparent{border-left-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-background{background-color:hsl(var(--background))}.bg-background\/95{background-color:hsl(var(--background) / .95)}.bg-black\/80{background-color:#000c}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-foreground{background-color:hsl(var(--foreground))}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted-foreground{background-color:hsl(var(--muted-foreground))}.bg-muted-foreground\/60{background-color:hsl(var(--muted-foreground) / .6)}.bg-muted\/10{background-color:hsl(var(--muted) / .1)}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/20{background-color:hsl(var(--primary) / .2)}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-muted\/5{--tw-gradient-to: hsl(var(--muted) / .05) var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-\[1px\]{padding:1px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-12{padding-bottom:3rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-10{padding-left:2.5rem}.pl-2{padding-left:.5rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pl-9{padding-left:2.25rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-foreground{color:hsl(var(--foreground))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-muted{color:hsl(var(--muted))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/20{color:hsl(var(--muted-foreground) / .2)}.text-muted-foreground\/60{color:hsl(var(--muted-foreground) / .6)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.animate-in{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.fade-in-0{--tw-enter-opacity: 0}.zoom-in-95{--tw-enter-scale: .95}.duration-200{animation-duration:.2s}.duration-300{animation-duration:.3s}.duration-500{animation-duration:.5s}.ease-in-out{animation-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{animation-timing-function:cubic-bezier(0,0,.2,1)}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-foreground::file-selector-button{color:hsl(var(--foreground))}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=open\]\:bg-secondary[data-state=open]{background-color:hsl(var(--secondary))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:hsl(var(--muted-foreground))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:hsl(var(--muted-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=closed\]\:duration-300[data-state=closed]{transition-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{transition-duration:.5s}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y: 100%}.data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x: -100%}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x: 100%}.data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y: -100%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y: 100%}.data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x: -100%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x: 100%}.data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y: -100%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.data-\[state\=closed\]\:duration-300[data-state=closed]{animation-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{animation-duration:.5s}.dark\:border-gray-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}@media (min-width: 640px){.sm\:flex{display:flex}.sm\:max-w-sm{max-width:24rem}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\>span\]\:line-clamp-1>span{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.\[\&\>svg\]\:size-4>svg{width:1rem;height:1rem}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>FastAPI Radar - Debugging Dashboard</title>
7
- <script type="module" crossorigin src="/__radar/assets/index-BJa0l2JD.js"></script>
8
- <link rel="stylesheet" crossorigin href="/__radar/assets/index-DCxkDBhr.css">
7
+ <script type="module" crossorigin src="/__radar/assets/index-By5DXl8Z.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/__radar/assets/index-XlGcZj49.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -4,13 +4,21 @@ import json
4
4
  import time
5
5
  import traceback
6
6
  import uuid
7
- from typing import Optional, Callable
8
7
  from contextvars import ContextVar
8
+ from typing import Callable, Optional
9
+
9
10
  from starlette.middleware.base import BaseHTTPMiddleware
10
11
  from starlette.requests import Request
11
12
  from starlette.responses import Response, StreamingResponse
13
+
12
14
  from .models import CapturedRequest, CapturedException
13
15
  from .utils import serialize_headers, get_client_ip, truncate_body
16
+ from .tracing import (
17
+ TraceContext,
18
+ TracingManager,
19
+ create_trace_context,
20
+ set_trace_context,
21
+ )
14
22
 
15
23
  request_context: ContextVar[Optional[str]] = ContextVar("request_id", default=None)
16
24
 
@@ -23,12 +31,17 @@ class RadarMiddleware(BaseHTTPMiddleware):
23
31
  exclude_paths: list[str] = None,
24
32
  max_body_size: int = 10000,
25
33
  capture_response_body: bool = True,
34
+ enable_tracing: bool = True,
35
+ service_name: str = "fastapi-app",
26
36
  ):
27
37
  super().__init__(app)
28
38
  self.get_session = get_session
29
39
  self.exclude_paths = exclude_paths or []
30
40
  self.max_body_size = max_body_size
31
41
  self.capture_response_body = capture_response_body
42
+ self.enable_tracing = enable_tracing
43
+ self.service_name = service_name
44
+ self.tracing_manager = TracingManager(get_session) if enable_tracing else None
32
45
 
33
46
  async def dispatch(self, request: Request, call_next) -> Response:
34
47
  if self._should_skip(request):
@@ -38,6 +51,43 @@ class RadarMiddleware(BaseHTTPMiddleware):
38
51
  request_context.set(request_id)
39
52
  start_time = time.time()
40
53
 
54
+ # Create tracing context for this request
55
+ trace_ctx = None
56
+ root_span_id = None
57
+
58
+ if self.enable_tracing and self.tracing_manager:
59
+ existing_trace_id = request.headers.get("x-trace-id")
60
+ parent_span_id = request.headers.get("x-parent-span-id")
61
+
62
+ if existing_trace_id:
63
+ # Child span for existing trace
64
+ trace_ctx = TraceContext(existing_trace_id, self.service_name)
65
+ else:
66
+ # Create a new trace
67
+ trace_ctx = create_trace_context(self.service_name)
68
+
69
+ # Set tracing context
70
+ set_trace_context(trace_ctx)
71
+
72
+ # Create root span
73
+ root_span_id = trace_ctx.create_span(
74
+ operation_name=f"{request.method} {request.url.path}",
75
+ parent_span_id=parent_span_id,
76
+ span_kind="server",
77
+ tags={
78
+ "http.method": request.method,
79
+ "http.url": str(request.url),
80
+ "http.path": request.url.path,
81
+ "http.query": (
82
+ str(request.query_params) if request.query_params else None
83
+ ),
84
+ "user_agent": request.headers.get("user-agent"),
85
+ "request_id": request_id,
86
+ },
87
+ )
88
+
89
+ trace_ctx.set_current_span(root_span_id)
90
+
41
91
  request_body = await self._get_request_body(request)
42
92
 
43
93
  captured_request = CapturedRequest(
@@ -59,24 +109,28 @@ class RadarMiddleware(BaseHTTPMiddleware):
59
109
  exception_occurred = False
60
110
 
61
111
  try:
62
- response = await call_next(request)
112
+ response = original_response = await call_next(request)
63
113
 
64
114
  captured_request.status_code = response.status_code
65
115
  captured_request.response_headers = serialize_headers(response.headers)
66
116
 
67
- if self.capture_response_body and not isinstance(
68
- response, StreamingResponse
69
- ):
70
- response_body = b""
71
- async for chunk in response.body_iterator:
72
- response_body += chunk
73
-
74
- captured_request.response_body = truncate_body(
75
- response_body.decode("utf-8", errors="ignore"), self.max_body_size
76
- )
77
-
78
- response = Response(
79
- content=response_body,
117
+ if self.capture_response_body:
118
+
119
+ async def capture_response():
120
+ response_body = ""
121
+ async for chunk in original_response.body_iterator:
122
+ yield chunk
123
+ if len(response_body) < self.max_body_size:
124
+ response_body += chunk.decode("utf-8", errors="ignore")
125
+ with self.get_session() as session:
126
+ captured_request.response_body = truncate_body(
127
+ response_body, self.max_body_size
128
+ )
129
+ session.add(captured_request)
130
+ session.commit()
131
+
132
+ response = StreamingResponse(
133
+ content=capture_response(),
80
134
  status_code=response.status_code,
81
135
  headers=dict(response.headers),
82
136
  media_type=response.media_type,
@@ -85,18 +139,45 @@ class RadarMiddleware(BaseHTTPMiddleware):
85
139
  except Exception as e:
86
140
  exception_occurred = True
87
141
  self._capture_exception(request_id, e)
142
+
143
+ # Record exception in span
144
+ if trace_ctx and root_span_id:
145
+ trace_ctx.add_span_log(
146
+ root_span_id,
147
+ f"Exception occurred: {str(e)}",
148
+ level="error",
149
+ exception_type=type(e).__name__,
150
+ )
151
+
88
152
  raise
89
153
 
90
154
  finally:
91
155
  duration = round((time.time() - start_time) * 1000, 2)
92
156
  captured_request.duration_ms = duration
93
157
 
158
+ # Finish span tracking
159
+ if trace_ctx and root_span_id:
160
+ status = "error" if exception_occurred else "ok"
161
+ trace_ctx.finish_span(
162
+ root_span_id,
163
+ status=status,
164
+ tags={
165
+ "http.status_code": response.status_code if response else None,
166
+ "duration_ms": duration,
167
+ },
168
+ )
169
+
94
170
  with self.get_session() as session:
95
171
  session.add(captured_request)
96
172
  if exception_occurred:
97
173
  exception_data = self._get_exception_data(request_id)
98
174
  if exception_data:
99
175
  session.add(exception_data)
176
+
177
+ # Persist trace data
178
+ if trace_ctx and self.tracing_manager:
179
+ self.tracing_manager.save_trace_context(trace_ctx)
180
+
100
181
  session.commit()
101
182
 
102
183
  request_context.set(None)
fastapi_radar/models.py CHANGED
@@ -1,9 +1,23 @@
1
1
  """Storage models for FastAPI Radar."""
2
2
 
3
3
  from datetime import datetime
4
- from sqlalchemy import Column, String, Integer, Float, Text, DateTime, ForeignKey, JSON
5
- from sqlalchemy.ext.declarative import declarative_base
6
- from sqlalchemy.orm import relationship
4
+
5
+ from sqlalchemy import (
6
+ Column,
7
+ String,
8
+ Integer,
9
+ Float,
10
+ Text,
11
+ DateTime,
12
+ JSON,
13
+ Sequence,
14
+ )
15
+
16
+ try:
17
+ from sqlalchemy.orm import declarative_base
18
+ except ImportError:
19
+ from sqlalchemy.ext.declarative import declarative_base
20
+ from sqlalchemy.orm import relationship, foreign # noqa: F401
7
21
 
8
22
  Base = declarative_base()
9
23
 
@@ -11,7 +25,9 @@ Base = declarative_base()
11
25
  class CapturedRequest(Base):
12
26
  __tablename__ = "radar_requests"
13
27
 
14
- id = Column(Integer, primary_key=True, index=True)
28
+ id = Column(
29
+ Integer, Sequence("radar_requests_id_seq"), primary_key=True, index=True
30
+ )
15
31
  request_id = Column(String(36), unique=True, index=True, nullable=False)
16
32
  method = Column(String(10), nullable=False)
17
33
  url = Column(String(500), nullable=False)
@@ -27,20 +43,26 @@ class CapturedRequest(Base):
27
43
  created_at = Column(DateTime, default=datetime.utcnow, index=True)
28
44
 
29
45
  queries = relationship(
30
- "CapturedQuery", back_populates="request", cascade="all, delete-orphan"
46
+ "CapturedQuery",
47
+ back_populates="request",
48
+ primaryjoin="CapturedRequest.request_id == foreign(CapturedQuery.request_id)",
49
+ cascade="all, delete-orphan",
31
50
  )
32
51
  exceptions = relationship(
33
- "CapturedException", back_populates="request", cascade="all, delete-orphan"
52
+ "CapturedException",
53
+ back_populates="request",
54
+ primaryjoin=(
55
+ "CapturedRequest.request_id == foreign(CapturedException.request_id)"
56
+ ),
57
+ cascade="all, delete-orphan",
34
58
  )
35
59
 
36
60
 
37
61
  class CapturedQuery(Base):
38
62
  __tablename__ = "radar_queries"
39
63
 
40
- id = Column(Integer, primary_key=True, index=True)
41
- request_id = Column(
42
- String(36), ForeignKey("radar_requests.request_id", ondelete="CASCADE")
43
- )
64
+ id = Column(Integer, Sequence("radar_queries_id_seq"), primary_key=True, index=True)
65
+ request_id = Column(String(36), index=True)
44
66
  sql = Column(Text, nullable=False)
45
67
  parameters = Column(JSON)
46
68
  duration_ms = Column(Float)
@@ -48,19 +70,88 @@ class CapturedQuery(Base):
48
70
  connection_name = Column(String(100))
49
71
  created_at = Column(DateTime, default=datetime.utcnow, index=True)
50
72
 
51
- request = relationship("CapturedRequest", back_populates="queries")
73
+ request = relationship(
74
+ "CapturedRequest",
75
+ back_populates="queries",
76
+ primaryjoin="foreign(CapturedQuery.request_id) == CapturedRequest.request_id",
77
+ )
52
78
 
53
79
 
54
80
  class CapturedException(Base):
55
81
  __tablename__ = "radar_exceptions"
56
82
 
57
- id = Column(Integer, primary_key=True, index=True)
58
- request_id = Column(
59
- String(36), ForeignKey("radar_requests.request_id", ondelete="CASCADE")
83
+ id = Column(
84
+ Integer, Sequence("radar_exceptions_id_seq"), primary_key=True, index=True
60
85
  )
86
+ request_id = Column(String(36), index=True)
61
87
  exception_type = Column(String(100), nullable=False)
62
88
  exception_value = Column(Text)
63
89
  traceback = Column(Text, nullable=False)
64
90
  created_at = Column(DateTime, default=datetime.utcnow, index=True)
65
91
 
66
- request = relationship("CapturedRequest", back_populates="exceptions")
92
+ request = relationship(
93
+ "CapturedRequest",
94
+ back_populates="exceptions",
95
+ primaryjoin=(
96
+ "foreign(CapturedException.request_id) == CapturedRequest.request_id"
97
+ ),
98
+ )
99
+
100
+
101
+ class Trace(Base):
102
+ __tablename__ = "radar_traces"
103
+
104
+ trace_id = Column(String(32), primary_key=True, index=True)
105
+ service_name = Column(String(100), index=True)
106
+ operation_name = Column(String(200))
107
+ start_time = Column(DateTime, default=datetime.utcnow, index=True)
108
+ end_time = Column(DateTime)
109
+ duration_ms = Column(Float)
110
+ span_count = Column(Integer, default=0)
111
+ status = Column(String(20), default="ok")
112
+ tags = Column(JSON)
113
+ created_at = Column(DateTime, default=datetime.utcnow, index=True)
114
+
115
+ spans = relationship(
116
+ "Span",
117
+ back_populates="trace",
118
+ primaryjoin="Trace.trace_id == foreign(Span.trace_id)",
119
+ cascade="all, delete-orphan",
120
+ )
121
+
122
+
123
+ class Span(Base):
124
+ __tablename__ = "radar_spans"
125
+
126
+ span_id = Column(String(16), primary_key=True, index=True)
127
+ trace_id = Column(String(32), index=True)
128
+ parent_span_id = Column(String(16), index=True, nullable=True)
129
+ operation_name = Column(String(200), nullable=False)
130
+ service_name = Column(String(100), index=True)
131
+ span_kind = Column(String(20), default="server")
132
+ start_time = Column(DateTime, nullable=False, index=True)
133
+ end_time = Column(DateTime)
134
+ duration_ms = Column(Float)
135
+ status = Column(String(20), default="ok")
136
+ tags = Column(JSON)
137
+ logs = Column(JSON)
138
+ created_at = Column(DateTime, default=datetime.utcnow, index=True)
139
+
140
+ trace = relationship(
141
+ "Trace",
142
+ back_populates="spans",
143
+ primaryjoin="foreign(Span.trace_id) == Trace.trace_id",
144
+ )
145
+
146
+
147
+ class SpanRelation(Base):
148
+ __tablename__ = "radar_span_relations"
149
+
150
+ id = Column(
151
+ Integer, Sequence("radar_span_relations_id_seq"), primary_key=True, index=True
152
+ )
153
+ trace_id = Column(String(32), index=True)
154
+ parent_span_id = Column(String(16), index=True)
155
+ child_span_id = Column(String(16), index=True)
156
+ depth = Column(Integer, default=0)
157
+ created_at = Column(DateTime, default=datetime.utcnow)
fastapi_radar/radar.py CHANGED
@@ -1,17 +1,19 @@
1
1
  """Main Radar class for FastAPI Radar."""
2
2
 
3
- from typing import Optional, List
4
- from pathlib import Path
5
3
  from contextlib import contextmanager
4
+ import os
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
6
8
  from fastapi import FastAPI
7
9
  from sqlalchemy import create_engine
8
10
  from sqlalchemy.engine import Engine
9
- from sqlalchemy.orm import sessionmaker, Session
11
+ from sqlalchemy.orm import Session, sessionmaker
10
12
 
11
- from .models import Base
12
- from .middleware import RadarMiddleware
13
- from .capture import QueryCapture
14
13
  from .api import create_api_router
14
+ from .capture import QueryCapture
15
+ from .middleware import RadarMiddleware
16
+ from .models import Base
15
17
 
16
18
 
17
19
  class Radar:
@@ -29,6 +31,10 @@ class Radar:
29
31
  capture_sql_bindings: bool = True,
30
32
  exclude_paths: Optional[List[str]] = None,
31
33
  theme: str = "auto",
34
+ enable_tracing: bool = True,
35
+ service_name: str = "fastapi-app",
36
+ include_in_schema: bool = True,
37
+ db_path: Optional[str] = None,
32
38
  ):
33
39
  self.app = app
34
40
  self.db_engine = db_engine
@@ -39,38 +45,75 @@ class Radar:
39
45
  self.capture_sql_bindings = capture_sql_bindings
40
46
  self.exclude_paths = exclude_paths or []
41
47
  self.theme = theme
42
- self.query_capture = None # Initialize to None
48
+ self.enable_tracing = enable_tracing
49
+ self.service_name = service_name
50
+ self.db_path = db_path
51
+ self.query_capture = None
43
52
 
44
- # Add all radar paths to excluded paths - exclude everything under /__radar
53
+ # Exclude radar dashboard paths
45
54
  if dashboard_path not in self.exclude_paths:
46
55
  self.exclude_paths.append(dashboard_path)
47
-
48
- # Exclude favicon.ico
49
56
  self.exclude_paths.append("/favicon.ico")
50
57
 
51
- # Setup storage engine (default to SQLite)
58
+ # Setup storage engine
52
59
  if storage_engine:
53
60
  self.storage_engine = storage_engine
54
61
  else:
55
- radar_db_path = Path.cwd() / "radar.db"
56
- self.storage_engine = create_engine(
57
- f"sqlite:///{radar_db_path}", connect_args={"check_same_thread": False}
58
- )
62
+ storage_url = os.environ.get("RADAR_STORAGE_URL")
63
+ if storage_url:
64
+ self.storage_engine = create_engine(storage_url)
65
+ else:
66
+ # Use DuckDB for analytics-optimized storage
67
+ # Import duckdb_engine to register the dialect
68
+ import duckdb_engine # noqa: F401
69
+
70
+ if self.db_path:
71
+ try:
72
+ # Avoid shadowing the attribute name by using a different variable name
73
+ provided_path = Path(self.db_path).resolve()
74
+ if provided_path.suffix.lower() == ".duckdb":
75
+ radar_db_path = provided_path
76
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
77
+ else:
78
+ radar_db_path = provided_path / "radar.duckdb"
79
+ provided_path.mkdir(parents=True, exist_ok=True)
80
+
81
+ except Exception as e:
82
+ # Fallback to current directory if path creation fails
83
+ import warnings
84
+
85
+ warnings.warn(
86
+ (
87
+ f"Failed to create database path '{self.db_path}': {e}. "
88
+ f"Using current directory."
89
+ ),
90
+ UserWarning,
91
+ )
92
+
93
+ radar_db_path = Path.cwd() / "radar.duckdb"
94
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
95
+ else:
96
+ radar_db_path = Path.cwd() / "radar.duckdb"
97
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
98
+ self.storage_engine = create_engine(
99
+ f"duckdb:///{radar_db_path}",
100
+ connect_args={
101
+ "read_only": False,
102
+ "config": {"memory_limit": "500mb"},
103
+ },
104
+ )
59
105
 
60
- # Create session maker for storage
61
106
  self.SessionLocal = sessionmaker(
62
107
  autocommit=False, autoflush=False, bind=self.storage_engine
63
108
  )
64
109
 
65
- # Initialize components
66
110
  self._setup_middleware()
67
111
 
68
- # Only setup query capture if db_engine is provided
69
112
  if self.db_engine:
70
113
  self._setup_query_capture()
71
114
 
72
- self._setup_api()
73
- self._setup_dashboard()
115
+ self._setup_api(include_in_schema=include_in_schema)
116
+ self._setup_dashboard(include_in_schema=include_in_schema)
74
117
 
75
118
  @contextmanager
76
119
  def get_session(self) -> Session:
@@ -89,6 +132,8 @@ class Radar:
89
132
  exclude_paths=self.exclude_paths,
90
133
  max_body_size=10000,
91
134
  capture_response_body=True,
135
+ enable_tracing=self.enable_tracing,
136
+ service_name=self.service_name,
92
137
  )
93
138
 
94
139
  def _setup_query_capture(self) -> None:
@@ -104,15 +149,15 @@ class Radar:
104
149
  )
105
150
  self.query_capture.register(self.db_engine)
106
151
 
107
- def _setup_api(self) -> None:
152
+ def _setup_api(self, include_in_schema: bool) -> None:
108
153
  """Mount API endpoints."""
109
154
  api_router = create_api_router(self.get_session)
110
- self.app.include_router(api_router)
155
+ self.app.include_router(api_router, include_in_schema=include_in_schema)
111
156
 
112
- def _setup_dashboard(self) -> None:
157
+ def _setup_dashboard(self, include_in_schema: bool) -> None:
113
158
  """Mount dashboard static files."""
114
- from fastapi.responses import FileResponse
115
159
  from fastapi import Request
160
+ from fastapi.responses import FileResponse
116
161
 
117
162
  dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
118
163
 
@@ -131,7 +176,10 @@ class Radar:
131
176
 
132
177
  # Add a catch-all route for the dashboard SPA
133
178
  # This ensures all sub-routes under /__radar serve the index.html
134
- @self.app.get(f"{self.dashboard_path}/{{full_path:path}}")
179
+ @self.app.get(
180
+ f"{self.dashboard_path}/{{full_path:path}}",
181
+ include_in_schema=include_in_schema,
182
+ )
135
183
  async def serve_dashboard(request: Request, full_path: str = ""):
136
184
  # Check if it's a request for a static asset
137
185
  if full_path and any(
@@ -160,7 +208,6 @@ class Radar:
160
208
  return {"error": "Dashboard not found. Please build the dashboard."}
161
209
 
162
210
  def _create_placeholder_dashboard(self, dashboard_dir: Path) -> None:
163
- """Create a placeholder dashboard for development."""
164
211
  index_html = dashboard_dir / "index.html"
165
212
  index_html.write_text(
166
213
  """
@@ -255,11 +302,15 @@ class Radar:
255
302
  const response = await fetch('/__radar/api/stats?hours=1');
256
303
  const data = await response.json();
257
304
 
258
- document.querySelectorAll('.stat-value')[0].textContent = data.total_requests;
259
- document.querySelectorAll('.stat-value')[1].textContent = data.total_queries;
305
+ document.querySelectorAll('.stat-value')[0].textContent =
306
+ data.total_requests;
307
+ document.querySelectorAll('.stat-value')[1].textContent =
308
+ data.total_queries;
260
309
  document.querySelectorAll('.stat-value')[2].textContent =
261
- data.avg_response_time ? `${{data.avg_response_time.toFixed(1)}}ms` : '--';
262
- document.querySelectorAll('.stat-value')[3].textContent = data.total_exceptions;
310
+ data.avg_response_time ?
311
+ `${{data.avg_response_time.toFixed(1)}}ms` : '--';
312
+ document.querySelectorAll('.stat-value')[3].textContent =
313
+ data.total_exceptions;
263
314
 
264
315
  document.querySelectorAll('.stat-value').forEach(el => {{
265
316
  el.classList.remove('loading');
@@ -282,16 +333,14 @@ class Radar:
282
333
  )
283
334
 
284
335
  def create_tables(self) -> None:
285
- """Create radar storage tables."""
286
336
  Base.metadata.create_all(bind=self.storage_engine)
287
337
 
288
338
  def drop_tables(self) -> None:
289
- """Drop radar storage tables."""
290
339
  Base.metadata.drop_all(bind=self.storage_engine)
291
340
 
292
341
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
293
- """Clean up old captured data."""
294
342
  from datetime import datetime, timedelta
343
+
295
344
  from .models import CapturedRequest
296
345
 
297
346
  with self.get_session() as session: