Compare commits

..

9 Commits

Author SHA1 Message Date
grandwizard28
ed8917f8e6 fix(ruletypes): mark threshold variant kind/spec required 2026-05-05 06:04:08 +05:30
grandwizard28
43b7fea253 style(ruletypes): add blank lines in PrepareJSONSchema 2026-05-05 05:59:01 +05:30
grandwizard28
d0080abc8c style(openapi): add blank lines between logical blocks in attachDiscriminators 2026-05-05 05:57:31 +05:30
grandwizard28
0bb29f9e7f refactor(ruletypes): strip envelope parent properties in attachDiscriminators
Revert the json:"-" + custom MarshalJSON dance on the envelope
structs. Restore the original tags (json:"kind" / json:"spec"),
keep the discriminator marker, and clear the parent's redundant
properties / required block in attachDiscriminators after the
discriminator is promoted.
2026-05-05 05:47:24 +05:30
grandwizard28
d2130b4d44 fix(ruletypes): keep envelope kind/spec out of the parent schema via json:"-" 2026-05-05 05:43:15 +05:30
grandwizard28
ff36066b07 fix(ruletypes): mark evaluation variant kind/spec required 2026-05-05 05:37:33 +05:30
grandwizard28
e78a111494 chore(ruletypes): trim discriminator comments 2026-05-05 05:34:55 +05:30
grandwizard28
f1af8f242c refactor(openapi): inline discriminator constant and tighten attachDiscriminators 2026-05-05 05:32:48 +05:30
grandwizard28
b4412c02b6 feat(ruletypes): publish OpenAPI 3 discriminator on RuleThresholdData and EvaluationEnvelope
Both types model `{kind, spec}` discriminated unions on the wire but
the generated OpenAPI lacked the `discriminator:` keyword, so
codegen tools (oapi-codegen, terraform-plugin-codegen-openapi) fell
back to opaque `Spec: any` and consumers had to hand-write
JSON-bridges instead of typed Expand/Flatten.

Add the discriminator declaration via a marker convention:

- Each parent type implements `jsonschema.Preparer` to set an
  `x-signoz-discriminator` extra property carrying `propertyName` and
  the per-kind `mapping`.
- A small `attachDiscriminators` pass in pkg/signoz/openapi.go runs
  after spec reflection, walks every component schema, promotes the
  marker into a real openapi3.Discriminator, and removes the marker
  so it doesn't leak into the rendered YAML.

The two-step is required because jsonschema-go.Schema has no
Discriminator field of its own and openapi-go only carries through
`x-`-prefixed extras unchanged. The wire shape is unchanged —
`{kind: "<value>", spec: <variant>}` is still what's sent and
received.

Adding a new variant: append to JSONSchemaOneOf and add a
mapping entry on PrepareJSONSchema.
2026-05-05 05:22:23 +05:30
5 changed files with 158 additions and 42 deletions

View File

@@ -4348,19 +4348,20 @@ components:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec:
$ref: '#/components/schemas/RuletypesCumulativeWindow'
type: object
RuletypesEvaluationEnvelope:
oneOf:
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
properties:
kind:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec: {}
required:
- kind
- spec
type: object
RuletypesEvaluationEnvelope:
discriminator:
mapping:
cumulative: '#/components/schemas/RuletypesEvaluationCumulative'
rolling: '#/components/schemas/RuletypesEvaluationRolling'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
type: object
RuletypesEvaluationKind:
enum:
- rolling
@@ -4372,6 +4373,9 @@ components:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec:
$ref: '#/components/schemas/RuletypesRollingWindow'
required:
- kind
- spec
type: object
RuletypesGettableTestRule:
properties:
@@ -4679,15 +4683,12 @@ components:
- compositeQuery
type: object
RuletypesRuleThresholdData:
discriminator:
mapping:
basic: '#/components/schemas/RuletypesThresholdBasic'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/RuletypesThresholdBasic'
properties:
kind:
$ref: '#/components/schemas/RuletypesThresholdKind'
spec: {}
required:
- kind
- spec
type: object
RuletypesRuleType:
enum:
@@ -4729,6 +4730,9 @@ components:
$ref: '#/components/schemas/RuletypesThresholdKind'
spec:
$ref: '#/components/schemas/RuletypesBasicRuleThresholds'
required:
- kind
- spec
type: object
RuletypesThresholdKind:
enum:

View File

@@ -6578,28 +6578,36 @@ export interface RuletypesCumulativeWindowDTO {
timezone: string;
}
export enum RuletypesEvaluationCumulativeDTOKind {
cumulative = 'cumulative',
}
export interface RuletypesEvaluationCumulativeDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesCumulativeWindowDTO;
/**
* @type string
* @enum cumulative
*/
kind: RuletypesEvaluationCumulativeDTOKind;
spec: RuletypesCumulativeWindowDTO;
}
export type RuletypesEvaluationEnvelopeDTO =
| (RuletypesEvaluationRollingDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
})
| (RuletypesEvaluationCumulativeDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
});
| RuletypesEvaluationRollingDTO
| RuletypesEvaluationCumulativeDTO;
export enum RuletypesEvaluationKindDTO {
rolling = 'rolling',
cumulative = 'cumulative',
}
export enum RuletypesEvaluationRollingDTOKind {
rolling = 'rolling',
}
export interface RuletypesEvaluationRollingDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesRollingWindowDTO;
/**
* @type string
* @enum rolling
*/
kind: RuletypesEvaluationRollingDTOKind;
spec: RuletypesRollingWindowDTO;
}
export interface RuletypesGettableTestRuleDTO {
@@ -6954,10 +6962,7 @@ export interface RuletypesRuleConditionDTO {
thresholds?: RuletypesRuleThresholdDataDTO;
}
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO & {
kind: RuletypesThresholdKindDTO;
spec: unknown;
};
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO;
export enum RuletypesRuleTypeDTO {
threshold_rule = 'threshold_rule',
@@ -6993,9 +6998,16 @@ export enum RuletypesSeasonalityDTO {
daily = 'daily',
weekly = 'weekly',
}
export enum RuletypesThresholdBasicDTOKind {
basic = 'basic',
}
export interface RuletypesThresholdBasicDTO {
kind?: RuletypesThresholdKindDTO;
spec?: RuletypesBasicRuleThresholdsDTO;
/**
* @type string
* @enum basic
*/
kind: RuletypesThresholdBasicDTOKind;
spec: RuletypesBasicRuleThresholdsDTO;
}
export enum RuletypesThresholdKindDTO {

View File

@@ -44,6 +44,8 @@ import (
"gopkg.in/yaml.v2"
)
const signozDiscriminatorKey string = "x-signoz-discriminator"
type OpenAPI struct {
apiserver apiserver.APIServer
reflector *openapi3.Reflector
@@ -142,6 +144,8 @@ func (openapi *OpenAPI) CreateAndWrite(path string) error {
return err
}
attachDiscriminators(openapi.reflector.Spec)
// The library's MarshalYAML does a JSON round-trip that converts all numbers
// to float64, causing large integers (e.g. epoch millisecond timestamps) to
// render in scientific notation (1.6409952e+12).
@@ -199,3 +203,59 @@ func convertJSONNumbers(v interface{}) {
}
}
}
// attachDiscriminators promotes x-signoz-discriminator extensions
// into openapi3 Discriminator fields. Malformed markers are dropped.
func attachDiscriminators(spec *openapi3.Spec) {
if spec.Components == nil || spec.Components.Schemas == nil {
return
}
for name, entry := range spec.Components.Schemas.MapOfSchemaOrRefValues {
if entry.Schema == nil {
continue
}
raw, ok := entry.Schema.MapOfAnything[signozDiscriminatorKey]
if !ok {
continue
}
marker, ok := raw.(map[string]any)
if !ok {
continue
}
propertyName, ok := marker["propertyName"].(string)
if !ok || propertyName == "" {
continue
}
disc := openapi3.Discriminator{PropertyName: propertyName}
if rawMapping, ok := marker["mapping"]; ok {
if mapping, ok := rawMapping.(map[string]string); ok {
disc.Mapping = mapping
} else if mapping, ok := rawMapping.(map[string]any); ok {
converted := make(map[string]string, len(mapping))
for k, v := range mapping {
if s, ok := v.(string); ok {
converted[k] = s
}
}
disc.Mapping = converted
}
}
entry.Schema.Discriminator = &disc
delete(entry.Schema.MapOfAnything, signozDiscriminatorKey)
// The parent's reflected `properties` / `required` duplicate
// what the oneOf variants already declare, and orval intersects
// the two — turning a clean discriminated union DTO into a
// noisy union of intersections. Drop them here.
entry.Schema.Properties = nil
entry.Schema.Required = nil
spec.Components.Schemas.MapOfSchemaOrRefValues[name] = entry
}
}

View File

@@ -250,17 +250,20 @@ type EvaluationEnvelope struct {
// evaluationRolling is the OpenAPI schema for an EvaluationEnvelope with kind=rolling.
type evaluationRolling struct {
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
Spec RollingWindow `json:"spec" description:"The rolling window evaluation specification."`
Kind EvaluationKind `json:"kind" description:"The kind of evaluation." required:"true"`
Spec RollingWindow `json:"spec" description:"The rolling window evaluation specification." required:"true"`
}
// evaluationCumulative is the OpenAPI schema for an EvaluationEnvelope with kind=cumulative.
type evaluationCumulative struct {
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
Spec CumulativeWindow `json:"spec" description:"The cumulative window evaluation specification."`
Kind EvaluationKind `json:"kind" description:"The kind of evaluation." required:"true"`
Spec CumulativeWindow `json:"spec" description:"The cumulative window evaluation specification." required:"true"`
}
var _ jsonschema.OneOfExposer = EvaluationEnvelope{}
var (
_ jsonschema.OneOfExposer = EvaluationEnvelope{}
_ jsonschema.Preparer = EvaluationEnvelope{}
)
// JSONSchemaOneOf returns the oneOf variants for the EvaluationEnvelope discriminated union.
// Each variant represents a different evaluation kind with its corresponding spec schema.
@@ -271,6 +274,22 @@ func (EvaluationEnvelope) JSONSchemaOneOf() []any {
}
}
func (EvaluationEnvelope) PrepareJSONSchema(schema *jsonschema.Schema) error {
if schema.ExtraProperties == nil {
schema.ExtraProperties = map[string]any{}
}
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "kind",
"mapping": map[string]string{
"rolling": "#/components/schemas/RuletypesEvaluationRolling",
"cumulative": "#/components/schemas/RuletypesEvaluationCumulative",
},
}
return nil
}
func (e *EvaluationEnvelope) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {

View File

@@ -36,11 +36,14 @@ type RuleThresholdData struct {
// thresholdBasic is the OpenAPI schema for a RuleThresholdData with kind=basic.
type thresholdBasic struct {
Kind ThresholdKind `json:"kind" description:"The kind of threshold."`
Spec BasicRuleThresholds `json:"spec" description:"The basic threshold specification (array of thresholds)."`
Kind ThresholdKind `json:"kind" description:"The kind of threshold." required:"true"`
Spec BasicRuleThresholds `json:"spec" description:"The basic threshold specification (array of thresholds)." required:"true"`
}
var _ jsonschema.OneOfExposer = RuleThresholdData{}
var (
_ jsonschema.OneOfExposer = RuleThresholdData{}
_ jsonschema.Preparer = RuleThresholdData{}
)
// JSONSchemaOneOf returns the oneOf variants for the RuleThresholdData discriminated union.
// Each variant represents a different threshold kind with its corresponding spec schema.
@@ -50,6 +53,24 @@ func (RuleThresholdData) JSONSchemaOneOf() []any {
}
}
// PrepareJSONSchema marks the schema with x-signoz-discriminator;
// signoz.attachDiscriminators promotes it to a real OpenAPI 3
// discriminator after reflection.
func (RuleThresholdData) PrepareJSONSchema(schema *jsonschema.Schema) error {
if schema.ExtraProperties == nil {
schema.ExtraProperties = map[string]any{}
}
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "kind",
"mapping": map[string]string{
"basic": "#/components/schemas/RuletypesThresholdBasic",
},
}
return nil
}
func (r *RuleThresholdData) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {