Mapeie dados usando fluxos de dados
Importante
Esta página inclui instruções para gerenciar componentes do Azure IoT Operations usando manifestos de implantação do Kubernetes, que está em visualização. Esse recurso é fornecido com várias limitações e não deve ser usado para cargas de trabalho de produção.
Veja Termos de Utilização Complementares da Pré-visualizações do Microsoft Azure para obter os termos legais que se aplicam às funcionalidades do Azure que estão na versão beta, na pré-visualização ou que ainda não foram lançadas para disponibilidade geral.
Use a linguagem de mapeamento de fluxo de dados para transformar dados nas Operações IoT do Azure. A sintaxe é uma maneira simples, mas poderosa, de definir mapeamentos que transformam dados de um formato para outro. Este artigo fornece uma visão geral da linguagem de mapeamento de fluxo de dados e dos principais conceitos.
O mapeamento permite transformar dados de um formato para outro. Considere o seguinte registro de entrada:
{
"Name": "Grace Owens",
"Place of birth": "London, TX",
"Birth Date": "19840202",
"Start Date": "20180812",
"Position": "Analyst",
"Office": "Kent, WA"
}
Compare-o com o registro de saída:
{
"Employee": {
"Name": "Grace Owens",
"Date of Birth": "19840202"
},
"Employment": {
"Start Date": "20180812",
"Position": "Analyst, Kent, WA",
"Base Salary": 78000
}
}
No registro de saída, as seguintes alterações são feitas nos dados do registro de entrada:
- Campos renomeados: O
Birth Date
campo agoraDate of Birth
é . - Campos reestruturados: Ambos
Name
eDate of Birth
estão agrupados na novaEmployee
categoria. - Campo excluído: o
Place of birth
campo é removido porque não está presente na saída. - Campo adicionado: O
Base Salary
campo é um novo campo naEmployment
categoria. - Valores de campo alterados ou mesclados: O
Position
campo na saída combina osPosition
campos eOffice
da entrada.
As transformações são alcançadas através de mapeamento, que normalmente envolve:
- Definição de entrada: Identificando os campos nos registros de entrada que são usados.
- Definição de saída: Especificando onde e como os campos de entrada são organizados nos registros de saída.
- Conversão (opcional): Modificar os campos de entrada para caber nos campos de saída.
expression
é necessário quando vários campos de entrada são combinados em um único campo de saída.
O mapeamento a seguir é um exemplo:
{
inputs: [
'BirthDate'
]
output: 'Employee.DateOfBirth'
}
{
inputs: [
'Position' // - - - - $1
'Office' // - - - - $2
]
output: 'Employment.Position'
expression: '$1 + ", " + $2'
}
{
inputs: [
'$context(position).BaseSalary'
]
output: 'Employment.BaseSalary'
}
Os mapas de exemplo:
- Mapeamento um-para-um:
BirthDate
é mapeado diretamente semEmployee.DateOfBirth
conversão. - Mapeamento muitos-para-um: combina
Position
eOffice
em um únicoEmployment.Position
campo. A fórmula de conversão ($1 + ", " + $2
) mescla esses campos em uma cadeia de caracteres formatada. - Dados contextuais:
BaseSalary
são adicionados a partir de um conjunto de dados contextual chamadoposition
.
Referências de campo
As referências de campo mostram como especificar caminhos na entrada e saída usando notação de pontos como Employee.DateOfBirth
ou acessando dados de um conjunto de dados contextual via $context(position)
.
Propriedades de metadados MQTT e Kafka
Quando você usa MQTT ou Kafka como origem ou destino, você pode acessar várias propriedades de metadados na linguagem de mapeamento. Essas propriedades podem ser mapeadas na entrada ou saída.
Propriedades de metadados
- Tópico: Funciona para MQTT e Kafka. Ele contém a cadeia de caracteres onde a mensagem foi publicada. Exemplo:
$metadata.topic
. - Propriedade do usuário: No MQTT, isso se refere aos pares chave/valor de forma livre que uma mensagem MQTT pode carregar. Por exemplo, se a mensagem MQTT foi publicada com uma propriedade de usuário com chave "prioridade" e valor "alto", então a
$metadata.user_property.priority
referência mantém o valor "alto". As chaves de propriedade do usuário podem ser cadeias de caracteres arbitrárias e podem exigir fuga:$metadata.user_property."weird key"
usa a chave "chave estranha" (com um espaço). - Propriedade do sistema: este termo é usado para cada propriedade que não é uma propriedade do usuário. Atualmente, apenas uma única propriedade do sistema é suportada:
$metadata.system_property.content_type
, que lê a propriedade de tipo de conteúdo da mensagem MQTT (se definida). - Cabeçalho: Este é o equivalente Kafka da propriedade de usuário MQTT. Kafka pode usar qualquer valor binário para uma chave, mas o fluxo de dados suporta apenas chaves de cadeia de caracteres UTF-8. Exemplo:
$metadata.header.priority
. Essa funcionalidade é semelhante às propriedades do usuário.
Mapeando propriedades de metadados
Mapeamento de entrada
No exemplo a seguir, a propriedade MQTT topic
é mapeada para o origin_topic
campo na saída:
inputs: [
'$metadata.topic'
]
output: 'origin_topic'
Se a propriedade priority
user estiver presente na mensagem MQTT, o exemplo a seguir demonstra como mapeá-la para um campo de saída:
inputs: [
'$metadata.user_property.priority'
]
output: 'priority'
Mapeamento de saída
Você também pode mapear propriedades de metadados para um cabeçalho de saída ou propriedade de usuário. No exemplo a seguir, o MQTT topic
é mapeado para o origin_topic
campo na propriedade user da saída:
inputs: [
'$metadata.topic'
]
output: '$metadata.user_property.origin_topic'
Se a carga de entrada contiver um priority
campo, o exemplo a seguir demonstra como mapeá-lo para uma propriedade de usuário MQTT:
inputs: [
'priority'
]
output: '$metadata.user_property.priority'
O mesmo exemplo para Kafka:
inputs: [
'priority'
]
output: '$metadata.header.priority'
Seletores de conjunto de dados de contextualização
Esses seletores permitem que mapeamentos integrem dados extras de bancos de dados externos, que são chamados de conjuntos de dados de contextualização.
Filtragem de registos
A filtragem de registros envolve a definição de condições para selecionar quais registros devem ser processados ou descartados.
Notação de pontos
A notação de pontos é amplamente utilizada em ciência da computação para campos de referência, mesmo recursivamente. Na programação, os nomes de campo normalmente consistem em letras e números. Um exemplo de notação de pontos padrão pode ser semelhante a este exemplo:
inputs: [
'Person.Address.Street.Number'
]
Em um fluxo de dados, um caminho descrito pela notação de pontos pode incluir cadeias de caracteres e alguns caracteres especiais sem precisar escapar:
inputs: [
'Person.Date of Birth'
]
Em outros casos, a fuga é necessária:
inputs: [
'Person."Tag.10".Value'
]
O exemplo anterior, entre outros caracteres especiais, contém pontos dentro do nome do campo. Sem escapar, o nome do campo serviria como um separador na própria notação de pontos.
Enquanto um fluxo de dados analisa um caminho, ele trata apenas dois caracteres como especiais:
- Os pontos (
.
) atuam como separadores de campo. - Aspas simples, quando colocadas no início ou no final de um segmento, iniciam uma seção com escape onde os pontos não são tratados como separadores de campo.
Quaisquer outros caracteres são tratados como parte do nome do campo. Essa flexibilidade é útil em formatos como JSON, onde os nomes de campo podem ser cadeias de caracteres arbitrárias.
No Bicep, todas as cadeias de caracteres são colocadas entre aspas simples ('
). Os exemplos sobre cotação adequada no YAML para uso do Kubernetes não se aplicam.
Fuga
A função principal de escapar em um caminho com anotação de pontos é acomodar o uso de pontos que fazem parte de nomes de campos em vez de separadores:
inputs: [
'Payload."Tag.10".Value'
]
Neste exemplo, o caminho consiste em três segmentos: Payload
, Tag.10
e Value
.
Escapando de regras na notação de pontos
Escape de cada segmento separadamente: se vários segmentos contiverem pontos, esses segmentos devem ser colocados entre aspas duplas. Outros segmentos também podem ser citados, mas isso não afeta a interpretação do caminho:
inputs: [ 'Payload."Tag.10".Measurements."Vibration.$12".Value' ]
Uso adequado de aspas duplas: aspas duplas devem abrir e fechar um segmento com escape. Todas as aspas no meio do segmento são consideradas parte do nome do campo:
inputs: [ 'Payload.He said: "Hello", and waved' ]
Este exemplo define dois campos: Payload
e He said: "Hello", and waved
. Quando um ponto aparece nessas circunstâncias, ele continua a servir como separador:
inputs: [
'Payload.He said: "No. It is done"'
]
Neste caso, o caminho é dividido nos segmentos Payload
, He said: "No
e It is done"
(começando com um espaço).
Algoritmo de segmentação
- Se o primeiro caractere de um segmento for uma aspas, o analisador procurará as aspas seguintes. A cadeia de caracteres entre essas aspas é considerada um único segmento.
- Se o segmento não começar com aspas, o analisador identificará segmentos procurando o próximo ponto ou o final do caminho.
Caráter universal
Em muitos cenários, o registro de saída se assemelha muito ao registro de entrada, com apenas pequenas modificações necessárias. Quando você lida com registros que contêm vários campos, especificar manualmente mapeamentos para cada campo pode se tornar tedioso. Os curingas simplificam esse processo, permitindo mapeamentos generalizados que podem ser aplicados automaticamente a vários campos.
Vamos considerar um cenário básico para entender o uso de asteriscos em mapeamentos:
inputs: [
'*'
]
output: '*'
Esta configuração mostra um mapeamento básico onde cada campo na entrada é diretamente mapeado para o mesmo campo na saída sem alterações. O asterisco (*
) serve como um curinga que corresponde a qualquer campo no registro de entrada.
Veja como o asterisco (*
) opera nesse contexto:
- Correspondência de padrões: o asterisco pode corresponder a um único segmento ou a vários segmentos de um caminho. Ele serve como um espaço reservado para quaisquer segmentos no caminho.
- Correspondência de campo: Durante o processo de mapeamento, o algoritmo avalia cada campo no registro de entrada em relação ao padrão especificado no
inputs
. O asterisco no exemplo anterior corresponde a todos os caminhos possíveis, ajustando efetivamente cada campo individual na entrada. - Segmento capturado: a parte do caminho à qual o asterisco corresponde é chamada de
captured segment
. - Mapeamento de saída: Na configuração de saída, o
captured segment
é colocado onde o asterisco aparece. Isso significa que a estrutura da entrada é preservada na saída, com ocaptured segment
preenchimento do espaço reservado fornecido pelo asterisco.
Outro exemplo ilustra como curingas podem ser usados para corresponder subseções e movê-las juntas. Este exemplo efetivamente nivela estruturas aninhadas dentro de um objeto JSON.
JSON original:
{
"ColorProperties": {
"Hue": "blue",
"Saturation": "90%",
"Brightness": "50%",
"Opacity": "0.8"
},
"TextureProperties": {
"type": "fabric",
"SurfaceFeel": "soft",
"SurfaceAppearance": "matte",
"Pattern": "knitted"
}
}
Configuração de mapeamento que usa curingas:
{
inputs: [
'ColorProperties.*'
]
output: '*'
}
{
inputs: [
'TextureProperties.*'
]
output: '*'
}
JSON resultante:
{
"Hue": "blue",
"Saturation": "90%",
"Brightness": "50%",
"Opacity": "0.8",
"type": "fabric",
"SurfaceFeel": "soft",
"SurfaceAppearance": "matte",
"Pattern": "knitted"
}
Colocação de curinga
Ao colocar um curinga, você deve seguir estas regras:
- Asterisco único por referência de dados: Apenas um asterisco (
*
) é permitido dentro de uma única referência de dados. - Correspondência completa de segmentos: O asterisco deve sempre corresponder a um segmento inteiro do caminho. Ele não pode ser usado para corresponder apenas a uma parte de um segmento, como
path1.partial*.path3
. - Posicionamento: O asterisco pode ser posicionado em várias partes de uma referência de dados:
- No início:
*.path2.path3
- Aqui, o asterisco corresponde a qualquer segmento que conduza apath2.path3
. - No meio:
path1.*.path3
- Nesta configuração, o asterisco corresponde a qualquer segmento entrepath1
epath3
. - No final:
path1.path2.*
- O asterisco no final corresponde a qualquer segmento que se siga depois depath1.path2
.
- No início:
- O caminho que contém o asterisco deve ser colocado entre aspas simples (
'
).
Curingas de entrada múltipla
JSON original:
{
"Saturation": {
"Max": 0.42,
"Min": 0.67,
},
"Brightness": {
"Max": 0.78,
"Min": 0.93,
},
"Opacity": {
"Max": 0.88,
"Min": 0.91,
}
}
Configuração de mapeamento que usa curingas:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
JSON resultante:
{
"ColorProperties" : {
"Saturation": 0.54,
"Brightness": 0.85,
"Opacity": 0.89
}
}
Se você usar curingas de várias entradas, o asterisco (*
) deverá representar consistentemente o mesmo Captured Segment
em todas as entradas. Por exemplo, quando *
capturas Saturation
no padrão *.Max
, o algoritmo de mapeamento espera que o correspondente Saturation.Min
corresponda ao padrão *.Min
. Aqui, *
é substituído pelo Captured Segment
da primeira entrada, orientando a correspondência para as entradas subsequentes.
Considere este exemplo detalhado:
JSON original:
{
"Saturation": {
"Max": 0.42,
"Min": 0.67,
"Mid": {
"Avg": 0.51,
"Mean": 0.56
}
},
"Brightness": {
"Max": 0.78,
"Min": 0.93,
"Mid": {
"Avg": 0.81,
"Mean": 0.82
}
},
"Opacity": {
"Max": 0.88,
"Min": 0.91,
"Mid": {
"Avg": 0.89,
"Mean": 0.89
}
}
}
Configuração de mapeamento inicial que usa curingas:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
'*.Avg' // - $3
'*.Mean' // - $4
]
Esse mapeamento inicial tenta criar uma matriz (por exemplo, para Opacity
: [0.88, 0.91, 0.89, 0.89]
). Esta configuração falha porque:
- A primeira entrada
*.Max
captura um segmento comoSaturation
. - O mapeamento espera que as entradas subsequentes estejam presentes no mesmo nível:
Saturation.Max
Saturation.Min
Saturation.Avg
Saturation.Mean
Como Avg
e Mean
estão aninhados no Mid
, o asterisco no mapeamento inicial não captura corretamente esses caminhos.
Configuração de mapeamento corrigida:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
'*.Mid.Avg' // - $3
'*.Mid.Mean' // - $4
]
Este mapeamento revisado captura com precisão os campos necessários. Ele especifica corretamente os caminhos para incluir o objeto aninhado Mid
, o que garante que os asteriscos funcionem efetivamente em diferentes níveis da estrutura JSON.
Segunda regra vs. especialização
Ao usar o exemplo anterior de curingas de várias entradas, considere os seguintes mapeamentos que geram dois valores derivados para cada propriedade:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*.Avg'
expression: '($1 + $2) / 2'
}
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*.Diff'
expression: '$1 - $2'
}
Esse mapeamento destina-se a criar dois cálculos separados (Avg
e Diff
) para cada propriedade em ColorProperties
. Este exemplo mostra o resultado:
{
"ColorProperties": {
"Saturation": {
"Avg": 0.54,
"Diff": 0.25
},
"Brightness": {
"Avg": 0.85,
"Diff": 0.15
},
"Opacity": {
"Avg": 0.89,
"Diff": 0.03
}
}
}
Aqui, a segunda definição de mapeamento nas mesmas entradas atua como uma segunda regra para mapeamento.
Agora, considere um cenário em que um campo específico precisa de um cálculo diferente:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
}
{
inputs: [
'Opacity.Max' // - $1
'Opacity.Min' // - $2
]
output: 'ColorProperties.OpacityAdjusted'
expression: '($1 + $2 + 1.32) / 2'
}
Neste caso, o Opacity
campo tem um cálculo único. Duas opções para lidar com esse cenário de sobreposição são:
- Inclua ambos os mapeamentos para
Opacity
. Como os campos de saída são diferentes neste exemplo, eles não substituiriam uns aos outros. - Use a regra mais específica e
Opacity
remova a mais genérica.
Considere um caso especial para os mesmos campos para ajudar a decidir a ação certa:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
}
{
inputs: [
'Opacity.Max' // - $1
'Opacity.Min' // - $2
]
output: ''
}
Um campo vazio output
na segunda definição implica não escrever os campos no registro de saída (removendo Opacity
efetivamente ). Esta configuração é mais do Specialization
que um Second Rule
arquivo .
Resolução de mapeamentos sobrepostos por fluxos de dados:
- A avaliação progride a partir da regra superior na definição do mapeamento.
- Se um novo mapeamento for resolvido para os mesmos campos de uma regra anterior, as seguintes condições se aplicam:
- A
Rank
é calculado para cada entrada resolvida com base no número de segmentos capturados pelo curinga. Por exemplo, se oCaptured Segments
sãoProperties.Opacity
, oRank
é 2. Se for apenasOpacity
, oRank
é 1. Um mapeamento sem curingas tem umRank
de 0. - Se a
Rank
última regra for igual ou superior à regra anterior, um fluxo de dados a tratará como umSecond Rule
. - Caso contrário, o fluxo de dados trata a configuração como um
Specialization
arquivo .
- A
Por exemplo, o mapeamento que direciona Opacity.Max
e Opacity.Min
para uma saída vazia tem um Rank
de 0. Como a segunda regra tem um valor menor Rank
do que a anterior, ela é considerada uma especialização e substitui a regra anterior, que calcularia um valor para Opacity
.
Curingas em conjuntos de dados de contextualização
Agora, vamos ver como os conjuntos de dados de contextualização podem ser usados com curingas por meio de um exemplo. Considere um conjunto de dados chamado position
que contenha o seguinte registro:
{
"Position": "Analyst",
"BaseSalary": 70000,
"WorkingHours": "Regular"
}
Em um exemplo anterior, usamos um campo específico desse conjunto de dados:
inputs: [
'$context(position).BaseSalary'
]
output: 'Employment.BaseSalary'
Esse mapeamento copia BaseSalary
do conjunto de dados de contexto diretamente para a Employment
seção do registro de saída. Se quiser automatizar o processo e incluir todos os campos do conjunto de position
dados na seção, você pode usar curingas Employment
:
inputs: [
'$context(position).*'
]
output: 'Employment.*'
Essa configuração permite um mapeamento dinâmico onde cada campo dentro do position
conjunto de dados é copiado para a Employment
seção do registro de saída:
{
"Employment": {
"Position": "Analyst",
"BaseSalary": 70000,
"WorkingHours": "Regular"
}
}
Último valor conhecido
Você pode acompanhar o último valor conhecido de uma propriedade. Sufixe o campo de entrada com ? $last
para capturar o último valor conhecido do campo. Quando uma propriedade está faltando um valor em uma carga de entrada subsequente, o último valor conhecido é mapeado para a carga útil de saída.
Por exemplo, considere o seguinte mapeamento:
inputs: [
'Temperature ? $last'
]
output: 'Thermostat.Temperature'
Neste exemplo, o último valor conhecido de Temperature
é rastreado. Se uma carga útil de entrada subsequente não contiver um Temperature
valor, o último valor conhecido será usado na saída.