ããã«ã¡ã¯ãHabrã
ç§ã®ååã¯ã¢ã³ãã³ã§ããDomClickã®ãã¯ãã«ã«ãªãŒã ã§ããDomClickã€ã³ãã©ã¹ãã©ã¯ãã£ãSberbankã®å éšãµãŒãã¹ãšããŒã¿ã亀æã§ããããã«ãããã€ã¯ããµãŒãã¹ãäœæããã³ä¿å® ããŠããŸãã
ããã¯ãCamundaããžãã¹ããã»ã¹ãã€ã¢ã°ã©ã ãšã³ãžã³ã®äœ¿çšçµéšã«é¢ããäžé£ã®èšäºã®ç¶ãã§ã ã ååã®èšäºã§ã¯ãBPMNã¹ããŒãã®å€æŽã衚瀺ã§ããBitbucketã®ãã©ã°ã€ã³ã®éçºã«ã€ããŠèª¬æããŸããã仿¥ã¯ãCamundaã䜿çšãããããžã§ã¯ãã®ç£èŠããµãŒãããŒãã£ããŒã«ã®äœ¿ç𿹿³ã«ã€ããŠèª¬æããŸãïŒãã®å Žåãããã¯ããã®Elasticsearchã¹ã¿ã㯠ã§ãïŒ Kibanaãš GrafanaïŒãªãã³ã«Camundaããã®ããã€ãã£ãã - ã³ãã¯ãããã Cockpitã䜿çšããéã«çºçããåé¡ãšãã®è§£æ±ºçã«ã€ããŠèª¬æããŸãã
ãã€ã¯ããµãŒãã¹ãããããããå Žåã¯ããã®äœæ¥ãšçŸåšã®ã¹ããŒã¿ã¹ã«ã€ããŠãã¹ãŠãç¥ããããšèããŠããŸããç£èŠã匷åããã»ã©ãéåžžã®ç¶æ³ãšç·æ¥ã®ç¶æ³ã®äž¡æ¹ã§ããªãªãŒã¹äžãªã©ã«èªä¿¡ãæãŠãããã«ãªããŸãã Elasticsearchã¹ã¿ãã¯ïŒKibanaãšGrafanaãç£èŠããŒã«ãšããŠäœ¿çšããŸãã Kibanaã§ã¯ãã°ã確èªããGrafanaã§ã¯ã¡ããªãã¯ã確èªããŸããããŒã¿ããŒã¹ã«ã¯ãCamundaããã»ã¹ã®å±¥æŽããŒã¿ãå«ãŸããŠããŸããããã¯ããµãŒãã¹ãæ£åžžã«æ©èœããŠãããã©ãããæ©èœããŠããªãå Žåã¯ãã®çç±ãçè§£ããã®ã«ååãªã¯ãã§ããæ¬ ç¹ã¯ã3ã€ã®ç°ãªãå Žæã§ããŒã¿ã確èªããå¿ èŠããããããããåžžã«æç¢ºã«çžäºã«é¢é£ããŠãããšã¯éããªãããšã§ããã€ã³ã·ãã³ãã®è§£æãšåæã«ã¯æéããããå ŽåããããŸããç¹ã«ãããŒã¿ããŒã¹ããã®ããŒã¿ã®åæã®å ŽåïŒCamundaã¯ãæçœãªããŒã¿ã¹ããŒã ãšã¯ã»ã©é ããããããã€ãã®å€æ°ãã·ãªã¢ã«åããã圢åŒã§æ ŒçŽããŸããçè«çã«ã¯ãããžãã¹ããã»ã¹ãç£èŠããããã®CamundaããŒã«ã§ããCockpitã¯ãã¿ã¹ã¯ãç°¡åã«ããããšãã§ããŸãã
ã³ãã¯ãããã€ã³ã¿ãŒãã§ãŒã¹ã
äž»ãªåé¡ã¯ãCockpitãã«ã¹ã¿ã URLãåŠçã§ããªãããšã§ãã圌ãã®ãã©ãŒã©ã ã§ã¯ããã«ã€ããŠå€ãã®èŠæ±ããããŸããããããŸã§ã®ãšãããç®±ããåºããŠãã®ãããªæ©èœã¯ãããŸãããå¯äžã®æ¹æ³ã¯èªåã§ããããšã§ããCockpitã«ã¯SringBootèªåæ§æ
CamundaBpmWebappAutoConfiguration
ããããããç¬èªã®ãã®ãšäº€æããå¿ èŠããããŸãã
CamundaBpmWebappInitializer
CockpitWebãã£ã«ã¿ãŒãšãµãŒãã¬ãããåæåããã¡ã€ã³Beanã«é¢å¿ã ãããŸãã
ã¡ã€ã³ãã£ã«ã¿ãŒïŒ
LazyProcessEnginesFilter
ïŒã«ããããæ©èœããURLã«é¢ããæ å ± ãæž¡ãå¿ èŠããã
ResourceLoadingProcessEnginesFilter
ãŸãããŸãã-JSããã³CSSãªãœãŒã¹ãæäŸããURLã«é¢ããæ å ±ãæž¡ãå¿ èŠããã ãŸãã
ãããè¡ãã«ã¯ãå®è£ ã§æ¬¡
CamundaBpmWebappInitializer
ã®è¡ã倿ŽããŸãã
registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")
ãªã³ïŒ
registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)
servicePath
ã«ã¹ã¿ã URLã§ãããŸã£ããåãããã«
CustomLazyProcessEnginesFilter
ãå®è£ ã 瀺ããŸã
ResourceLoadingProcessEnginesFilter
ã
class CustomLazyProcessEnginesFilter:
LazyDelegateFilter<ResourceLoaderDependingFilter>
(CustomResourceLoadingProcessEnginesFilter::class.java)
CustomResourceLoadingProcessEnginesFilter
å ããŠ
servicePath
ãæã ã¯ã¯ã©ã€ã¢ã³ãåŽã«äžããããšãèšç»ããŠãªãœãŒã¹ãžã®ãã¹ãŠã®ãªã³ã¯ãžïŒ
override fun replacePlaceholder(
data: String,
appName: String,
engineName: String,
contextPath: String,
request: HttpServletRequest,
response: HttpServletResponse
) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")
.replace(BASE_PLACEHOLDER,
String.format("%s$servicePath/app/%s/%s/",
contextPath, appName, engineName))
.replace(PLUGIN_PACKAGES_PLACEHOLDER,
createPluginPackagesString(appName, contextPath))
.replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,
createPluginDependenciesString(appName))
ããã§ãã³ãã¯ãããããªã¯ãšã¹ãããªãã¹ã³ããŠãªãœãŒã¹ãæäŸããURLãæå®ã§ããŸãã
ããããããã¯ããã»ã©åçŽãªããšã§ã¯ãããŸãããããã®å ŽåãOAuth2ãšJWTã®ä»£ããã«ãããŒã«ã«ãã£ãã·ã¥ã«æ ŒçŽãããŠããå€ãè¯ãjsessionidã䜿çšããããããCockpitã¯ã¢ããªã±ãŒã·ã§ã³ã®è€æ°ã®ã€ã³ã¹ã¿ã³ã¹ïŒããšãã°ãKubernetesãããïŒã§ãã®ãŸãŸã§ã¯æ©èœããŸãããã€ãŸããCamundaã«æ¥ç¶ãããCockpitã«ãã°ã€ã³ããããšãããšãäžåºŠã«è€æ°ã®ã€ã³ã¹ã¿ã³ã¹ã§èµ·åãããåãjsessionidãçºè¡ãããŸãããã®åŸãã¯ã©ã€ã¢ã³ãããã®ãªãœãŒã¹ã®èŠæ±ããšã«ã確çxã§401ãšã©ãŒãçºçããå¯èœæ§ããããŸããããã§ãx =ïŒ1-1 / number_podsïŒãããªãã¯ããã«ã€ããŠäœãã§ããŸããïŒã³ãã¯ããããåãã§ã
CamundaBpmWebappInitializer
èªèšŒãã£ã«ã¿ãŒã宣èšãããããŒã¯ã³ã®åŠçããã¹ãŠè¡ãããŸããããªãã¯ãããããªãèªèº«ã®ãã®ãšåãæ¿ããå¿ èŠããããŸãããã®äžã§ãã»ãã·ã§ã³ãã£ãã·ã¥ããjsessionidãååŸããæ¿èªèŠæ±ã®å Žåã¯ããŒã¿ããŒã¹ã«ä¿åãããã以å€ã®å Žåã¯ããŒã¿ããŒã¹ã«å¯ŸããŠãã®æå¹æ§ã確èªããŸããããã§ã䟿å©ãªCockpitã°ã©ãã£ã«ã«ã€ã³ã¿ãŒãã§ã€ã¹ãä»ããŠããžãã¹ããã»ã¹ããšã«ã€ã³ã·ãã³ããç£èŠã§ããããã«ãªããŸããããã®ã€ã³ã¿ãŒãã§ã€ã¹ã§ã¯ãã€ã³ã·ãã³ãçºçæã«ããã»ã¹ã«ãã£ãã¹ã¿ãã¯ãã¬ãŒã¹ãšã©ãŒãšå€æ°ãããã«ç¢ºèªã§ããŸãã
ã€ã³ã·ãã³ãã®åå ãäŸå€ã®ã¹ã¿ãã¯ãã¬ãŒã¹ããæãããªå ŽåãCockpitã䜿çšãããšãã€ã³ã·ãã³ãã®åæã«ãããæéã3ã5åã«ççž®ã§ããŸããç§ãå ¥ã£ãŠãããã»ã¹ã®ã€ã³ã·ãã³ãã調ã¹ãã¹ã¿ãã¯ãã¬ãŒã¹ã倿°ãããã³åºæ¥äžããã調ã¹ãŸãããã€ã³ã·ãã³ãã¯æŽçãããJIRAã«ãã°ãå ¥ããŸããããšé転ããŸãããããããç¶æ³ãããå°ãè€éãªå Žåãã¹ã¿ãã¯ãã¬ãŒã¹ã以åã®ãšã©ãŒã®çµæã§ããããããã»ã¹ãã€ã³ã·ãã³ãããŸã£ããäœæããã«çµäºããå ŽåïŒã€ãŸããæè¡çã«ã¯ãã¹ãŠããŸããããŸããããããžãã¹ããžãã¯ã®èгç¹ããã¯ãééã£ãããŒã¿ã転éãããããããã»ã¹ãééã£ããã©ã³ãã«æ²¿ã£ãŠé²ãã å Žåã¯ã©ããªããŸããïŒã¹ããŒã ïŒããã®å ŽåãããäžåºŠKibanaã«ç§»åãããã°ã確èªããŠãããããCamundaããã»ã¹ã«æ¥ç¶ããå¿ èŠããããŸããããããéåžžã«æéãããããŸãããã¡ãããçŸåšã®ããã»ã¹ã®UUIDãšçŸåšã®BPMNãã€ã¢ã°ã©ã èŠçŽ ã®IDïŒactivityIdïŒãåãã°ã«è¿œå ã§ããŸãããããã«ã¯å€ãã®æåäœæ¥ãå¿ èŠã§ããã³ãŒãããŒã¹ãä¹±éã«ãªããã³ãŒãã¬ãã¥ãŒãè€éã«ãªããŸãããã®ããã»ã¹å šäœãèªååã§ããŸãã
Sleuthãããžã§ã¯ãã§ ã¯ãäžæã®èå¥åïŒãã®å Žåã¯ããã»ã¹UUIDïŒã䜿çšããŠãã°ããã¬ãŒã¹ã§ããŸããSleuthã³ã³ããã¹ãã®èšå®ã«ã€ããŠã¯ãããã¥ã¡ã³ãã§è©³ãã説æãããŠããŸããããã§ã¯ãCamundaã§Sleuthã³ã³ããã¹ããéå§ããæ¹æ³ã®ã¿ã瀺ããŸãã
ãŸãã
customPreBPMNParseListeners
çŸåšã®
processEngine
Camundaã«ç»é²ããå¿ èŠããããŸã ããªã¹ããŒã§ãã¡ãœããããªãŒããŒã©ã€ãããŸã
parseStartEvent
ïŒãããã¬ãã«ããã»ã¹
parseServiceTask
ã®éå§ã€ãã³ãã«ãªã¹ããŒã远å ããŸãïŒããã³ ïŒéå§ã€ãã³ãã«ãªã¹ããŒã远å ããŸã
ServiceTask
ïŒã
æåã®ã±ãŒã¹ã§ã¯ãSleuthã³ã³ããã¹ããäœæããŸãã
customContext[X_B_3_TRACE_ID] = businessKey
customContext[X_B_3_SPAN_ID] = businessKeyHalf
customContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalf
customContext[X_B_3_SAMPLED] = "0"
val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()
.extractor(OrcGetter())
.extract(customContext)
val newSpan: Span = tracing.tracer().nextSpan(contextFlags)
tracing.currentTraceContext().newScope(newSpan.context())
...ãããŠãããããžãã¹ããã»ã¹å€æ°ã«ä¿åããŸãïŒ
execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)
2çªç®ã®ã±ãŒã¹ã§ã¯ã次ã®å€æ°ãã埩å ããŸãã
val storedContext = execution
.getVariableTyped<ObjectValue>(TRACING_CONTEXT)
.getValue(HashMap::class.java) as HashMap<String?, String?>
val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()
.extractor(OrcGetter())
.extract(storedContext)
val newSpan: Span = tracing.tracer().nextSpan(contextFlags)
tracing.currentTraceContext().newScope(newSpan.context())
activityId
ïŒçŸåšã®BPMNèŠçŽ ã®IDïŒã
activityName
ïŒãã®ããžãã¹åïŒã
scenarioId
ïŒããžãã¹ããã»ã¹å³ã®ID ïŒ ãªã©ã®è¿œå ãã©ã¡ãŒã¿ãŒãšãšãã«ãã°ããã¬ãŒã¹ããå¿ èŠããããŸã ããã®æ©èœã¯ãSleuth 3ã®ãªãªãŒã¹ã§ã®ã¿ç»å ŽããŸããã
ãã©ã¡ãŒã¿ãŒããšã«ã次ã®ããšã宣èšããå¿ èŠããããŸã
BaggageField
ã
companion object {
val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")
val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")
val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")
val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")
}
次ã«ããããã®ãã£ãŒã«ããåŠçãã3ã€ã®Beanã宣èšããŸãã
@Bean
open fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =
BaggagePropagationCustomizer { fb ->
fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))
fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))
fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))
fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))
}
/** [BaggageField.updateValue] now flushes to MDC */
@Bean
open fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =
CorrelationScopeCustomizer { builder ->
builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())
}
/** [.BUSINESS_PROCESS] is added as a tag only in the first span. */
@Bean
open fun tagBusinessProcessOncePerProcess(): SpanHandler =
object : SpanHandler() {
override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {
if (context.isLocalRoot && cause == Cause.FINISHED) {
Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)
}
return true
}
}
次ã«ã远å ã®ãã£ãŒã«ããSleuthã³ã³ããã¹ãã«ä¿åã§ããŸãã
HEADER_BUSINESS_KEY.updateValue(businessKey) HEADER_SCENARIO_ID.updateValue(scenarioId) HEADER_ACTIVITY_NAME.updateValue(activityName) HEADER_ACTIVITY_ID.updateValue(activityId)
ããŒããšã«ããžãã¹ããã»ã¹ããšã«åå¥ã«ãã°ã衚瀺ã§ããå Žåãã€ã³ã·ãã³ãã®åæã¯ã¯ããã«é«éã§ãã確ãã«ãããããšã³ãã¯ããããåãæ¿ããå¿ èŠããããŸããã€ãŸãã1ã€ã®UIå ã§ããããçµã¿åãããå¿ èŠããããŸãã
ãããŠããã®ãããªæ©äŒããããŸãã Cockpitã¯ã«ã¹ã¿ã æ¡åŒµæ©èœïŒãã©ã°ã€ã³ïŒããµããŒãããŸããKibanaã«ã¯Rest APIãšããããæäœããããã®2ã€ã®ã¯ã©ã€ã¢ã³ãã©ã€ãã©ãªïŒ elasticsearch-rest-low-level-clientãš elasticsearch-rest-high-level-clientïŒããããŸãã
ãã©ã°ã€ã³ã¯ãcamunda-release-parentã¢ãŒãã£ãã¡ã¯ãããç¶æ¿ãããMavenãããžã§ã¯ãã§ãããJax-RSããã¯ãšã³ããšAngularJSããã³ããšã³ããåããŠããŸããã¯ããAngularã§ã¯ãªãAngularJSã§ãã
ã³ãã¯ãããã®è©³çް ãã®ããã®ãã©ã°ã€ã³ãäœæããæ¹æ³ã«é¢ããããã¥ã¡ã³ãã
ããã³ããšã³ãã«ãã°ã衚瀺ããã«ã¯ãããã»ã¹å®çŸ©æ å ±ããŒãžïŒcockpit.processDefinition.runtime.tabïŒã®ã¿ãããã«ãšããã»ã¹ã€ã³ã¹ã¿ã³ã¹ãã¥ãŒããŒãžïŒcockpit.processInstance.runtime.tabïŒã«é¢å¿ãããããšã ããæç¢ºã«ããŸããã³ã³ããŒãã³ããç»é²ããŸãã
ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {
id: 'process-definition-runtime-tab-log',
priority: 20,
label: 'Logs',
url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'
});
ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {
id: 'process-instance-runtime-tab-log',
priority: 20,
label: 'Logs',
url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'
});
ã³ãã¯ãããã«ã¯ãæ å ±ã衚圢åŒã§è¡šç€ºããããã®UIã³ã³ããŒãã³ãããããŸãããããã¥ã¡ã³ãã«ã¯ããã«ã€ããŠã¯èšèŒãããŠããããã³ãã¯ãããã®ãœãŒã¹ã³ãŒããèªãã ãã§æ å ±ãšãã®äœ¿çšæ³ãèŠã€ããããšãã§ããŸããã€ãŸããã³ã³ããŒãã³ãã®äœ¿çšã¯æ¬¡ã®ããã«ãªããŸãã
<div cam-searchable-area (1)
config="searchConfig" (2)
on-search-change="onSearchChange(query, pages)" (3)
loading-state="âLoading...â" (4)
text-empty="Not found"(5)
storage-group="'ANU'"
blocked="blocked">
<div class="col-lg-12 col-md-12 col-sm-12">
<table class="table table-hover cam-table">
<thead cam-sortable-table-header (6)
default-sort-by="time"
default-sort-order="asc" (7)
sorting-id="admin-sorting-logs"
on-sort-change="onSortChanged(sorting)"
on-sort-initialized="onSortInitialized(sorting)" (8)>
<tr>
<!-- headers -->
</tr>
</thead>
<tbody>
<!-- table content -->
</tbody>
</table>
</div>
</div>
- æ€çŽ¢ã³ã³ããŒãã³ãã宣èšãã屿§ã
- ã³ã³ããŒãã³ãæ§æãããã«æ¬¡ã®æ§é ããããŸãã
tooltips = { // , // 'inputPlaceholder': 'Add criteria', 'invalid': 'This search query is not valid', 'deleteSearch': 'Remove search', 'type': 'Type', 'name': 'Property', 'operator': 'Operator', 'value': 'Value' }, operators = { //, , 'string': [ {'key': 'eq', 'value': '='}, {'key': 'like','value': 'like'} ] }, types = [// , , businessKey { 'id': { 'key': 'businessKey', 'value': 'Business Key' }, 'operators': [ {'key': 'eq', 'value': '='} ], enforceString: true } ]
- ããŒã¿æ€çŽ¢æ©èœã¯ãæ€çŽ¢ãã©ã¡ãŒã¿ã®å€æŽæãšååããŠã³ããŒãæã®äž¡æ¹ã§äœ¿çšãããŸãã
- ããŒã¿ã®èªã¿èŸŒã¿äžã«è¡šç€ºããã¡ãã»ãŒãžã
- äœãèŠã€ãããªãã£ãå Žåã«è¡šç€ºããã¡ãã»ãŒãžã
- ã«ãã¯ã¢ããããŒã¿ãããã³ã°ããŒãã«ã宣èšãã屿§ã
- ããã©ã«ãã®ãœãŒããã£ãŒã«ããšã¿ã€ãã
- ãœãŒãæ©èœã
ããã¯ãšã³ãã§ã¯ãKibanaAPIãšé£æºããããã«ã¯ã©ã€ã¢ã³ããæ§æããå¿ èŠããããŸãããããè¡ãã«ã¯ãelasticsearch-rest-high-level-clientã©ã€ãã©ãªã®RestHighLevelClientã䜿çšããã ãã§ããããã§ãKibanaãžã®ãã¹ãèªèšŒããŒã¿ïŒãã°ã€ã³ãšãã¹ã¯ãŒããæå®ããæå·åãããã³ã«ã䜿çšããå Žåã¯ãé©åãªX509TrustManagerå®è£ ãæå®ããå¿ èŠããããŸãã
æ€çŽ¢ã¯ãšãª
QueryBuilders.boolQuery()
ãäœæããã«ã¯ ãããã䜿çšããŸããããã«ãããæ¬¡ã®åœ¢åŒã®è€éãªã¯ãšãªãäœæã§ããŸãã
val boolQueryBuilder = QueryBuilders.boolQuery();
KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(key, value))
);
if (!StringUtils.isEmpty(businessKey)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));
}
if (!StringUtils.isEmpty(procDefKey)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));
}
if (!StringUtils.isEmpty(activityId)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));
}
ããã§ãCockpitããçŽæ¥ãããã»ã¹ããšããã³ã¢ã¯ãã£ããã£ããšã«åå¥ã«ãã°ã衚瀺ã§ããŸããæ¬¡ã®ããã«ãªããŸãã
ã³ãã¯ãããã€ã³ã¿ãŒãã§ãŒã¹ã§ãã°ã衚瀺ããããã®ã¿ãã
ãããããããžã§ã¯ãã®éçºã®ããã®ã¢ã€ãã¢ã®èšç»ã§ã¯ãããã§æ¢ãŸãããšã¯ã§ããŸããããŸããæ€çŽ¢æ©èœãæ¡åŒµããŸããå€ãã®å Žåãã€ã³ã·ãã³ãã®è§£æã®éå§æã«ãæå ã«ããžãã¹ããŒããã»ã¹ã¯ãããŸããããä»ã®ããŒãã©ã¡ãŒã¿ã«é¢ããæ å ±ãããããããã®æ€çŽ¢ãã«ã¹ã¿ãã€ãºããæ©èœã远å ãããšäŸ¿å©ã§ãããŸãããã°ã«é¢ããæ å ±ã衚瀺ãããããŒãã«ã¯ã€ã³ã¿ã©ã¯ãã£ãã§ã¯ãããŸãããããŒãã«ã®å¯Ÿå¿ããè¡ãã¯ãªãã¯ããŠãå¿ èŠãªããã»ã¹ã€ã³ã¹ã¿ã³ã¹ã«ç§»åããæ¹æ³ã¯ãããŸãããèŠããã«ãéçºã®äœå°ããããŸããïŒé±æ«ãçµããæ¬¡ç¬¬ããããžã§ã¯ãã®Githubãžã®ãªã³ã¯ãæçš¿ããããã«èå³ã®ãããã¹ãŠã®äººãæåŸ ããŸããïŒ