fastapi-radar 0.1.5__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-CxIRSjZZ.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,20 +1,24 @@
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:
20
+ query_capture: Optional[QueryCapture]
21
+
18
22
  def __init__(
19
23
  self,
20
24
  app: FastAPI,
@@ -27,6 +31,9 @@ class Radar:
27
31
  capture_sql_bindings: bool = True,
28
32
  exclude_paths: Optional[List[str]] = None,
29
33
  theme: str = "auto",
34
+ enable_tracing: bool = True,
35
+ service_name: str = "fastapi-app",
36
+ include_in_schema: bool = True,
30
37
  ):
31
38
  self.app = app
32
39
  self.db_engine = db_engine
@@ -37,38 +44,47 @@ class Radar:
37
44
  self.capture_sql_bindings = capture_sql_bindings
38
45
  self.exclude_paths = exclude_paths or []
39
46
  self.theme = theme
40
- self.query_capture = None # Initialize to None
47
+ self.enable_tracing = enable_tracing
48
+ self.service_name = service_name
49
+ self.query_capture = None
41
50
 
42
- # Add all radar paths to excluded paths - exclude everything under /__radar
51
+ # Exclude radar dashboard paths
43
52
  if dashboard_path not in self.exclude_paths:
44
53
  self.exclude_paths.append(dashboard_path)
45
-
46
- # Exclude favicon.ico
47
54
  self.exclude_paths.append("/favicon.ico")
48
55
 
49
- # Setup storage engine (default to SQLite)
56
+ # Setup storage engine
50
57
  if storage_engine:
51
58
  self.storage_engine = storage_engine
52
59
  else:
53
- radar_db_path = Path.cwd() / "radar.db"
54
- self.storage_engine = create_engine(
55
- f"sqlite:///{radar_db_path}", connect_args={"check_same_thread": False}
56
- )
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
+ )
57
76
 
58
- # Create session maker for storage
59
77
  self.SessionLocal = sessionmaker(
60
78
  autocommit=False, autoflush=False, bind=self.storage_engine
61
79
  )
62
80
 
63
- # Initialize components
64
81
  self._setup_middleware()
65
82
 
66
- # Only setup query capture if db_engine is provided
67
83
  if self.db_engine:
68
84
  self._setup_query_capture()
69
85
 
70
- self._setup_api()
71
- self._setup_dashboard()
86
+ self._setup_api(include_in_schema=include_in_schema)
87
+ self._setup_dashboard(include_in_schema=include_in_schema)
72
88
 
73
89
  @contextmanager
74
90
  def get_session(self) -> Session:
@@ -87,10 +103,16 @@ class Radar:
87
103
  exclude_paths=self.exclude_paths,
88
104
  max_body_size=10000,
89
105
  capture_response_body=True,
106
+ enable_tracing=self.enable_tracing,
107
+ service_name=self.service_name,
90
108
  )
91
109
 
92
110
  def _setup_query_capture(self) -> None:
93
111
  """Setup SQLAlchemy query capture."""
112
+ assert (
113
+ self.db_engine is not None
114
+ ), "db_engine must be set before calling _setup_query_capture"
115
+
94
116
  self.query_capture = QueryCapture(
95
117
  get_session=self.get_session,
96
118
  capture_bindings=self.capture_sql_bindings,
@@ -98,15 +120,15 @@ class Radar:
98
120
  )
99
121
  self.query_capture.register(self.db_engine)
100
122
 
101
- def _setup_api(self) -> None:
123
+ def _setup_api(self, include_in_schema: bool) -> None:
102
124
  """Mount API endpoints."""
103
125
  api_router = create_api_router(self.get_session)
104
- self.app.include_router(api_router)
126
+ self.app.include_router(api_router, include_in_schema=include_in_schema)
105
127
 
106
- def _setup_dashboard(self) -> None:
128
+ def _setup_dashboard(self, include_in_schema: bool) -> None:
107
129
  """Mount dashboard static files."""
108
- from fastapi.responses import FileResponse
109
130
  from fastapi import Request
131
+ from fastapi.responses import FileResponse
110
132
 
111
133
  dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
112
134
 
@@ -125,7 +147,10 @@ class Radar:
125
147
 
126
148
  # Add a catch-all route for the dashboard SPA
127
149
  # This ensures all sub-routes under /__radar serve the index.html
128
- @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
+ )
129
154
  async def serve_dashboard(request: Request, full_path: str = ""):
130
155
  # Check if it's a request for a static asset
131
156
  if full_path and any(
@@ -154,7 +179,6 @@ class Radar:
154
179
  return {"error": "Dashboard not found. Please build the dashboard."}
155
180
 
156
181
  def _create_placeholder_dashboard(self, dashboard_dir: Path) -> None:
157
- """Create a placeholder dashboard for development."""
158
182
  index_html = dashboard_dir / "index.html"
159
183
  index_html.write_text(
160
184
  """
@@ -249,11 +273,15 @@ class Radar:
249
273
  const response = await fetch('/__radar/api/stats?hours=1');
250
274
  const data = await response.json();
251
275
 
252
- document.querySelectorAll('.stat-value')[0].textContent = data.total_requests;
253
- 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;
254
280
  document.querySelectorAll('.stat-value')[2].textContent =
255
- data.avg_response_time ? `${{data.avg_response_time.toFixed(1)}}ms` : '--';
256
- 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;
257
285
 
258
286
  document.querySelectorAll('.stat-value').forEach(el => {{
259
287
  el.classList.remove('loading');
@@ -276,16 +304,14 @@ class Radar:
276
304
  )
277
305
 
278
306
  def create_tables(self) -> None:
279
- """Create radar storage tables."""
280
307
  Base.metadata.create_all(bind=self.storage_engine)
281
308
 
282
309
  def drop_tables(self) -> None:
283
- """Drop radar storage tables."""
284
310
  Base.metadata.drop_all(bind=self.storage_engine)
285
311
 
286
312
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
287
- """Clean up old captured data."""
288
313
  from datetime import datetime, timedelta
314
+
289
315
  from .models import CapturedRequest
290
316
 
291
317
  with self.get_session() as session: