fastapi-radar 0.1.6__py3-none-any.whl → 0.1.7__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.

Potentially problematic release.


This version of fastapi-radar might be problematic. Click here for more details.

@@ -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(
@@ -85,18 +135,45 @@ class RadarMiddleware(BaseHTTPMiddleware):
85
135
  except Exception as e:
86
136
  exception_occurred = True
87
137
  self._capture_exception(request_id, e)
138
+
139
+ # Record exception in span
140
+ if trace_ctx and root_span_id:
141
+ trace_ctx.add_span_log(
142
+ root_span_id,
143
+ f"Exception occurred: {str(e)}",
144
+ level="error",
145
+ exception_type=type(e).__name__,
146
+ )
147
+
88
148
  raise
89
149
 
90
150
  finally:
91
151
  duration = round((time.time() - start_time) * 1000, 2)
92
152
  captured_request.duration_ms = duration
93
153
 
154
+ # Finish span tracking
155
+ if trace_ctx and root_span_id:
156
+ status = "error" if exception_occurred else "ok"
157
+ trace_ctx.finish_span(
158
+ root_span_id,
159
+ status=status,
160
+ tags={
161
+ "http.status_code": response.status_code if response else None,
162
+ "duration_ms": duration,
163
+ },
164
+ )
165
+
94
166
  with self.get_session() as session:
95
167
  session.add(captured_request)
96
168
  if exception_occurred:
97
169
  exception_data = self._get_exception_data(request_id)
98
170
  if exception_data:
99
171
  session.add(exception_data)
172
+
173
+ # Persist trace data
174
+ if trace_ctx and self.tracing_manager:
175
+ self.tracing_manager.save_trace_context(trace_ctx)
176
+
100
177
  session.commit()
101
178
 
102
179
  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,9 @@ 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,
32
37
  ):
33
38
  self.app = app
34
39
  self.db_engine = db_engine
@@ -39,38 +44,47 @@ class Radar:
39
44
  self.capture_sql_bindings = capture_sql_bindings
40
45
  self.exclude_paths = exclude_paths or []
41
46
  self.theme = theme
42
- self.query_capture = None # Initialize to None
47
+ self.enable_tracing = enable_tracing
48
+ self.service_name = service_name
49
+ self.query_capture = None
43
50
 
44
- # Add all radar paths to excluded paths - exclude everything under /__radar
51
+ # Exclude radar dashboard paths
45
52
  if dashboard_path not in self.exclude_paths:
46
53
  self.exclude_paths.append(dashboard_path)
47
-
48
- # Exclude favicon.ico
49
54
  self.exclude_paths.append("/favicon.ico")
50
55
 
51
- # Setup storage engine (default to SQLite)
56
+ # Setup storage engine
52
57
  if storage_engine:
53
58
  self.storage_engine = storage_engine
54
59
  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
- )
60
+ storage_url = os.environ.get("RADAR_STORAGE_URL")
61
+ if storage_url:
62
+ self.storage_engine = create_engine(storage_url)
63
+ else:
64
+ # Use DuckDB for analytics-optimized storage
65
+ # Import duckdb_engine to register the dialect
66
+ import duckdb_engine # noqa: F401
67
+
68
+ radar_db_path = Path.cwd() / "radar.duckdb"
69
+ self.storage_engine = create_engine(
70
+ f"duckdb:///{radar_db_path}",
71
+ connect_args={
72
+ "read_only": False,
73
+ "config": {"memory_limit": "500mb"},
74
+ },
75
+ )
59
76
 
60
- # Create session maker for storage
61
77
  self.SessionLocal = sessionmaker(
62
78
  autocommit=False, autoflush=False, bind=self.storage_engine
63
79
  )
64
80
 
65
- # Initialize components
66
81
  self._setup_middleware()
67
82
 
68
- # Only setup query capture if db_engine is provided
69
83
  if self.db_engine:
70
84
  self._setup_query_capture()
71
85
 
72
- self._setup_api()
73
- self._setup_dashboard()
86
+ self._setup_api(include_in_schema=include_in_schema)
87
+ self._setup_dashboard(include_in_schema=include_in_schema)
74
88
 
75
89
  @contextmanager
76
90
  def get_session(self) -> Session:
@@ -89,6 +103,8 @@ class Radar:
89
103
  exclude_paths=self.exclude_paths,
90
104
  max_body_size=10000,
91
105
  capture_response_body=True,
106
+ enable_tracing=self.enable_tracing,
107
+ service_name=self.service_name,
92
108
  )
93
109
 
94
110
  def _setup_query_capture(self) -> None:
@@ -104,15 +120,15 @@ class Radar:
104
120
  )
105
121
  self.query_capture.register(self.db_engine)
106
122
 
107
- def _setup_api(self) -> None:
123
+ def _setup_api(self, include_in_schema: bool) -> None:
108
124
  """Mount API endpoints."""
109
125
  api_router = create_api_router(self.get_session)
110
- self.app.include_router(api_router)
126
+ self.app.include_router(api_router, include_in_schema=include_in_schema)
111
127
 
112
- def _setup_dashboard(self) -> None:
128
+ def _setup_dashboard(self, include_in_schema: bool) -> None:
113
129
  """Mount dashboard static files."""
114
- from fastapi.responses import FileResponse
115
130
  from fastapi import Request
131
+ from fastapi.responses import FileResponse
116
132
 
117
133
  dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
118
134
 
@@ -131,7 +147,10 @@ class Radar:
131
147
 
132
148
  # Add a catch-all route for the dashboard SPA
133
149
  # This ensures all sub-routes under /__radar serve the index.html
134
- @self.app.get(f"{self.dashboard_path}/{{full_path:path}}")
150
+ @self.app.get(
151
+ f"{self.dashboard_path}/{{full_path:path}}",
152
+ include_in_schema=include_in_schema,
153
+ )
135
154
  async def serve_dashboard(request: Request, full_path: str = ""):
136
155
  # Check if it's a request for a static asset
137
156
  if full_path and any(
@@ -160,7 +179,6 @@ class Radar:
160
179
  return {"error": "Dashboard not found. Please build the dashboard."}
161
180
 
162
181
  def _create_placeholder_dashboard(self, dashboard_dir: Path) -> None:
163
- """Create a placeholder dashboard for development."""
164
182
  index_html = dashboard_dir / "index.html"
165
183
  index_html.write_text(
166
184
  """
@@ -255,11 +273,15 @@ class Radar:
255
273
  const response = await fetch('/__radar/api/stats?hours=1');
256
274
  const data = await response.json();
257
275
 
258
- document.querySelectorAll('.stat-value')[0].textContent = data.total_requests;
259
- document.querySelectorAll('.stat-value')[1].textContent = data.total_queries;
276
+ document.querySelectorAll('.stat-value')[0].textContent =
277
+ data.total_requests;
278
+ document.querySelectorAll('.stat-value')[1].textContent =
279
+ data.total_queries;
260
280
  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;
281
+ data.avg_response_time ?
282
+ `${{data.avg_response_time.toFixed(1)}}ms` : '--';
283
+ document.querySelectorAll('.stat-value')[3].textContent =
284
+ data.total_exceptions;
263
285
 
264
286
  document.querySelectorAll('.stat-value').forEach(el => {{
265
287
  el.classList.remove('loading');
@@ -282,16 +304,14 @@ class Radar:
282
304
  )
283
305
 
284
306
  def create_tables(self) -> None:
285
- """Create radar storage tables."""
286
307
  Base.metadata.create_all(bind=self.storage_engine)
287
308
 
288
309
  def drop_tables(self) -> None:
289
- """Drop radar storage tables."""
290
310
  Base.metadata.drop_all(bind=self.storage_engine)
291
311
 
292
312
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
293
- """Clean up old captured data."""
294
313
  from datetime import datetime, timedelta
314
+
295
315
  from .models import CapturedRequest
296
316
 
297
317
  with self.get_session() as session: